diff --git a/avro-android/src/main/AndroidManifest.xml b/avro-android/src/main/AndroidManifest.xml
index d30de6534..9b65eb06c 100644
--- a/avro-android/src/main/AndroidManifest.xml
+++ b/avro-android/src/main/AndroidManifest.xml
@@ -1,2 +1 @@
-
diff --git a/avro-android/src/test/java/org/apache/avro/specific/SpecificRecordTest.java b/avro-android/src/test/java/org/apache/avro/specific/SpecificRecordTest.java
index be69b4e2d..f35ee572b 100644
--- a/avro-android/src/test/java/org/apache/avro/specific/SpecificRecordTest.java
+++ b/avro-android/src/test/java/org/apache/avro/specific/SpecificRecordTest.java
@@ -1,7 +1,6 @@
package org.apache.avro.specific;
import static org.junit.Assert.assertEquals;
-import static org.radarbase.producer.rest.AvroDataMapperFactory.IDENTITY_MAPPER;
import org.apache.avro.Schema;
import org.apache.avro.SchemaValidationException;
@@ -17,8 +16,8 @@
import org.radarbase.data.AvroEncoder;
import org.radarbase.data.Record;
import org.radarbase.data.RemoteSchemaEncoder;
-import org.radarbase.producer.rest.AvroDataMapperFactory;
-import org.radarbase.producer.rest.ParsedSchemaMetadata;
+import org.radarbase.producer.avro.AvroDataMapperFactory;
+import org.radarbase.producer.schema.ParsedSchemaMetadata;
import org.radarbase.topic.AvroTopic;
import org.radarcns.active.questionnaire.Answer;
import org.radarcns.active.questionnaire.Questionnaire;
@@ -64,30 +63,28 @@ public void serializationTest() throws IOException {
actual = avroDeserializer.deserialize(input);
}
- assertEquals(record.key, actual.key);
- assertEquals(record.value, actual.value);
+ assertEquals(record.getKey(), actual.getKey());
+ assertEquals(record.getValue(), actual.getValue());
}
@Test
public void avroBinaryEncodingTest() throws IOException, SchemaValidationException {
AvroEncoder encoder = new RemoteSchemaEncoder(true);
- AvroEncoder.AvroWriter accelerationWriter = encoder.writer(PhoneAcceleration.getClassSchema(), PhoneAcceleration.class);
ParsedSchemaMetadata schemaMetadata = new ParsedSchemaMetadata(1, 1, new Schema.Parser().parse("{\"name\":\"Test\",\"type\":\"record\",\"fields\":[{\"name\":\"time\",\"type\":\"double\"},{\"name\":\"timeReceived\",\"type\":\"double\"},{\"name\":\"x\",\"type\":\"float\"},{\"name\":\"y\",\"type\":\"float\"},{\"name\":\"z\",\"type\":\"float\"},{\"name\":\"def\",\"type\":[\"null\",\"string\"],\"default\":null}]}"));
- accelerationWriter.setReaderSchema(schemaMetadata);
- byte[] result = accelerationWriter.encode(record.value);
+ AvroEncoder.AvroWriter accelerationWriter = encoder.writer(PhoneAcceleration.getClassSchema(), PhoneAcceleration.class, schemaMetadata.getSchema());
+ byte[] result = accelerationWriter.encode(record.getValue());
AvroDecoder decoder = new AvroDatumDecoder(SpecificData.get(), true);
AvroDecoder.AvroReader accelerationReader = decoder.reader(PhoneAcceleration.getClassSchema(), PhoneAcceleration.class);
GenericRecord acceleration = accelerationReader.decode(result);
- assertEquals(record.value, acceleration);
+ assertEquals(record.getValue(), acceleration);
}
@Test
public void avroJsonEncodingTest() throws IOException, SchemaValidationException {
AvroEncoder encoder = new RemoteSchemaEncoder(false);
- AvroEncoder.AvroWriter accelerationWriter = encoder.writer(Questionnaire.getClassSchema(), GenericRecord.class);
ParsedSchemaMetadata schemaMetadata = new ParsedSchemaMetadata(1, 1, Questionnaire.getClassSchema());
- accelerationWriter.setReaderSchema(schemaMetadata);
+ AvroEncoder.AvroWriter accelerationWriter = encoder.writer(Questionnaire.getClassSchema(), GenericRecord.class, schemaMetadata.getSchema());
List list = new ArrayList<>(2);
list.add(new Answer("qid1", 1, 0.4, 0.5));
list.add(new Answer("qid2", "a", 0.6, 0.7));
@@ -107,9 +104,8 @@ public void avroMiscEncodingTest() throws IOException, SchemaValidationException
AvroEncoder encoder = new RemoteSchemaEncoder(false);
Schema schema = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"test\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"},{\"name\":\"bool\",\"type\":\"boolean\"},{\"name\":\"unionFixed\",\"type\":[\"null\",{\"type\":\"fixed\",\"size\":4,\"name\":\"four\"}],\"default\":null},{\"name\":\"map\",\"type\":{\"type\":\"map\", \"values\":{\"type\":\"int\"}}}]}");
- AvroEncoder.AvroWriter accelerationWriter = encoder.writer(schema, GenericRecord.class);
ParsedSchemaMetadata schemaMetadata = new ParsedSchemaMetadata(1, 1, schema);
- accelerationWriter.setReaderSchema(schemaMetadata);
+ AvroEncoder.AvroWriter accelerationWriter = encoder.writer(schema, GenericRecord.class, schemaMetadata.getSchema());
GenericRecordBuilder record = new GenericRecordBuilder(schema);
record.set("bytes", ByteBuffer.wrap(new byte[] {1, 2, 3, 4}));
Map map = new HashMap<>();
@@ -128,12 +124,18 @@ public void avroMiscEncodingTest() throws IOException, SchemaValidationException
assertEquals(record.build(), result);
}
- @Test
- public void dataMappingTest() throws SchemaValidationException {
- Schema answer = Answer.SCHEMA$;
- Schema serverAnswer = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Answer\",\"namespace\":\"org.radarcns.active.questionnaire\",\"fields\":[{\"name\":\"questionId\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"value\",\"type\":[\"int\",\"string\",\"double\"]},{\"name\":\"startTime\",\"type\":\"double\"},{\"name\":\"endTime\",\"type\":\"double\"}]}");
+ /**
+ *Commenting now because IDENTITY_MAPPER is now a property of a private class.
+ * This will be replaced with similar functionality
+ *
+ * @Test
+ * public void dataMappingTest() throws SchemaValidationException {
+ * Schema answer = Answer.SCHEMA$;
+ * Schema serverAnswer = new Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Answer\",\"namespace\":\"org.radarcns.active.questionnaire\",\"fields\":[{\"name\":\"questionId\",\"type\":[\"null\",\"string\"],\"default\":null},{\"name\":\"value\",\"type\":[\"int\",\"string\",\"double\"]},{\"name\":\"startTime\",\"type\":\"double\"},{\"name\":\"endTime\",\"type\":\"double\"}]}");
+ *
+ * assertEquals(IDENTITY_MAPPER, AvroDataMapperFactory.createMapper(answer, serverAnswer, null));
+ * assertEquals(IDENTITY_MAPPER, AvroDataMapperFactory.createMapper(serverAnswer, answer, null));
+ * }
+ */
- assertEquals(IDENTITY_MAPPER, AvroDataMapperFactory.get().createMapper(answer, serverAnswer, null));
- assertEquals(IDENTITY_MAPPER, AvroDataMapperFactory.get().createMapper(serverAnswer, answer, null));
- }
}
diff --git a/build.gradle b/build.gradle
index 796180a8c..85e15b083 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,6 +26,7 @@ buildscript {
classpath "com.github.bjoernq:unmockplugin:$unmock_plugin_version"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:$dokka_android_gradle_plugin_version"
classpath("org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version")
+ classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
@@ -45,6 +46,9 @@ allprojects {
group = 'org.radarbase'
ext.versionCode = 54
+ configurations.all {
+ exclude group: 'org.apache.avro', module: 'avro'
+ }
}
subprojects {
diff --git a/gradle.properties b/gradle.properties
index 45f93edfe..8e434a535 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -37,7 +37,7 @@ dokka_version=1.9.20
publish_plugin_version=2.0.0
versions_plugin_version=0.51.0
-radar_commons_version=0.15.0
+radar_commons_version=1.1.2
radar_schemas_commons_version=0.8.11
radar_faros_sdk_version=0.1.0
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index c1962a79e..943f0cbfa 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradlew b/gradlew
index aeb74cbb4..3306bcaac 100755
--- a/gradlew
+++ b/gradlew
@@ -85,6 +85,9 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationState.kt b/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationState.kt
index 84dff7a2f..2219fc9fb 100755
--- a/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationState.kt
+++ b/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationState.kt
@@ -16,14 +16,14 @@
package org.radarbase.monitor.application
-import org.radarbase.android.kafka.ServerStatusListener
+import org.radarbase.android.kafka.ServerStatus
import org.radarbase.android.source.BaseSourceState
import java.util.concurrent.ConcurrentHashMap
class ApplicationState : BaseSourceState() {
@set:Synchronized
- var serverStatus: ServerStatusListener.Status? = null
- @Synchronized get() = field ?: ServerStatusListener.Status.DISCONNECTED
+ var serverStatus: ServerStatus? = null
+ @Synchronized get() = field ?: ServerStatus.DISCONNECTED
@get:Synchronized
var recordsSent = 0L
diff --git a/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationStatusManager.kt b/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationStatusManager.kt
index dc5a676d8..a3532170c 100755
--- a/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationStatusManager.kt
+++ b/plugins/radar-android-application-status/src/main/java/org/radarbase/monitor/application/ApplicationStatusManager.kt
@@ -21,43 +21,73 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.SystemClock
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import org.radarbase.android.data.DataCache
-import org.radarbase.android.kafka.ServerStatusListener
+import org.radarbase.android.data.TableDataHandler
+import org.radarbase.android.kafka.TopicSendReceipt
import org.radarbase.android.source.AbstractSourceManager
-import org.radarbase.android.source.SourceService.Companion.CACHE_RECORDS_UNSENT_NUMBER
-import org.radarbase.android.source.SourceService.Companion.CACHE_TOPIC
-import org.radarbase.android.source.SourceService.Companion.SERVER_RECORDS_SENT_NUMBER
-import org.radarbase.android.source.SourceService.Companion.SERVER_RECORDS_SENT_TOPIC
-import org.radarbase.android.source.SourceService.Companion.SERVER_STATUS_CHANGED
import org.radarbase.android.source.SourceStatusListener
-import org.radarbase.android.util.*
+import org.radarbase.android.util.ChangeRunner
+import org.radarbase.android.util.CoroutineTaskExecutor
+import org.radarbase.android.util.OfflineProcessor
+import org.radarbase.android.util.takeTrimmedIfNotEmpty
import org.radarbase.monitor.application.ApplicationStatusService.Companion.UPDATE_RATE_DEFAULT
import org.radarcns.kafka.ObservationKey
-import org.radarcns.monitor.application.*
+import org.radarcns.monitor.application.ApplicationDeviceInfo
+import org.radarcns.monitor.application.ApplicationExternalTime
+import org.radarcns.monitor.application.ApplicationRecordCounts
+import org.radarcns.monitor.application.ApplicationServerStatus
+import org.radarcns.monitor.application.ApplicationTimeZone
+import org.radarcns.monitor.application.ApplicationUptime
+import org.radarcns.monitor.application.ExternalTimeProtocol
+import org.radarcns.monitor.application.OperatingSystem
+import org.radarcns.monitor.application.ServerStatus
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
-import java.util.*
+import java.util.TimeZone
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS
class ApplicationStatusManager(
- service: ApplicationStatusService
+ service: ApplicationStatusService
) : AbstractSourceManager(service) {
- private val serverTopic: DataCache = createCache("application_server_status", ApplicationServerStatus())
- private val recordCountsTopic: DataCache = createCache("application_record_counts", ApplicationRecordCounts())
- private val uptimeTopic: DataCache = createCache("application_uptime", ApplicationUptime())
- private val ntpTopic: DataCache = createCache("application_external_time", ApplicationExternalTime())
- private val timeZoneTopic: DataCache = createCache("application_time_zone", ApplicationTimeZone())
- private val deviceInfoTopic: DataCache = createCache("application_device_info", ApplicationDeviceInfo())
+ private val serverTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("application_server_status", ApplicationServerStatus())
+ }
+ private val recordCountsTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("application_record_counts", ApplicationRecordCounts())
+ }
+ private val uptimeTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("application_uptime", ApplicationUptime())
+ }
+ private val ntpTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("application_external_time", ApplicationExternalTime())
+ }
+ private val timeZoneTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("application_time_zone", ApplicationTimeZone())
+ }
+ private val deviceInfoTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("application_device_info", ApplicationDeviceInfo())
+ }
private val processor: OfflineProcessor
private val creationTimeStamp: Long = SystemClock.elapsedRealtime()
private val sntpClient: SntpClient = SntpClient()
private val prefs: SharedPreferences = service.getSharedPreferences(ApplicationStatusManager::class.java.name, Context.MODE_PRIVATE)
private var tzProcessor: OfflineProcessor? = null
+ private val tzIntervalMutex: Mutex = Mutex()
+
+ private val applicationStatusExecutor: CoroutineTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
@get:Synchronized
@set:Synchronized
@@ -74,9 +104,9 @@ class ApplicationStatusManager(
private lateinit var tzOffsetCache: ChangeRunner
private lateinit var deviceInfoCache: ChangeRunner
- private var serverStatusReceiver: BroadcastRegistration? = null
- private var serverRecordsReceiver: BroadcastRegistration? = null
- private var cacheReceiver: BroadcastRegistration? = null
+ private var cachedRecordTrackJob: Job? = null
+ private var serverRecordsSentJob: Job? = null
+ private var serverStatusJob: Job? = null
init {
name = service.getString(R.string.applicationServiceDisplayName)
@@ -98,6 +128,7 @@ class ApplicationStatusManager(
override fun start(acceptableIds: Set) {
status = SourceStatusListener.Status.READY
+ applicationStatusExecutor.start()
processor.start {
val osVersionCode: Int = this.prefs.getInt("operatingSystemVersionCode", -1)
val appVersionCode: Int = this.prefs.getInt("appVersionCode", -1)
@@ -133,20 +164,33 @@ class ApplicationStatusManager(
tzProcessor?.start()
logger.info("Starting ApplicationStatusManager")
- LocalBroadcastManager.getInstance(service).apply {
- serverStatusReceiver = register(SERVER_STATUS_CHANGED) { _, intent ->
- state.serverStatus = ServerStatusListener.Status.values()[intent.getIntExtra(SERVER_STATUS_CHANGED, 0)]
- }
- serverRecordsReceiver = register(SERVER_RECORDS_SENT_TOPIC) { _, intent ->
- val numberOfRecordsSent = intent.getLongExtra(SERVER_RECORDS_SENT_NUMBER, 0)
- state.addRecordsSent(numberOfRecordsSent.coerceAtLeast(0))
- }
- cacheReceiver = register(CACHE_TOPIC) { _, intent ->
- val topic = intent.getStringExtra(CACHE_TOPIC) ?: return@register
- val records = intent.getLongExtra(CACHE_RECORDS_UNSENT_NUMBER, 0)
- state.cachedRecords[topic] = records.coerceAtLeast(0)
+ with(applicationStatusExecutor) {
+ service.dataHandler?.let { handler ->
+ cachedRecordTrackJob = service.lifecycleScope.launch(Dispatchers.Default) {
+ handler.numberOfRecords
+ .collect { records: TableDataHandler.CacheSize ->
+ val topic = records.topicName
+ val noOfRecords = records.numberOfRecords
+ logger.trace("Topic {} has {} records in cache", topic, noOfRecords)
+ state.cachedRecords[topic] = noOfRecords.coerceAtLeast(0)
+ }
+ }
+
+ serverRecordsSentJob = service.lifecycleScope.launch(Dispatchers.Default) {
+ handler.recordsSent.collect { sent: TopicSendReceipt ->
+ logger.trace("Topic {} sent {} records", sent.topic, sent.numberOfRecords)
+ state.addRecordsSent(sent.numberOfRecords)
+ }
+ }
+
+ serverStatusJob = service.lifecycleScope.launch(Dispatchers.Default) {
+ handler.serverStatus.collect {
+ state.serverStatus = it
+ logger.trace("Updated Server Status to {}", it)
+ }
+ }
+ }
}
- }
status = SourceStatusListener.Status.CONNECTED
}
@@ -172,34 +216,37 @@ class ApplicationStatusManager(
}
// using versionCode
- private fun processDeviceInfo() {
+ private suspend fun processDeviceInfo() {
deviceInfoCache.applyIfChanged(currentApplicationInfo) { deviceInfo ->
- send(
- deviceInfoTopic,
- ApplicationDeviceInfo(
- currentTime,
- deviceInfo.manufacturer,
- deviceInfo.model,
- OperatingSystem.ANDROID,
- deviceInfo.osVersion,
- deviceInfo.osVersionCode,
- deviceInfo.appVersion,
- deviceInfo.appVersionCode,
- ),
- )
-
- prefs.edit().apply {
- putString("manufacturer", deviceInfo.manufacturer)
- putString("model", deviceInfo.model)
- putString("operatingSystemVersion", deviceInfo.osVersion)
- putInt("operatingSystemVersionCode", deviceInfo.osVersionCode ?: -1)
- putString("appVersion", deviceInfo.appVersion)
- putInt("appVersionCode", deviceInfo.appVersionCode ?: -1)
- }.apply()
+ applicationStatusExecutor.execute {
+ send(
+ deviceInfoTopic.await(),
+ ApplicationDeviceInfo(
+ currentTime,
+ deviceInfo.manufacturer,
+ deviceInfo.model,
+ OperatingSystem.ANDROID,
+ deviceInfo.osVersion,
+ deviceInfo.osVersionCode,
+ deviceInfo.appVersion,
+ deviceInfo.appVersionCode,
+ ),
+ )
+ withContext(Dispatchers.IO) {
+ prefs.edit().apply {
+ putString("manufacturer", deviceInfo.manufacturer)
+ putString("model", deviceInfo.model)
+ putString("operatingSystemVersion", deviceInfo.osVersion)
+ putInt("operatingSystemVersionCode", deviceInfo.osVersionCode ?: -1)
+ putString("appVersion", deviceInfo.appVersion)
+ putInt("appVersionCode", deviceInfo.appVersionCode ?: -1)
+ }.apply()
+ }
+ }
}
}
- private fun processReferenceTime() {
+ private suspend fun processReferenceTime() {
val ntpServer = ntpServer ?: return
if (sntpClient.requestTime(ntpServer, 5000)) {
val delay = sntpClient.roundTripTime / 1000.0
@@ -207,7 +254,7 @@ class ApplicationStatusManager(
val ntpTime = (sntpClient.ntpTime + SystemClock.elapsedRealtime() - sntpClient.ntpTimeReference) / 1000.0
send(
- ntpTopic,
+ ntpTopic.await(),
ApplicationExternalTime(
time,
ntpTime,
@@ -219,14 +266,14 @@ class ApplicationStatusManager(
}
}
- private fun processServerStatus() {
+ private suspend fun processServerStatus() {
val time = currentTime
val status: ServerStatus = state.serverStatus.toServerStatus()
val ipAddress = if (isProcessingIp) lookupIpAddress() else null
logger.info("Server Status: {}; Device IP: {}", status, ipAddress)
- send(serverTopic, ApplicationServerStatus(time, status, ipAddress))
+ send(serverTopic.await(), ApplicationServerStatus(time, status, ipAddress))
}
// Find Ip via NetworkInterfaces. Works via wifi, ethernet and mobile network
@@ -245,12 +292,12 @@ class ApplicationStatusManager(
return previousInetAddress?.hostAddress
}
- private fun processUptime() {
+ private suspend fun processUptime() {
val uptime = (SystemClock.elapsedRealtime() - creationTimeStamp) / 1000.0
- send(uptimeTopic, ApplicationUptime(currentTime, uptime))
+ send(uptimeTopic.await(), ApplicationUptime(currentTime, uptime))
}
- private fun processRecordsSent() {
+ private suspend fun processRecordsSent() {
val time = currentTime
val recordsCached = state.cachedRecords.values
@@ -260,58 +307,79 @@ class ApplicationStatusManager(
logger.info("Number of records: {sent: {}, unsent: {}, cached: {}}",
recordsSent, recordsCached, recordsCached)
- send(recordCountsTopic, ApplicationRecordCounts(time,
+ send(recordCountsTopic.await(), ApplicationRecordCounts(time,
recordsCached, recordsSent, recordsCached.toIntCapped()))
}
override fun onClose() {
- this.processor.close()
- cacheReceiver?.unregister()
- serverRecordsReceiver?.unregister()
- serverStatusReceiver?.unregister()
+ applicationStatusExecutor.stop {
+ this.processor.stop()
+ cachedRecordTrackJob?.also {
+ it.cancel()
+ cachedRecordTrackJob = null
+ }
+ serverRecordsSentJob?.also {
+ it.cancel()
+ serverRecordsSentJob = null
+ }
+ serverStatusJob?.also {
+ it.cancel()
+ serverStatusJob = null
+ }
+ tzProcessor?.stop()
+ }
}
- private fun processTimeZone() {
+ private suspend fun processTimeZone() {
val now = System.currentTimeMillis()
val tzOffset = TimeZone.getDefault().getOffset(now)
tzOffsetCache.applyIfChanged(tzOffset / 1000) { offset ->
- send(
- timeZoneTopic,
- ApplicationTimeZone(
- now / 1000.0,
- offset,
- ),
- )
- prefs.edit()
- .putInt("timeZoneOffset", offset)
- .apply()
+ applicationStatusExecutor.execute {
+ send(
+ timeZoneTopic.await(),
+ ApplicationTimeZone(
+ now / 1000.0,
+ offset,
+ ),
+ )
+ withContext(Dispatchers.IO) {
+ prefs.edit()
+ .putInt("timeZoneOffset", offset)
+ .apply()
+ }
+ }
}
}
- @Synchronized
fun setTzUpdateRate(tzUpdateRate: Long, unit: TimeUnit) {
- if (tzUpdateRate > 0) { // enable timezone processor
- var processor = this.tzProcessor
- if (processor == null) {
- processor = OfflineProcessor(service) {
- process = listOf(this@ApplicationStatusManager::processTimeZone)
- requestCode = APPLICATION_TZ_PROCESSOR_REQUEST_CODE
- requestName = APPLICATION_TZ_PROCESSOR_REQUEST_NAME
- interval(tzUpdateRate, unit)
- wake = false
+ service.lifecycleScope.launch(Dispatchers.Default) {
+ tzIntervalMutex.withLock {
+ if (tzUpdateRate > 0) { // enable timezone processor
+ var processor = this@ApplicationStatusManager.tzProcessor
+ if (processor == null) {
+ processor = OfflineProcessor(service) {
+ process = listOf { this@ApplicationStatusManager.processTimeZone() }
+ requestCode = APPLICATION_TZ_PROCESSOR_REQUEST_CODE
+ requestName = APPLICATION_TZ_PROCESSOR_REQUEST_NAME
+ interval(tzUpdateRate, unit)
+ wake = false
+ }
+ this@ApplicationStatusManager.tzProcessor = processor
+
+ if (this@ApplicationStatusManager.state.status == SourceStatusListener.Status.CONNECTED) {
+ processor.start()
+ }
+ } else {
+ processor.interval(tzUpdateRate, unit)
+ }
+ } else { // disable timezone processor
+ this@ApplicationStatusManager.tzProcessor?.let {
+ service.lifecycleScope.launch(Dispatchers.Default) {
+ it.stop()
+ }
+ this@ApplicationStatusManager.tzProcessor = null
+ }
}
- this.tzProcessor = processor
-
- if (this.state.status == SourceStatusListener.Status.CONNECTED) {
- processor.start()
- }
- } else {
- processor.interval(tzUpdateRate, unit)
- }
- } else { // disable timezone processor
- this.tzProcessor?.let {
- it.close()
- this.tzProcessor = null
}
}
}
@@ -339,13 +407,13 @@ class ApplicationStatusManager(
private fun Long.toIntCapped(): Int = if (this <= Int.MAX_VALUE) toInt() else Int.MAX_VALUE
- private fun ServerStatusListener.Status?.toServerStatus(): ServerStatus = when (this) {
- ServerStatusListener.Status.CONNECTED,
- ServerStatusListener.Status.READY,
- ServerStatusListener.Status.UPLOADING -> ServerStatus.CONNECTED
- ServerStatusListener.Status.DISCONNECTED,
- ServerStatusListener.Status.DISABLED,
- ServerStatusListener.Status.UPLOADING_FAILED -> ServerStatus.DISCONNECTED
+ private fun org.radarbase.android.kafka.ServerStatus?.toServerStatus(): ServerStatus = when (this) {
+ org.radarbase.android.kafka.ServerStatus.CONNECTED,
+ org.radarbase.android.kafka.ServerStatus.READY,
+ org.radarbase.android.kafka.ServerStatus.UPLOADING -> ServerStatus.CONNECTED
+ org.radarbase.android.kafka.ServerStatus.DISCONNECTED,
+ org.radarbase.android.kafka.ServerStatus.DISABLED,
+ org.radarbase.android.kafka.ServerStatus.UPLOADING_FAILED -> ServerStatus.DISCONNECTED
else -> ServerStatus.UNKNOWN
}
}
diff --git a/plugins/radar-android-audio/src/main/java/org/radarbase/passive/audio/OpensmileAudioManager.kt b/plugins/radar-android-audio/src/main/java/org/radarbase/passive/audio/OpensmileAudioManager.kt
index 3f8bf249a..b8005c96b 100644
--- a/plugins/radar-android-audio/src/main/java/org/radarbase/passive/audio/OpensmileAudioManager.kt
+++ b/plugins/radar-android-audio/src/main/java/org/radarbase/passive/audio/OpensmileAudioManager.kt
@@ -16,14 +16,17 @@
package org.radarbase.passive.audio
-import android.os.Process
import android.util.Base64
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceStatusListener
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.OfflineProcessor
-import org.radarbase.android.util.SafeHandler
import org.radarbase.passive.audio.OpenSmileAudioService.Companion.DEFAULT_RECORD_RATE
import org.radarbase.passive.audio.opensmile.SmileJNI
import org.radarcns.kafka.ObservationKey
@@ -34,8 +37,11 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
/** Manages Phone sensors */
-class OpensmileAudioManager constructor(service: OpenSmileAudioService) : AbstractSourceManager(service) {
- private val audioTopic: DataCache = createCache("android_processed_audio", OpenSmile2PhoneAudio())
+class OpensmileAudioManager(service: OpenSmileAudioService) : AbstractSourceManager(service) {
+ private val audioTopic: Deferred> = service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_processed_audio", OpenSmile2PhoneAudio())
+ }
@get:Synchronized
@set:Synchronized
@@ -44,6 +50,7 @@ class OpensmileAudioManager constructor(service: OpenSmileAudioService) : Abstra
private val processor: OfflineProcessor
private var isRunning: Boolean = false
private val dataDirectory: File?
+ private val audioTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
init {
name = service.getString(R.string.header_audio_status)
@@ -53,7 +60,6 @@ class OpensmileAudioManager constructor(service: OpenSmileAudioService) : Abstra
requestCode = AUDIO_REQUEST_CODE
requestName = AUDIO_REQUEST_NAME
interval(DEFAULT_RECORD_RATE, TimeUnit.SECONDS)
- handler(SafeHandler.getInstance("RADARAudio", Process.THREAD_PRIORITY_BACKGROUND))
wake = true
}
val externalDirectory = service.filesDir
@@ -69,6 +75,7 @@ class OpensmileAudioManager constructor(service: OpenSmileAudioService) : Abstra
}
override fun start(acceptableIds: Set) {
+ audioTaskExecutor.start()
isRunning = true
processor.start {
SmileJNI.init(service)
@@ -78,7 +85,7 @@ class OpensmileAudioManager constructor(service: OpenSmileAudioService) : Abstra
status = SourceStatusListener.Status.CONNECTED
}
- private fun processAudio() {
+ private suspend fun processAudio() {
status = SourceStatusListener.Status.CONNECTED
logger.info("Setting up audio recording")
val dataPath = File(dataDirectory, "audio_" + System.currentTimeMillis() + ".bin")
@@ -98,7 +105,7 @@ class OpensmileAudioManager constructor(service: OpenSmileAudioService) : Abstra
try {
if (dataPath.exists()) {
- send(audioTopic, OpenSmile2PhoneAudio(
+ send(audioTopic.await(), OpenSmile2PhoneAudio(
startTime,
currentTime,
localConfig.configFile,
@@ -115,7 +122,9 @@ class OpensmileAudioManager constructor(service: OpenSmileAudioService) : Abstra
override fun onClose() {
if (isRunning) {
- processor.close()
+ audioTaskExecutor.stop {
+ processor.stop()
+ }
}
clearDataDirectory()
}
diff --git a/plugins/radar-android-empatica/src/main/java/org/radarbase/passive/empatica/E4Manager.kt b/plugins/radar-android-empatica/src/main/java/org/radarbase/passive/empatica/E4Manager.kt
index fc050036a..40083115b 100644
--- a/plugins/radar-android-empatica/src/main/java/org/radarbase/passive/empatica/E4Manager.kt
+++ b/plugins/radar-android-empatica/src/main/java/org/radarbase/passive/empatica/E4Manager.kt
@@ -16,6 +16,7 @@
package org.radarbase.passive.empatica
+import androidx.lifecycle.lifecycleScope
import com.empatica.empalink.ConnectionNotAllowedException
import com.empatica.empalink.EmpaDeviceManager
import com.empatica.empalink.EmpaticaDevice
@@ -26,11 +27,17 @@ import com.empatica.empalink.config.EmpaStatus
import com.empatica.empalink.delegate.EmpaDataDelegate
import com.empatica.empalink.delegate.EmpaSessionManagerDelegate
import com.empatica.empalink.delegate.EmpaStatusDelegate
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.SourceStatusListener
import org.radarbase.android.util.BluetoothStateReceiver.Companion.bluetoothIsEnabled
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.NotificationHandler
import org.radarbase.android.util.SafeHandler
+import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.empatica.*
import org.slf4j.LoggerFactory
import java.lang.Exception
@@ -49,20 +56,39 @@ class E4Manager(
NotificationHandler(service)
}
private var doNotify: Boolean = false
- private val accelerationTopic = createCache("android_empatica_e4_acceleration", EmpaticaE4Acceleration())
- private val batteryLevelTopic = createCache("android_empatica_e4_battery_level", EmpaticaE4BatteryLevel())
- private val bloodVolumePulseTopic = createCache("android_empatica_e4_blood_volume_pulse", EmpaticaE4BloodVolumePulse())
- private val edaTopic = createCache("android_empatica_e4_electrodermal_activity", EmpaticaE4ElectroDermalActivity())
- private val interBeatIntervalTopic = createCache("android_empatica_e4_inter_beat_interval", EmpaticaE4InterBeatInterval())
- private val temperatureTopic = createCache("android_empatica_e4_temperature", EmpaticaE4Temperature())
- private val sensorStatusTopic = createCache("android_empatica_e4_sensor_status", EmpaticaE4SensorStatus())
- private val tagTopic = createCache("android_empatica_e4_tag", EmpaticaE4Tag())
+ private val accelerationTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_acceleration", EmpaticaE4Acceleration())
+ }
+ private val batteryLevelTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_battery_level", EmpaticaE4BatteryLevel())
+ }
+ private val bloodVolumePulseTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_blood_volume_pulse", EmpaticaE4BloodVolumePulse())
+ }
+ private val edaTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_electrodermal_activity", EmpaticaE4ElectroDermalActivity())
+ }
+ private val interBeatIntervalTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_inter_beat_interval", EmpaticaE4InterBeatInterval())
+ }
+ private val temperatureTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_temperature", EmpaticaE4Temperature())
+ }
+ private val sensorStatusTopic: Deferred> = e4Service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_empatica_e4_sensor_status", EmpaticaE4SensorStatus())
+ }
+ private val tagTopic: Deferred> = e4Service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_empatica_e4_tag", EmpaticaE4Tag())
+ }
private val isScanning = AtomicBoolean(false)
private var hasBeenConnecting = false
private var apiKey: String? = null
private var connectingFuture: SafeHandler.HandlerFuture? = null
+ private val dataSenderExecutor: CoroutineTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
+
init {
status = SourceStatusListener.Status.UNAVAILABLE
name = service.getString(R.string.empaticaE4DisplayName)
@@ -70,6 +96,7 @@ class E4Manager(
override fun start(acceptableIds: Set) {
logger.info("Starting scanning")
+ dataSenderExecutor.start()
handler.execute {
// Create a new EmpaDeviceManager. E4DeviceManager is both its data and status delegate.
// Initialize the Device Manager using your API key. You need to have Internet access at this point.
@@ -162,7 +189,8 @@ class E4Manager(
Pair("macAddress", address),
Pair("serialNumber", empaDevice.serialNumber))) {
if (it == null) {
- logger.info("Device {} with ID {} is not listed in acceptable device IDs", deviceName, address)
+ logger.info("Device {} with ID {} is not listed in acceptable device IDs",
+ deviceName, address)
service.sourceFailedToConnect(deviceName)
} else {
handler.execute {
@@ -206,6 +234,7 @@ class E4Manager(
}
override fun onClose() {
+ dataSenderExecutor.stop()
handler.execute(true) {
stopScanning()
connectingFuture?.let {
@@ -248,51 +277,79 @@ class E4Manager(
override fun didUpdateOnWristStatus(status: Int) {
val now = currentTime
- send(sensorStatusTopic, EmpaticaE4SensorStatus(
- now, now, "e4", status.toEmpaStatusString()))
+ dataSenderExecutor.execute {
+ send(
+ sensorStatusTopic.await(), EmpaticaE4SensorStatus(
+ now, now, "e4", status.toEmpaStatusString()
+ )
+ )
+ }
}
override fun didReceiveAcceleration(x: Int, y: Int, z: Int, timestamp: Double) {
state.setAcceleration(x / 64f, y / 64f, z / 64f)
val latestAcceleration = state.acceleration
- send(accelerationTopic, EmpaticaE4Acceleration(
- timestamp, currentTime,
- latestAcceleration[0], latestAcceleration[1], latestAcceleration[2]))
+ dataSenderExecutor.execute {
+ send(
+ accelerationTopic.await(), EmpaticaE4Acceleration(
+ timestamp, currentTime,
+ latestAcceleration[0], latestAcceleration[1], latestAcceleration[2]
+ )
+ )
+ }
}
override fun didReceiveBVP(bvp: Float, timestamp: Double) {
state.bloodVolumePulse = bvp
- send(bloodVolumePulseTopic, EmpaticaE4BloodVolumePulse(
- timestamp, currentTime, bvp))
+ dataSenderExecutor.execute {
+ send(
+ bloodVolumePulseTopic.await(), EmpaticaE4BloodVolumePulse(
+ timestamp, currentTime, bvp
+ )
+ )
+ }
}
override fun didReceiveBatteryLevel(battery: Float, timestamp: Double) {
state.batteryLevel = battery
- send(batteryLevelTopic, EmpaticaE4BatteryLevel(
- timestamp, currentTime, battery))
- }
+ dataSenderExecutor.execute {
+ send(
+ batteryLevelTopic.await(), EmpaticaE4BatteryLevel(
+ timestamp, currentTime, battery
+ )
+ )
+ }
+ }
override fun didReceiveTag(timestamp: Double) {
val value = EmpaticaE4Tag(timestamp, currentTime)
- send(tagTopic, value)
+ dataSenderExecutor.execute {
+ send(tagTopic.await(), value)
+ }
}
override fun didReceiveGSR(gsr: Float, timestamp: Double) {
state.electroDermalActivity = gsr
val value = EmpaticaE4ElectroDermalActivity(timestamp, currentTime, gsr)
- send(edaTopic, value)
+ dataSenderExecutor.execute {
+ send(edaTopic.await(), value)
+ }
}
override fun didReceiveIBI(ibi: Float, timestamp: Double) {
state.interBeatInterval = ibi
val value = EmpaticaE4InterBeatInterval(timestamp, currentTime, ibi)
- send(interBeatIntervalTopic, value)
+ dataSenderExecutor.execute {
+ send(interBeatIntervalTopic.await(), value)
+ }
}
override fun didReceiveTemperature(temperature: Float, timestamp: Double) {
state.temperature = temperature
val value = EmpaticaE4Temperature(timestamp, currentTime, temperature)
- send(temperatureTopic, value)
+ dataSenderExecutor.execute {
+ send(temperatureTopic.await(), value)
+ }
}
override fun didUpdateSensorStatus(status: Int, type: EmpaSensorType) {
@@ -300,7 +357,9 @@ class E4Manager(
state.setSensorStatus(type, statusString)
val now = currentTime
val value = EmpaticaE4SensorStatus(now, now, type.name, statusString)
- send(sensorStatusTopic, value)
+ dataSenderExecutor.execute {
+ send(sensorStatusTopic.await(), value)
+ }
}
override fun hashCode() = System.identityHashCode(this)
diff --git a/plugins/radar-android-faros/src/main/java/org/radarbase/passive/bittium/FarosManager.kt b/plugins/radar-android-faros/src/main/java/org/radarbase/passive/bittium/FarosManager.kt
index 5a20854d2..2c11b8199 100644
--- a/plugins/radar-android-faros/src/main/java/org/radarbase/passive/bittium/FarosManager.kt
+++ b/plugins/radar-android-faros/src/main/java/org/radarbase/passive/bittium/FarosManager.kt
@@ -16,13 +16,18 @@
package org.radarbase.passive.bittium
-import org.radarbase.android.RadarApplication.Companion.radarApp
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.SourceStatusListener
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.NotificationHandler
import org.radarbase.android.util.SafeHandler
-import org.radarbase.util.Strings
+import org.radarbase.util.StringTransforms
import org.radarcns.bittium.faros.*
import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.bittium.*
@@ -39,16 +44,29 @@ class FarosManager internal constructor(
NotificationHandler(this.service)
}
private var doNotify: Boolean = false
- private val accelerationTopic: DataCache = createCache("android_bittium_faros_acceleration", BittiumFarosAcceleration())
- private val ecgTopic: DataCache = createCache("android_bittium_faros_ecg", BittiumFarosEcg())
- private val ibiTopic: DataCache = createCache("android_bittium_faros_inter_beat_interval", BittiumFarosInterBeatInterval())
- private val temperatureTopic: DataCache = createCache("android_bittium_faros_temperature", BittiumFarosTemperature())
- private val batteryTopic: DataCache = createCache("android_bittium_faros_battery_level", BittiumFarosBatteryLevel())
+ private val accelerationTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_bittium_faros_acceleration", BittiumFarosAcceleration())
+ }
+ private val ecgTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_bittium_faros_ecg", BittiumFarosEcg())
+ }
+ private val ibiTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_bittium_faros_inter_beat_interval", BittiumFarosInterBeatInterval())
+ }
+ private val temperatureTopic: Deferred> = service.lifecycleScope.async(Dispatchers.Default) {
+ createCache("android_bittium_faros_temperature", BittiumFarosTemperature())
+ }
+ private val batteryTopic: Deferred> = service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_bittium_faros_battery_level", BittiumFarosBatteryLevel())
+ }
private lateinit var acceptableIds: Array
private lateinit var apiManager: FarosSdkManager
private var settings: FarosSettings = farosFactory.defaultSettingsBuilder().build()
+ private val farosTaskExecutor: CoroutineTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
+
private var faros: FarosDevice? = null
override fun start(acceptableIds: Set) {
@@ -56,8 +74,9 @@ class FarosManager internal constructor(
service.getString(R.string.farosLabel)
handler.start()
- handler.execute {
- this.acceptableIds = Strings.containsPatterns(acceptableIds)
+ farosTaskExecutor.start()
+ farosTaskExecutor.execute {
+ this.acceptableIds = StringTransforms.containsPatterns(acceptableIds)
apiManager = farosFactory.createSdkManager(service)
try {
@@ -73,7 +92,7 @@ class FarosManager internal constructor(
override fun onStatusUpdate(status: Int) {
val radarStatus = when(status) {
FarosDeviceListener.IDLE -> {
- handler.execute {
+ farosTaskExecutor.execute {
logger.debug("Faros status is IDLE. Request to start/restart measurements.")
applySettings(this.settings)
faros?.run {
@@ -115,10 +134,10 @@ class FarosManager internal constructor(
else -> "unknown"
}))
- if ((acceptableIds.isEmpty() || Strings.findAny(acceptableIds, device.name))) {
+ if ((acceptableIds.isEmpty() || StringTransforms.findAny(acceptableIds, device.name))) {
register(device.name, device.name, attributes) {
if (it != null) {
- handler.executeReentrant {
+ farosTaskExecutor.executeReentrant {
logger.info("Stopping scanning")
apiManager.stopScanning()
name = service.getString(R.string.farosDeviceName, device.name)
@@ -137,17 +156,23 @@ class FarosManager internal constructor(
override fun didReceiveAcceleration(timestamp: Double, x: Float, y: Float, z: Float) {
state.setAcceleration(x, y, z)
- send(accelerationTopic, BittiumFarosAcceleration(timestamp, currentTime, x, y, z))
+ farosTaskExecutor.execute {
+ send(accelerationTopic.await(), BittiumFarosAcceleration(timestamp, currentTime, x, y, z))
+ }
}
override fun didReceiveTemperature(timestamp: Double, temperature: Float) {
state.temperature = temperature
- send(temperatureTopic, BittiumFarosTemperature(timestamp, currentTime, temperature))
+ farosTaskExecutor.execute {
+ send(temperatureTopic.await(), BittiumFarosTemperature(timestamp, currentTime, temperature))
+ }
}
override fun didReceiveInterBeatInterval(timestamp: Double, interBeatInterval: Float) {
state.heartRate = 60 / interBeatInterval
- send(ibiTopic, BittiumFarosInterBeatInterval(timestamp, currentTime, interBeatInterval))
+ farosTaskExecutor.execute {
+ send(ibiTopic.await(), BittiumFarosInterBeatInterval(timestamp, currentTime, interBeatInterval))
+ }
}
override fun didReceiveEcg(timestamp: Double, channels: FloatArray) {
@@ -155,33 +180,42 @@ class FarosManager internal constructor(
val channelTwo = if (channels.size > 1) channels[1] else null
val channelThree = if (channels.size > 2) channels[2] else null
- send(ecgTopic, BittiumFarosEcg(timestamp, currentTime, channelOne, channelTwo, channelThree))
+ farosTaskExecutor.execute {
+ send(
+ ecgTopic.await(),
+ BittiumFarosEcg(timestamp, currentTime, channelOne, channelTwo, channelThree)
+ )
+ }
}
override fun didReceiveBatteryStatus(timestamp: Double, status: Int) {
// only send approximate battery levels if the battery level interval is disabled.
- val level = when(status) {
+ val level = when (status) {
FarosDeviceListener.BATTERY_STATUS_CRITICAL -> 0.05f
- FarosDeviceListener.BATTERY_STATUS_LOW -> 0.175f
- FarosDeviceListener.BATTERY_STATUS_MEDIUM -> 0.5f
- FarosDeviceListener.BATTERY_STATUS_FULL -> 0.875f
+ FarosDeviceListener.BATTERY_STATUS_LOW -> 0.175f
+ FarosDeviceListener.BATTERY_STATUS_MEDIUM -> 0.5f
+ FarosDeviceListener.BATTERY_STATUS_FULL -> 0.875f
else -> {
logger.warn("Unknown battery status {} passed", status)
return
}
}
state.batteryLevel = level
- send(batteryTopic, BittiumFarosBatteryLevel(timestamp, currentTime, level, false))
+ farosTaskExecutor.execute {
+ send(batteryTopic.await(), BittiumFarosBatteryLevel(timestamp, currentTime, level, false))
+ }
}
override fun didReceiveBatteryLevel(timestamp: Double, level: Float) {
state.batteryLevel = level
- send(batteryTopic, BittiumFarosBatteryLevel(timestamp, currentTime, level, true))
+ farosTaskExecutor.execute {
+ send(batteryTopic.await(), BittiumFarosBatteryLevel(timestamp, currentTime, level, true))
+ }
}
internal fun applySettings(settings: FarosSettings) {
- handler.executeReentrant {
- this.settings = settings
+ service.lifecycleScope.launch(Dispatchers.Default) {
+ this@FarosManager.settings = settings
faros?.run {
if (isMeasuring) {
logger.info("Device is measuring. Stopping device before applying settings.")
@@ -213,7 +247,8 @@ class FarosManager internal constructor(
override fun onClose() {
logger.info("Faros BT Closing device {}", this)
- handler.stop {
+ handler.stop()
+ farosTaskExecutor.stop {
try {
faros?.close()
} catch (e2: IOException) {
diff --git a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/ActivityTransitionReceiver.kt b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/ActivityTransitionReceiver.kt
index d250451ae..bf64f50cf 100644
--- a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/ActivityTransitionReceiver.kt
+++ b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/ActivityTransitionReceiver.kt
@@ -20,11 +20,19 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.google.android.gms.location.ActivityTransitionResult
+import org.radarbase.android.util.CoroutineTaskExecutor
-class ActivityTransitionReceiver(private val googleActivityManager: GoogleActivityManager) : BroadcastReceiver() {
+class ActivityTransitionReceiver(
+ private val googleActivityManager: GoogleActivityManager,
+ private val activityTaskExecutor: CoroutineTaskExecutor
+) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
- if (ActivityTransitionResult.hasResult(intent)) googleActivityManager.sendActivityTransitionUpdates(intent)
+ if (ActivityTransitionResult.hasResult(intent)) {
+ activityTaskExecutor.execute {
+ googleActivityManager.sendActivityTransitionUpdates(intent)
+ }
+ }
}
}
\ No newline at end of file
diff --git a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt
index e32f3b016..b355ee412 100644
--- a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt
+++ b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt
@@ -21,18 +21,21 @@ import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
-import android.os.Process
import android.os.SystemClock
import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
import com.google.android.gms.location.ActivityRecognition
import com.google.android.gms.location.ActivityTransition
import com.google.android.gms.location.ActivityTransitionResult
import com.google.android.gms.location.DetectedActivity
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceStatusListener
-import org.radarbase.android.util.SafeHandler
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.toPendingIntentFlag
import org.radarbase.passive.google.activity.GoogleActivityProvider.Companion.ACTIVITY_RECOGNITION_COMPAT
import org.radarcns.kafka.ObservationKey
@@ -42,9 +45,15 @@ import org.radarcns.passive.google.TransitionType
import org.slf4j.LoggerFactory
class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceManager(context) {
- private val activityTransitionEventTopic: DataCache = createCache("android_google_activity_transition_event", GoogleActivityTransitionEvent())
+ private val activityTransitionEventTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_google_activity_transition_event",
+ GoogleActivityTransitionEvent()
+ )
+ }
- private val activityHandler = SafeHandler.getInstance("Google Activity", Process.THREAD_PRIORITY_BACKGROUND)
+ private val activityTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private val activityPendingIntent: PendingIntent
private val activityTransitionReceiver: ActivityTransitionReceiver
@@ -53,15 +62,15 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana
init {
name = service.getString(R.string.google_activity_display_name)
- activityTransitionReceiver = ActivityTransitionReceiver(this)
+ activityTransitionReceiver = ActivityTransitionReceiver(this, activityTaskExecutor)
activityPendingIntent = createActivityPendingIntent()
}
override fun start(acceptableIds: Set) {
register()
- activityHandler.start()
+ activityTaskExecutor.start()
status = SourceStatusListener.Status.READY
- activityHandler.execute {
+ activityTaskExecutor.execute {
registerActivityTransitionReceiver()
registerForActivityUpdates()
}
@@ -99,7 +108,7 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana
}
}
- fun sendActivityTransitionUpdates(activityIntent: Intent) {
+ suspend fun sendActivityTransitionUpdates(activityIntent: Intent) {
logger.info("Activity transition event data received")
val activityTransitionResult: ActivityTransitionResult = ActivityTransitionResult.extractResult(activityIntent) ?: return
@@ -112,7 +121,7 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana
val activity = event.activityType.toActivityType()
val transition = event.transitionType.toTransitionType()
send(
- activityTransitionEventTopic,
+ activityTransitionEventTopic.await(),
GoogleActivityTransitionEvent(time, currentTime, activity, transition)
)
}
@@ -152,7 +161,7 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana
}
override fun onClose() {
- activityHandler.stop {
+ activityTaskExecutor.stop {
unRegisterFromActivityUpdates()
unRegisterFromActivityReceiver()
}
diff --git a/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesManager.kt b/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesManager.kt
index bd80c32d0..146673252 100644
--- a/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesManager.kt
+++ b/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesManager.kt
@@ -23,6 +23,7 @@ import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.ApiException
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.AddressComponent
@@ -33,13 +34,18 @@ import com.google.android.libraries.places.api.net.FetchPlaceResponse
import com.google.android.libraries.places.api.net.FindCurrentPlaceRequest
import com.google.android.libraries.places.api.net.FindCurrentPlaceResponse
import com.google.android.libraries.places.api.net.PlacesClient
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
+import org.radarbase.android.source.SourceService.Companion.DEVICE_LOCATION_CHANGED
import org.radarbase.android.source.SourceStatusListener
-import org.radarbase.android.util.BroadcastRegistration
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.OfflineProcessor
-import org.radarbase.android.util.SafeHandler
-import org.radarbase.android.util.register
import org.radarbase.passive.google.places.GooglePlacesService.Companion.GOOGLE_PLACES_API_KEY_DEFAULT
import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.google.GooglePlacesInfo
@@ -48,8 +54,14 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.pow
-class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized private var apiKey: String, private val placeHandler: SafeHandler) : AbstractSourceManager(service) {
- private val placesInfoTopic: DataCache = createCache("android_google_places_info", GooglePlacesInfo())
+class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized private var apiKey: String, private val placeExecutor: CoroutineTaskExecutor) : AbstractSourceManager(service) {
+ private val placesInfoTopic: Deferred> = service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_google_places_info",
+ GooglePlacesInfo()
+ )
+ }
private val placesProcessor: OfflineProcessor
// Delay in seconds for exponential backoff
@@ -83,7 +95,7 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
get() = if (shouldFetchPlaceId||shouldFetchAdditionalInfo) listOf(Place.Field.TYPES, Place.Field.ID) else listOf(Place.Field.TYPES)
private val detailsPlaceFields: List = listOf(Place.Field.ADDRESS_COMPONENTS)
- private var placesBroadcastReceiver: BroadcastRegistration? = null
+ private var placesBroadcastReceiver: Job? = null
private var isRecentlySent: AtomicBoolean = AtomicBoolean(false)
private lateinit var currentPlaceRequest:FindCurrentPlaceRequest
@@ -107,23 +119,28 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
register()
status = SourceStatusListener.Status.READY
placesProcessor.start()
- placeHandler.execute { if (!placesClientCreated) {
+ placeExecutor.execute { if (!placesClientCreated) {
service.createPlacesClient()
}
if (!placesClientCreated && !Places.isInitialized()) {
updateApiKey(apiKey)
} }
updateConnected()
- placesBroadcastReceiver = service.broadcaster?.register(DEVICE_LOCATION_CHANGED) { _, _ ->
- if (placesClientCreated) {
- placeHandler.execute {
- state.fromBroadcast.set(true)
- processPlacesData()
- state.fromBroadcast.set(false)
+
+ placesBroadcastReceiver = service.lifecycleScope.launch(Dispatchers.Default) {
+ service.locationChangedBroadcast.collectLatest { status ->
+ if (status == DEVICE_LOCATION_CHANGED) {
+ if (placesClientCreated) {
+ placeExecutor.execute {
+ state.fromBroadcast.set(true)
+ processPlacesData()
+ state.fromBroadcast.set(false)
+ }
+ }
}
}
}
- }
+ }
fun updateApiKey(apiKey: String) {
when (apiKey) {
@@ -164,10 +181,9 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
}
}
- @Synchronized
@SuppressLint("MissingPermission")
- private fun processPlacesData() {
- placeHandler.execute {
+ private suspend fun processPlacesData() {
+ placeExecutor.execute {
if (isPermissionGranted) {
val client = placesClient ?: return@execute
currentPlaceRequest = FindCurrentPlaceRequest.newInstance(currentPlaceFields)
@@ -240,13 +256,16 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
val city = findComponent(addressComponents, CITY_KEY)
val state = findComponent(addressComponents, STATE_KEY)
val country = findComponent(addressComponents, COUNTRY_KEY)
- send(placesInfoTopic, placesInfoBuilder.apply {
- this.city = city
- this.state = state
- this.country = country
- this.placeId = it }.build())
- updateRecentlySent()
- logger.info("Google Places data with additional info sent")
+ placeExecutor.execute {
+ send(placesInfoTopic.await(), placesInfoBuilder.apply {
+ this.city = city
+ this.state = state
+ this.country = country
+ this.placeId = it
+ }.build())
+ updateRecentlySent()
+ logger.info("Google Places data with additional info sent")
+ }
}
.addOnFailureListener { ex ->
when (ex.javaClass) {
@@ -255,14 +274,21 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
handleApiException(exception, exception.statusCode)
}
else -> {
- send(placesInfoTopic, placesInfoBuilder.apply { this.placeId = it }.build())
- logger.info("Google Places data sent")
+ placeExecutor.execute {
+ send(placesInfoTopic.await(),
+ placesInfoBuilder.apply { this.placeId = it }
+ .build()
+ )
+ logger.info("Google Places data sent")
+ }
}
}
}
}
} else {
- send(placesInfoTopic, placesInfoBuilder.build())
+ placeExecutor.execute {
+ send(placesInfoTopic.await(), placesInfoBuilder.build())
+ }
}
}
}
@@ -272,7 +298,7 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
*/
private fun updateRecentlySent() {
isRecentlySent.set(true)
- placeHandler.delay(additionalFetchDelay) {
+ placeExecutor.delay(additionalFetchDelay) {
isRecentlySent.set(false)
}
}
@@ -315,7 +341,7 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
override fun onClose() {
val currentDelay = (baseDelay + 2.0.pow(service.numOfAttempts)).toLong()
- placeHandler.execute {
+ placeExecutor.execute {
if (currentDelay < maxDelay) {
service.numOfAttempts++
service.preferences
@@ -323,14 +349,14 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
.putInt(NUMBER_OF_ATTEMPTS_KEY, service.numOfAttempts).apply()
}
try {
- placesBroadcastReceiver?.unregister()
+ placesBroadcastReceiver?.cancel()
} catch (ex: IllegalStateException) {
logger.warn("Places receiver already unregistered in broadcast")
}
placesBroadcastReceiver = null
// Not using Places.deinitialize for now as it may prevent PlacesClient from being created during a plugin reload. Additionally, multiple calls to Places.initialize do not impact memory or CPU.
// Places.deinitialize()
- placesProcessor.close()
+ placesProcessor.stop()
}
}
@@ -345,6 +371,5 @@ class GooglePlacesManager(service: GooglePlacesService, @get: Synchronized priva
private const val STATE_KEY = "administrative_area_level_1"
private const val COUNTRY_KEY = "country"
const val NUMBER_OF_ATTEMPTS_KEY = "number_of_attempts_key"
- const val DEVICE_LOCATION_CHANGED = "org.radarbase.passive.google.places.GooglePlacesManager.DEVICE_LOCATION_CHANGED"
}
}
\ No newline at end of file
diff --git a/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesService.kt b/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesService.kt
index 7ef24d22d..18bbefb16 100644
--- a/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesService.kt
+++ b/plugins/radar-android-google-places/src/main/java/org.radarbase.passive.google.places/GooglePlacesService.kt
@@ -18,17 +18,15 @@ package org.radarbase.passive.google.places
import android.content.Context
import android.content.SharedPreferences
-import android.os.Process
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.net.PlacesClient
+import org.radarbase.android.AbstractRadarApplication
import org.radarbase.android.config.SingleRadarConfiguration
import org.radarbase.android.source.SourceManager
import org.radarbase.android.source.SourceService
import org.radarbase.android.source.SourceStatusListener
import org.radarbase.android.util.ChangeRunner
-import org.radarbase.android.util.SafeHandler
-import org.radarbase.android.util.send
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.passive.google.places.GooglePlacesManager.Companion.NUMBER_OF_ATTEMPTS_KEY
import org.slf4j.LoggerFactory
import java.util.concurrent.TimeUnit
@@ -39,9 +37,7 @@ class GooglePlacesService: SourceService() {
private val apiKey: ChangeRunner = ChangeRunner()
lateinit var preferences: SharedPreferences
val placesClientCreated = AtomicBoolean(false)
- var broadcaster: LocalBroadcastManager? = null
- private set
- private lateinit var placeHandler: SafeHandler
+ private lateinit var placeHandler: CoroutineTaskExecutor
var placesClient: PlacesClient? = null
@Synchronized get() = if (placesClientCreated.get()) field else null
@@ -53,11 +49,12 @@ class GooglePlacesService: SourceService() {
override fun onCreate() {
super.onCreate()
- placeHandler = SafeHandler.getInstance("Google-Places-Handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
+ val applicationPlacesClientStatus = (application as AbstractRadarApplication).placesClientCreated.get()
+ placesClientCreated.set(applicationPlacesClientStatus)
+ placeHandler = CoroutineTaskExecutor(this::class.simpleName!!).apply {
start()
}
preferences = getSharedPreferences(GooglePlacesService::class.java.name, Context.MODE_PRIVATE)
- broadcaster = LocalBroadcastManager.getInstance(this)
}
override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) {
@@ -95,10 +92,7 @@ class GooglePlacesService: SourceService() {
val currentManager = sourceManager
(currentManager as? GooglePlacesManager)?.reScan()
if (currentManager == null) {
- broadcaster?.send(SOURCE_STATUS_CHANGED) {
- putExtra(SOURCE_STATUS_CHANGED, SourceStatusListener.Status.DISCONNECTED.ordinal)
- putExtra(SOURCE_SERVICE_CLASS, this@GooglePlacesService.javaClass.name)
- }
+ super._status.value = SourceStatusListener.Status.DISCONNECTED
}
stopRecording()
}
@@ -112,8 +106,10 @@ class GooglePlacesService: SourceService() {
try {
placesClient = Places.createClient(this)
placesClientCreated.set(true)
+ (application as AbstractRadarApplication).placesClientCreated.set(true)
} catch (ex: IllegalStateException) {
placesClientCreated.set(false)
+ (application as AbstractRadarApplication).placesClientCreated.set(false)
logger.error("Places client has not been initialized yet.")
}
}
diff --git a/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt b/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt
index dc0488934..00aa59bfb 100644
--- a/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt
+++ b/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt
@@ -19,24 +19,23 @@ package org.radarbase.passive.google.sleep
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
-import android.content.Context.RECEIVER_NOT_EXPORTED
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
-import android.os.Build
-import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES
-import android.os.Process
import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
import com.google.android.gms.location.ActivityRecognition
import com.google.android.gms.location.SleepClassifyEvent
import com.google.android.gms.location.SleepSegmentEvent
import com.google.android.gms.location.SleepSegmentRequest
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceStatusListener
-import org.radarbase.android.util.SafeHandler
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.toPendingIntentFlag
import org.radarbase.passive.google.sleep.GoogleSleepProvider.Companion.ACTIVITY_RECOGNITION_COMPAT
import org.radarcns.kafka.ObservationKey
@@ -46,26 +45,38 @@ import org.radarcns.passive.google.SleepClassificationStatus
import org.slf4j.LoggerFactory
class GoogleSleepManager(context: GoogleSleepService) : AbstractSourceManager(context) {
- private val segmentEventTopic: DataCache = createCache("android_google_sleep_segment_event", GoogleSleepSegmentEvent())
- private val classifyEventTopic: DataCache = createCache("android_google_sleep_classify_event", GoogleSleepClassifyEvent())
+ private val segmentEventTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_google_sleep_segment_event",
+ GoogleSleepSegmentEvent()
+ )
+ }
+ private val classifyEventTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_google_sleep_classify_event",
+ GoogleSleepClassifyEvent()
+ )
+ }
private val sleepBroadcastReceiver: BroadcastReceiver
- private val sleepHandler = SafeHandler.getInstance("Google Sleep", Process.THREAD_PRIORITY_BACKGROUND)
+ private val sleepTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private val sleepPendingIntent: PendingIntent
private val isPermissionGranted
get() = ContextCompat.checkSelfPermission(service,ACTIVITY_RECOGNITION_COMPAT) == PackageManager.PERMISSION_GRANTED
init {
name = context.getString(R.string.googleSleepDisplayName)
- sleepBroadcastReceiver = SleepReceiver(this)
+ sleepBroadcastReceiver = SleepReceiver(this, sleepTaskExecutor)
sleepPendingIntent = createSleepPendingIntent()
}
override fun start(acceptableIds: Set) {
register()
- sleepHandler.start()
+ sleepTaskExecutor.start()
status = SourceStatusListener.Status.READY
- sleepHandler.execute {
+ sleepTaskExecutor.execute {
registerSleepReceiver()
registerForSleepData()
}
@@ -79,7 +90,7 @@ class GoogleSleepManager(context: GoogleSleepService) : AbstractSourceManager = SleepSegmentEvent.extractEvents(sleepIntent)
sleepSegmentEvents.forEach { sleepSegmentEvent ->
@@ -88,11 +99,11 @@ class GoogleSleepManager(context: GoogleSleepService) : AbstractSourceManager = SleepClassifyEvent.extractEvents(sleepIntent)
sleepClassifyEvents.forEach { sleepClassifyEvent ->
@@ -101,7 +112,7 @@ class GoogleSleepManager(context: GoogleSleepService) : AbstractSourceManager(service), PhoneAudioInputState.AudioRecordManager, PhoneAudioInputState.AudioRecordingManager {
- private val audioInputTopic: DataCache = createCache("android_phone_audio_input", PhoneAudioInput())
+ private val audioInputTopic: Deferred> = service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_audio_input",
+ PhoneAudioInput()
+ )
+ }
private var audioRecord: AudioRecord? = null
private var randomAccessWriter: RandomAccessFile? = null
private var buffer: ByteArray = byteArrayOf()
- private val audioRecordingHandler = SafeHandler.getInstance(
- "PHONE-AUDIO-INPUT", Process.THREAD_PRIORITY_BACKGROUND)
- private val recordProcessingHandler: SafeHandler = SafeHandler.getInstance(
- "AUDIO-RECORD-PROCESSING", Process.THREAD_PRIORITY_AUDIO)
+ private val audioRecordingHandler = CoroutineTaskExecutor("AudioRecordExecutor")
+ private val recordProcessingHandler: CoroutineTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private val mainHandler = Handler(Looper.getMainLooper())
private val preferences: SharedPreferences =
service.getSharedPreferences(PHONE_AUDIO_INPUT_SHARED_PREFS, Context.MODE_PRIVATE)
@@ -233,21 +240,35 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : AbstractSourceMa
} catch (ex: Exception) {
logger.error("Cannot retrieve audio file duration. Discarding sending data")
}
- send(
- audioInputTopic, PhoneAudioInput(
- currentTime, currentTime, "will be set after s3 functionality is added",
- "after data sending to s3 is enabled", mic.productName.toString(), mic.id.toString(),
- mic.sampleRates.joinToString(" "),
- mic.encodings.joinToString(" ") {
- AudioTypeFormatUtil.toLogFriendlyEncoding(it) },
- mic.type.toLogFriendlyType(), mic.channelCounts.joinToString(" "), audioDuration, wavFile.length(),
- state.isRecordingPlayed, wavFile.extension, sampleRate, AudioTypeFormatUtil.toLogFriendlyEncoding(audioFormat)
+ recordProcessingHandler.execute {
+ send(
+ audioInputTopic.await(), PhoneAudioInput(
+ currentTime,
+ currentTime,
+ "will be set after s3 functionality is added",
+ "after data sending to s3 is enabled",
+ mic.productName.toString(),
+ mic.id.toString(),
+ mic.sampleRates.joinToString(" "),
+ mic.encodings.joinToString(" ") {
+ AudioTypeFormatUtil.toLogFriendlyEncoding(it)
+ },
+ mic.type.toLogFriendlyType(),
+ mic.channelCounts.joinToString(" "),
+ audioDuration,
+ wavFile.length(),
+ state.isRecordingPlayed,
+ wavFile.extension,
+ sampleRate,
+ AudioTypeFormatUtil.toLogFriendlyEncoding(audioFormat)
+ )
)
- )
+ }
// Dummy Toast, will be removed after file uploading to s3 will be enabled.
Boast.makeText(service, "Sending last recorded audio. Is played? : ${state.isRecordingPlayed}", Toast.LENGTH_LONG).show()
}
+
}
// After the data is sent:
diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/ui/PhoneAudioInputPlaybackFragment.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/ui/PhoneAudioInputPlaybackFragment.kt
index 39e32287f..7186f7e58 100644
--- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/ui/PhoneAudioInputPlaybackFragment.kt
+++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/ui/PhoneAudioInputPlaybackFragment.kt
@@ -23,7 +23,6 @@ import android.media.MediaPlayer
import android.os.Bundle
import android.os.Handler
import android.os.Looper
-import android.os.Process
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -36,12 +35,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import org.radarbase.android.util.Boast
-import org.radarbase.android.util.SafeHandler
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.passive.phone.audio.input.PhoneAudioInputState
import org.radarbase.passive.phone.audio.input.R
+import org.radarbase.passive.phone.audio.input.databinding.FragmentAudioInputPlaybackBinding
import org.radarbase.passive.phone.audio.input.ui.PhoneAudioInputActivity.Companion.AUDIO_FILE_NAME
import org.radarbase.passive.phone.audio.input.ui.PhoneAudioInputActivity.Companion.EXTERNAL_DEVICE_NAME
-import org.radarbase.passive.phone.audio.input.databinding.FragmentAudioInputPlaybackBinding
import org.radarbase.passive.phone.audio.input.utils.AudioDeviceUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@@ -57,7 +56,7 @@ class PhoneAudioInputPlaybackFragment : Fragment() {
private var mediaPlayer: MediaPlayer? = null
private val phoneAudioViewModel: PhoneAudioInputViewModel by activityViewModels()
private var phoneAudioInputState: PhoneAudioInputState? = null
- private val mediaPlaybackHandler: SafeHandler = SafeHandler.getInstance("PhoneAudioInput", Process.THREAD_PRIORITY_AUDIO)
+ private val mediaPlaybackHandler: CoroutineTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private val mainHandler: Handler = Handler(Looper.getMainLooper())
private var audioFilePath: String? = null
diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/AudioFormatUtils.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/AudioFormatUtils.kt
index acd27d2f3..f55bc31f8 100644
--- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/AudioFormatUtils.kt
+++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/AudioFormatUtils.kt
@@ -48,7 +48,7 @@ object AudioTypeFormatUtil {
AudioFormat.ENCODING_MPEGH_BL_L4 -> "ENCODING_MPEGH_BL_L4"
AudioFormat.ENCODING_MPEGH_LC_L3 -> "ENCODING_MPEGH_LC_L3"
AudioFormat.ENCODING_MPEGH_LC_L4 -> "ENCODING_MPEGH_LC_L4"
- AudioFormat.ENCODING_DTS_UHD -> "ENCODING_DTS_UHD"
+ AudioFormat.ENCODING_DTS_UHD_P1 -> "ENCODING_DTS_UHD_P1"
AudioFormat.ENCODING_DRA -> "ENCODING_DRA"
else -> "invalid encoding $enc"
}
diff --git a/plugins/radar-android-phone-telephony/src/main/java/org/radarbase/passive/phone/telephony/PhoneLogManager.kt b/plugins/radar-android-phone-telephony/src/main/java/org/radarbase/passive/phone/telephony/PhoneLogManager.kt
index 0928179b5..a638a79ef 100644
--- a/plugins/radar-android-phone-telephony/src/main/java/org/radarbase/passive/phone/telephony/PhoneLogManager.kt
+++ b/plugins/radar-android-phone-telephony/src/main/java/org/radarbase/passive/phone/telephony/PhoneLogManager.kt
@@ -39,11 +39,34 @@ import java.util.*
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import android.os.Bundle
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import org.radarbase.android.util.CoroutineTaskExecutor
class PhoneLogManager(context: PhoneLogService) : AbstractSourceManager(context) {
- private val callTopic: DataCache = createCache("android_phone_call", PhoneCall())
- private val smsTopic: DataCache = createCache("android_phone_sms", PhoneSms())
- private val smsUnreadTopic: DataCache = createCache("android_phone_sms_unread", PhoneSmsUnread())
+ private val callTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_call",
+ PhoneCall()
+ )
+ }
+ private val smsTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_sms",
+ PhoneSms()
+ )
+ }
+ private val smsUnreadTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_sms_unread",
+ PhoneSmsUnread()
+ )
+ }
private val preferences: SharedPreferences = context.getSharedPreferences(PhoneLogService::class.java.name, Context.MODE_PRIVATE)
private val hashGenerator = HashGenerator(context, PhoneLogService::class.java.name)
private val db: ContentResolver = context.contentResolver
@@ -51,6 +74,8 @@ class PhoneLogManager(context: PhoneLogService) : AbstractSourceManager
@@ -216,16 +242,20 @@ class PhoneLogManager(context: PhoneLogService) : AbstractSourceManager PhoneCallType.MISSED
else -> PhoneCallType.UNKNOWN
}
-
- send(callTopic, PhoneCall(
- eventTimestamp,
- currentTime,
- duration,
- targetKey,
- type,
- targetIsContact,
- phoneNumber == null,
- target.length))
+ logsExecutor.execute {
+ send(
+ callTopic.await(), PhoneCall(
+ eventTimestamp,
+ currentTime,
+ duration,
+ targetKey,
+ type,
+ targetIsContact,
+ phoneNumber == null,
+ target.length
+ )
+ )
+ }
}
private fun sendPhoneSms(eventTimestamp: Double, target: String, typeCode: Int, message: String, targetIsContact: Boolean) {
@@ -246,21 +276,27 @@ class PhoneLogManager(context: PhoneLogService) : AbstractSourceManager(context) {
- private val usageEventTopic: DataCache?
- private val userInteractionTopic: DataCache = createCache("android_phone_user_interaction", PhoneUserInteraction())
+ private val usageEventTopic: Deferred>?
+ private val userInteractionTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_user_interaction",
+ PhoneUserInteraction()
+ )
+ }
private val phoneStateReceiver: BroadcastReceiver
private val usageStatsManager: UsageStatsManager?
@@ -50,12 +62,16 @@ class PhoneUsageManager(context: PhoneUsageService) : AbstractSourceManager
+
+
+
(service) {
private val processor: OfflineProcessor
- private val bluetoothDevicesTopic: DataCache = createCache("android_phone_bluetooth_devices", PhoneBluetoothDevices())
- private val bluetoothScannedTopic: DataCache = createCache("android_phone_bluetooth_device_scanned", PhoneBluetoothDeviceScanned())
+ private val bluetoothDevicesTopic: Deferred> =
+ service.lifecycleScope.async(
+ Dispatchers.Default
+ ) {
+ createCache(
+ "android_phone_bluetooth_devices",
+ PhoneBluetoothDevices()
+ )
+ }
+ private val bluetoothScannedTopic: Deferred> =
+ service.lifecycleScope.async(
+ Dispatchers.Default
+ ) {
+ createCache(
+ "android_phone_bluetooth_device_scanned",
+ PhoneBluetoothDeviceScanned()
+ )
+ }
+
+ private val bluetoothTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private var bluetoothBroadcastReceiver: BroadcastReceiver? = null
private val hashGenerator: HashGenerator = HashGenerator(service, "bluetooth_devices")
@@ -82,10 +106,11 @@ class PhoneBluetoothManager(service: PhoneBluetoothService) : AbstractSourceMana
status = SourceStatusListener.Status.READY
register()
processor.start()
+ bluetoothTaskExecutor.start(SupervisorJob())
status = SourceStatusListener.Status.CONNECTED
}
- private fun processBluetoothDevices() {
+ private suspend fun processBluetoothDevices() {
val bluetoothAdapter = service.bluetoothAdapter
if (bluetoothAdapter == null) {
logger.error("Bluetooth is not available.")
@@ -133,21 +158,24 @@ class PhoneBluetoothManager(service: PhoneBluetoothService) : AbstractSourceMana
pairedDevices.forEach { bd ->
val mac = bd.address
- val hash = hashGenerator.createHashByteBuffer(mac + "$hashSaltReference")
+ val hash =
+ hashGenerator.createHashByteBuffer(mac + "$hashSaltReference")
- send(bluetoothScannedTopic, scannedTopicBuilder.apply {
+ bluetoothTaskExecutor.execute {
+ send(bluetoothScannedTopic.await(), scannedTopicBuilder.apply {
this.macAddressHash = hash
this.pairedState = bd.bondState.toPairedState()
this.hashSaltReference = hashSaltReference
}.build())
}
-
- send(bluetoothScannedTopic, scannedTopicBuilder.apply {
- this.macAddressHash = macAddressHash
- this.pairedState = device.bondState.toPairedState()
- this.hashSaltReference = hashSaltReference
- }.build())
-
+ }
+ bluetoothTaskExecutor.execute {
+ send(bluetoothScannedTopic.await(), scannedTopicBuilder.apply {
+ this.macAddressHash = macAddressHash
+ this.pairedState = device.bondState.toPairedState()
+ this.hashSaltReference = hashSaltReference
+ }.build())
+ }
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
@@ -158,8 +186,13 @@ class PhoneBluetoothManager(service: PhoneBluetoothService) : AbstractSourceMana
if (!isClosed) {
val time = currentTime
- send(bluetoothDevicesTopic, PhoneBluetoothDevices(
- time, time, bondedDevices, numberOfDevices, true))
+ bluetoothTaskExecutor.execute {
+ send(
+ bluetoothDevicesTopic.await(), PhoneBluetoothDevices(
+ time, time, bondedDevices, numberOfDevices, true
+ )
+ )
+ }
}
}
}
@@ -170,13 +203,15 @@ class PhoneBluetoothManager(service: PhoneBluetoothService) : AbstractSourceMana
bluetoothAdapter.startDiscovery()
} else {
val time = currentTime
- send(bluetoothDevicesTopic, PhoneBluetoothDevices(
+ send(bluetoothDevicesTopic.await(), PhoneBluetoothDevices(
time, time, null, null, false))
}
}
override fun onClose() {
- processor.close()
+ bluetoothTaskExecutor.stop {
+ processor.stop()
+ }
bluetoothBroadcastReceiver?.let {
try {
service.unregisterReceiver(it)
diff --git a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneContactListManager.kt b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneContactListManager.kt
index 387cb38de..7d9c38062 100644
--- a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneContactListManager.kt
+++ b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneContactListManager.kt
@@ -23,25 +23,37 @@ import android.database.Cursor
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceStatusListener
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.OfflineProcessor
import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.phone.PhoneContactList
-import java.util.*
import java.util.concurrent.TimeUnit
-import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashSet
class PhoneContactListManager(service: PhoneContactsListService) : AbstractSourceManager(service) {
private val preferences: SharedPreferences = service.getSharedPreferences(PhoneContactListManager::class.java.name, Context.MODE_PRIVATE)
- private val contactsTopic: DataCache = createCache("android_phone_contacts", PhoneContactList())
+ private val contactsTopic: Deferred> = service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_contacts",
+ PhoneContactList()
+ )
+ }
private val processor: OfflineProcessor
private val db: ContentResolver = service.contentResolver
private var savedContactLookups: Set = emptySet()
+ private val contactsTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
+
init {
name = service.getString(R.string.contact_list)
processor = OfflineProcessor(service) {
@@ -50,6 +62,7 @@ class PhoneContactListManager(service: PhoneContactsListService) : AbstractSourc
requestName = ACTION_UPDATE_CONTACTS_LIST
wake = false
}
+ contactsTaskExecutor.start()
}
override fun start(acceptableIds: Set) {
@@ -58,11 +71,14 @@ class PhoneContactListManager(service: PhoneContactsListService) : AbstractSourc
processor.start {
// deprecated using contact _ID, using LOOKUP instead.
- preferences.edit()
+ withContext(Dispatchers.IO) {
+ preferences.edit()
.remove(CONTACT_IDS)
.apply()
- savedContactLookups = preferences.getStringSet(CONTACT_LOOKUPS, emptySet()) ?: emptySet()
+ savedContactLookups =
+ preferences.getStringSet(CONTACT_LOOKUPS, emptySet()) ?: emptySet()
+ }
}
status = SourceStatusListener.Status.CONNECTED
@@ -111,7 +127,9 @@ class PhoneContactListManager(service: PhoneContactsListService) : AbstractSourc
}
override fun onClose() {
- processor.close()
+ contactsTaskExecutor.stop {
+ processor.stop()
+ }
}
private fun makeQuery(
@@ -135,7 +153,7 @@ class PhoneContactListManager(service: PhoneContactsListService) : AbstractSourc
}
}
- private fun processContacts() {
+ private suspend fun processContacts() {
val newContactLookups = queryContacts() ?: return
var added: Int? = null
@@ -147,12 +165,14 @@ class PhoneContactListManager(service: PhoneContactsListService) : AbstractSourc
}
savedContactLookups = newContactLookups
- preferences.edit()
+ withContext(Dispatchers.IO) {
+ preferences.edit()
.putStringSet(CONTACT_LOOKUPS, savedContactLookups)
.apply()
+ }
val timestamp = currentTime
- send(contactsTopic, PhoneContactList(timestamp, timestamp, added, removed, newContactLookups.size))
+ send(contactsTopic.await(), PhoneContactList(timestamp, timestamp, added, removed, newContactLookups.size))
}
internal fun setCheckInterval(checkInterval: Long, unit: TimeUnit) {
diff --git a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationManager.kt b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationManager.kt
index 0ee429f0d..3cea694f7 100644
--- a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationManager.kt
+++ b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationManager.kt
@@ -18,23 +18,24 @@ package org.radarbase.passive.phone
import android.annotation.SuppressLint
import android.content.Context
-import android.content.SharedPreferences
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
-import android.os.Process
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
+import org.radarbase.android.source.SourceService.Companion.DEVICE_LOCATION_CHANGED
import org.radarbase.android.source.SourceStatusListener
import org.radarbase.android.util.BatteryStageReceiver
import org.radarbase.android.util.ChangeRunner
-import org.radarbase.android.util.SafeHandler
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.StageLevels
-import org.radarbase.android.util.send
-import org.radarbase.passive.google.places.GooglePlacesManager.Companion.DEVICE_LOCATION_CHANGED
import org.radarbase.passive.phone.PhoneLocationService.Companion.LOCATION_GPS_INTERVAL_DEFAULT
import org.radarbase.passive.phone.PhoneLocationService.Companion.LOCATION_GPS_INTERVAL_REDUCED_DEFAULT
import org.radarbase.passive.phone.PhoneLocationService.Companion.LOCATION_NETWORK_INTERVAL_DEFAULT
@@ -47,9 +48,15 @@ import java.math.BigDecimal
import java.util.concurrent.ThreadLocalRandom
class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManager(context), LocationListener {
- private val locationTopic: DataCache = createCache("android_phone_relative_location", PhoneRelativeLocation())
+ private val locationTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache(
+ "android_phone_relative_location",
+ PhoneRelativeLocation()
+ )
+ }
private val locationManager = service.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
- private val handler = SafeHandler.getInstance("PhoneLocation", Process.THREAD_PRIORITY_BACKGROUND)
+ private val locationExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private val batteryLevelReceiver = BatteryStageReceiver(context, StageLevels(0.1f, 0.3f), ::onBatteryLevelChanged)
private var latitudeReference: BigDecimal? = null
private var longitudeReference: BigDecimal? = null
@@ -58,56 +65,59 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
private val intervals = ChangeRunner(LocationPollingIntervals())
private var isStarted: Boolean = false
private var referenceId: Int = 0
- private lateinit var broadcaster: LocalBroadcastManager
@Volatile
var isAbsoluteLocation: Boolean = false
- private val preferences: SharedPreferences
- get() = service.getSharedPreferences(PhoneLocationService::class.java.name, Context.MODE_PRIVATE)
-
init {
name = service.getString(R.string.phoneLocationServiceDisplayName)
- preferences.apply {
- latitudeReference = getString(LATITUDE_REFERENCE, null)
+ }
+
+ override fun start(acceptableIds: Set) {
+ locationManager ?: return
+ register()
+ locationExecutor.start()
+
+ locationExecutor.execute {
+ service.withMutablePreferences { editor ->
+ latitudeReference = getString(LATITUDE_REFERENCE, null)
?.let { BigDecimal(it) }
- longitudeReference = getString(LONGITUDE_REFERENCE, null)
+ longitudeReference = getString(LONGITUDE_REFERENCE, null)
?.let { BigDecimal(it) }
- if (contains(ALTITUDE_REFERENCE)) {
- try {
- altitudeReference = Double.fromBits(getLong(ALTITUDE_REFERENCE, 0))
- } catch (ex: ClassCastException) {
- // to fix bug where this was stored as String
- altitudeReference = getString(ALTITUDE_REFERENCE, "0.0")?.toDouble() ?: 0.0
- edit().putLong(ALTITUDE_REFERENCE, altitudeReference.toRawBits()).apply()
+ var editorWasUsed = false
+
+ if (contains(ALTITUDE_REFERENCE)) {
+ try {
+ altitudeReference = Double.fromBits(getLong(ALTITUDE_REFERENCE, 0))
+ } catch (ex: ClassCastException) {
+ // to fix bug where this was stored as String
+ altitudeReference = getString(ALTITUDE_REFERENCE, "0.0")?.toDouble() ?: 0.0
+ editor.putLong(ALTITUDE_REFERENCE, altitudeReference.toRawBits())
+ editorWasUsed = true
+ }
+ } else {
+ altitudeReference = Double.NaN
}
- } else {
- altitudeReference = Double.NaN
- }
- if (contains(REFERENCE_ID)) {
- referenceId = getInt(REFERENCE_ID, -1)
- } else {
- val random = ThreadLocalRandom.current()
- while (referenceId == 0) {
- referenceId = random.nextInt()
+ if (contains(REFERENCE_ID)) {
+ referenceId = getInt(REFERENCE_ID, -1)
+ } else {
+ val random = ThreadLocalRandom.current()
+ while (referenceId == 0) {
+ referenceId = random.nextInt()
+ }
+ editor.putInt(REFERENCE_ID, referenceId)
+ editorWasUsed = true
}
- edit().putInt(REFERENCE_ID, referenceId).apply()
+
+ editorWasUsed
}
}
- isStarted = false
- }
-
- override fun start(acceptableIds: Set) {
- locationManager ?: return
- register()
- handler.start()
status = SourceStatusListener.Status.READY
- broadcaster = LocalBroadcastManager.getInstance(service)
- handler.execute {
+ locationExecutor.execute {
isStarted = true
batteryLevelReceiver.register()
status = SourceStatusListener.Status.CONNECTED
@@ -139,9 +149,14 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
eventTimestamp, timestamp, reference, provider,
latitude.normalize(), longitude.normalize(),
altitude?.normalize(), accuracy?.normalize(), speed?.normalize(), bearing?.normalize())
- send(locationTopic, value)
+ locationExecutor.execute {
+ send(locationTopic.await(), value)
+ }
+
+ locationExecutor.execute {
+ service.locationChangedBroadcast.emit(DEVICE_LOCATION_CHANGED)
+ }
- broadcaster.send(DEVICE_LOCATION_CHANGED)
logger.info("Location: {} {} {} {} {} {} {} {} {}", provider, eventTimestamp, latitude,
longitude, accuracy, altitude, speed, bearing, timestamp)
}
@@ -155,7 +170,7 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
@SuppressLint("MissingPermission")
fun setLocationUpdateRate(periodGPS: Long, periodNetwork: Long) {
- handler.executeReentrant {
+ locationExecutor.executeReentrant {
if (!isStarted || locationManager == null) {
return@executeReentrant
}
@@ -202,9 +217,12 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
val reference = ThreadLocalRandom.current().nextDouble(-4.0, 4.0) // interval [-4,4)
latitudeReference = BigDecimal.valueOf(reference)
.also {
- preferences.edit()
- .putString(LATITUDE_REFERENCE, it.toString())
- .apply()
+ locationExecutor.execute {
+ service.withMutablePreferences { editor ->
+ editor.putString(LATITUDE_REFERENCE, it.toString())
+ true
+ }
+ }
}
}
@@ -221,10 +239,12 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
val longitude = BigDecimal.valueOf(absoluteLongitude)
if (longitudeReference == null) {
longitudeReference = longitude
-
- preferences.edit()
- .putString(LONGITUDE_REFERENCE, longitude.toString())
- .apply()
+ locationExecutor.execute {
+ service.withMutablePreferences { editor ->
+ editor.putString(LONGITUDE_REFERENCE, longitude.toString())
+ true
+ }
+ }
}
val relativeLongitude = longitude.subtract(longitudeReference).toDouble()
@@ -250,15 +270,18 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
if (altitudeReference.isNaN()) {
altitudeReference = absoluteAltitude
- preferences.edit()
- .putLong(ALTITUDE_REFERENCE, absoluteAltitude.toRawBits())
- .apply()
+ locationExecutor.execute {
+ service.withMutablePreferences { editor ->
+ editor.putLong(ALTITUDE_REFERENCE, absoluteAltitude.toRawBits())
+ true
+ }
+ }
}
return (absoluteAltitude - altitudeReference).toFloat()
}
private fun onBatteryLevelChanged(stage: BatteryStageReceiver.BatteryStage) {
- handler.executeReentrant {
+ locationExecutor.executeReentrant {
frequency.applyIfChanged(stage) { resetPollingIntervals() }
}
}
@@ -276,7 +299,7 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
override fun onClose() {
locationManager?.let { manager ->
- handler.stop {
+ locationExecutor.stop {
batteryLevelReceiver.unregister()
manager.removeUpdates(this@PhoneLocationManager)
}
@@ -284,13 +307,13 @@ class PhoneLocationManager(context: PhoneLocationService) : AbstractSourceManage
}
fun setBatteryLevels(stageLevels: StageLevels) {
- handler.execute {
+ service.lifecycleScope.launch(Dispatchers.Default) {
batteryLevelReceiver.stageLevels = stageLevels
}
}
fun setIntervals(value: LocationPollingIntervals) {
- handler.execute {
+ service.lifecycleScope.launch(Dispatchers.Default) {
intervals.applyIfChanged(value) { resetPollingIntervals() }
}
}
diff --git a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationService.kt b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationService.kt
index d4c5d23a9..c635e3c79 100644
--- a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationService.kt
+++ b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneLocationService.kt
@@ -16,6 +16,12 @@
package org.radarbase.passive.phone
+import android.content.Context
+import android.content.SharedPreferences
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import org.radarbase.android.config.SingleRadarConfiguration
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceManager
@@ -28,6 +34,23 @@ class PhoneLocationService : SourceService() {
override val isBluetoothConnectionRequired: Boolean = false
+ private val prefLock = Mutex()
+ private val preferences: SharedPreferences
+ get() = getSharedPreferences("org.radarbase.passive.phone.PhoneLocationService", Context.MODE_PRIVATE)
+
+ suspend fun withPreferences(block: SharedPreferences.() -> Unit) = prefLock.withLock {
+ preferences.block()
+ }
+ suspend fun withMutablePreferences(block: SharedPreferences.(SharedPreferences.Editor) -> Boolean) = prefLock.withLock {
+ val prefs = preferences
+ val editor = prefs.edit()
+ if (prefs.block(editor)) {
+ withContext(Dispatchers.IO) {
+ editor.commit()
+ }
+ }
+ }
+
override fun createSourceManager() = PhoneLocationManager(this)
override fun configureSourceManager(manager: SourceManager, config: SingleRadarConfiguration) {
diff --git a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorManager.kt b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorManager.kt
index 88d1043ad..75d94dca2 100644
--- a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorManager.kt
+++ b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorManager.kt
@@ -25,36 +25,72 @@ import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
-import android.os.BatteryManager.*
+import android.os.BatteryManager.BATTERY_STATUS_CHARGING
+import android.os.BatteryManager.BATTERY_STATUS_DISCHARGING
+import android.os.BatteryManager.BATTERY_STATUS_FULL
+import android.os.BatteryManager.BATTERY_STATUS_NOT_CHARGING
+import android.os.BatteryManager.BATTERY_STATUS_UNKNOWN
+import android.os.BatteryManager.EXTRA_LEVEL
+import android.os.BatteryManager.EXTRA_PLUGGED
+import android.os.BatteryManager.EXTRA_SCALE
+import android.os.BatteryManager.EXTRA_STATUS
import android.os.PowerManager
-import android.os.Process.THREAD_PRIORITY_BACKGROUND
import android.os.SystemClock
import android.util.SparseArray
import android.util.SparseIntArray
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.SourceStatusListener
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.OfflineProcessor
-import org.radarbase.android.util.SafeHandler
import org.radarbase.passive.phone.PhoneSensorService.Companion.PHONE_SENSOR_INTERVAL_DEFAULT
import org.radarcns.kafka.ObservationKey
-import org.radarcns.passive.phone.*
+import org.radarcns.passive.phone.BatteryStatus
+import org.radarcns.passive.phone.PhoneAcceleration
+import org.radarcns.passive.phone.PhoneBatteryLevel
+import org.radarcns.passive.phone.PhoneGyroscope
+import org.radarcns.passive.phone.PhoneLight
+import org.radarcns.passive.phone.PhoneMagneticField
+import org.radarcns.passive.phone.PhoneStepCount
import org.slf4j.LoggerFactory
import java.util.concurrent.TimeUnit
class PhoneSensorManager(context: PhoneSensorService) : AbstractSourceManager(context), SensorEventListener {
- private val accelerationTopic: DataCache = createCache("android_phone_acceleration", PhoneAcceleration())
- private val lightTopic: DataCache = createCache("android_phone_light", PhoneLight())
- private val stepCountTopic: DataCache = createCache("android_phone_step_count", PhoneStepCount())
- private val gyroscopeTopic: DataCache = createCache("android_phone_gyroscope", PhoneGyroscope())
- private val magneticFieldTopic: DataCache = createCache("android_phone_magnetic_field", PhoneMagneticField())
- private val batteryTopic: DataCache = createCache("android_phone_battery_level", PhoneBatteryLevel())
+ private val accelerationTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_phone_acceleration", PhoneAcceleration())
+ }
+ private val lightTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_phone_light", PhoneLight())
+ }
+ private val stepCountTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_phone_step_count", PhoneStepCount())
+ }
+ private val gyroscopeTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_phone_gyroscope", PhoneGyroscope())
+ }
+ private val magneticFieldTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_phone_magnetic_field", PhoneMagneticField())
+ }
+ private val batteryTopic: Deferred> = context.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_phone_battery_level", PhoneBatteryLevel())
+ }
var sensorDelays: SparseIntArray = SparseIntArray()
set(value) {
- mHandler.execute(defaultToCurrentThread = true) {
+ service.lifecycleScope.launch(Dispatchers.Default) {
if (field.contentsEquals(value)) {
- return@execute
+ return@launch
}
field = value
@@ -66,7 +102,7 @@ class PhoneSensorManager(context: PhoneSensorService) : AbstractSourceManager()
- private val mHandler = SafeHandler.getInstance("Phone sensors", THREAD_PRIORITY_BACKGROUND)
+ private val sensorTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private val sensorManager: SensorManager? = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager?
private val batteryProcessor: OfflineProcessor
@@ -92,8 +128,8 @@ class PhoneSensorManager(context: PhoneSensorService) : AbstractSourceManager) {
register()
- mHandler.start()
- mHandler.execute {
+ sensorTaskExecutor.start()
+ service.lifecycleScope.launch(Dispatchers.Default) {
wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm ->
pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.phone:PhoneSensorManager")
.also { it.acquire() }
@@ -116,7 +152,7 @@ class PhoneSensorManager(context: PhoneSensorService) : AbstractSourceManager
- processSensorEvent(sensorType, postponedTime, postponedEvent, sendState)
+ sendState.postponeEvent(time, event, sensorTaskExecutor, delay) { postponedTime, postponedEvent ->
+ sensorTaskExecutor.execute {
+ processSensorEvent(sensorType, postponedTime, postponedEvent, sendState)
+ }
}
}
}
}
- private fun processSensorEvent(
+ private suspend fun processSensorEvent(
sensorType: Int,
time: Double,
event: SensorEvent,
@@ -192,41 +230,41 @@ class PhoneSensorManager(context: PhoneSensorService) : AbstractSourceManager Unit,
) {
postponedTime = time
postponedEvent = event
if (postponeFuture == null) {
- postponeFuture = mHandler.delay(timeUntilNextIntervalEnds(delay)) {
+ postponeFuture = executor.delay(timeUntilNextIntervalEnds(delay)) {
postponeFuture = null
process(
postponedTime ?: return@delay,
diff --git a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorProvider.kt b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorProvider.kt
index a8310273c..4ad6b297e 100644
--- a/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorProvider.kt
+++ b/plugins/radar-android-phone/src/main/java/org/radarbase/passive/phone/PhoneSensorProvider.kt
@@ -16,6 +16,8 @@
package org.radarbase.passive.phone
+import android.Manifest
+import android.os.Build
import org.radarbase.android.BuildConfig
import org.radarbase.android.RadarService
import org.radarbase.android.source.SourceProvider
@@ -36,7 +38,7 @@ open class PhoneSensorProvider(radarService: RadarService) : SourceProvider = emptyList()
+ override val permissionsNeeded: List = listOf(SENSOR_ACTIVITY_RECOGNITION_COMPAT)
override val sourceProducer: String = PRODUCER
@@ -47,5 +49,7 @@ open class PhoneSensorProvider(radarService: RadarService) : SourceProvider= 29 ) Manifest.permission.ACTIVITY_RECOGNITION
+ else "com.google.android.gms.permission.ACTIVITY_RECOGNITION"
}
}
diff --git a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt
index 50c39bea6..59c9688ee 100644
--- a/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt
+++ b/plugins/radar-android-polar/src/main/java/org/radarbase/passive/polar/PolarManager.kt
@@ -1,22 +1,26 @@
package org.radarbase.passive.polar
+import PolarUtils
import android.annotation.SuppressLint
import android.content.Context.POWER_SERVICE
import android.os.PowerManager
-import android.os.Process.THREAD_PRIORITY_BACKGROUND
import android.util.Log
+import androidx.lifecycle.lifecycleScope
import com.polar.sdk.api.PolarBleApi
import com.polar.sdk.api.PolarBleApiCallback
import com.polar.sdk.api.PolarBleApiDefaultImpl.defaultImplementation
import com.polar.sdk.api.errors.PolarInvalidArgument
import com.polar.sdk.api.model.*
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
-import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.disposables.Disposable
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.SourceStatusListener
-import org.radarbase.android.util.SafeHandler
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.polar.*
import org.slf4j.LoggerFactory
@@ -27,20 +31,32 @@ class PolarManager(
polarService: PolarService,
) : AbstractSourceManager(polarService) {
- private val accelerationTopic: DataCache =
- createCache("android_polar_acceleration", PolarAcceleration())
- private val batteryLevelTopic: DataCache =
+ private val accelerationTopic: Deferred> = polarService.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_polar_acceleration", PolarAcceleration())
+ }
+ private val batteryLevelTopic: Deferred> = polarService.lifecycleScope.async(
+ Dispatchers.Default) {
createCache("android_polar_battery_level", PolarBatteryLevel())
- private val ecgTopic: DataCache =
+ }
+ private val ecgTopic: Deferred> = polarService.lifecycleScope.async(
+ Dispatchers.Default) {
createCache("android_polar_ecg", PolarEcg())
- private val heartRateTopic: DataCache =
+ }
+ private val heartRateTopic: Deferred> = polarService.lifecycleScope.async(
+ Dispatchers.Default) {
createCache("android_polar_heart_rate", PolarHeartRate())
- private val ppIntervalTopic: DataCache =
+ }
+ private val ppIntervalTopic: Deferred> = polarService.lifecycleScope.async(
+ Dispatchers.Default) {
createCache("android_polar_pulse_to_pulse_interval", PolarPpInterval())
- private val ppgTopic: DataCache =
+ }
+ private val ppgTopic: Deferred> = polarService.lifecycleScope.async(
+ Dispatchers.Default) {
createCache("android_polar_ppg", PolarPpg())
+ }
- private val mHandler = SafeHandler.getInstance("Polar sensors", THREAD_PRIORITY_BACKGROUND)
+ private val polarExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
private var wakeLock: PowerManager.WakeLock? = null
private lateinit var api: PolarBleApi
@@ -68,8 +84,8 @@ class PolarManager(
connectToPolarSDK()
register()
- mHandler.start()
- mHandler.execute {
+ polarExecutor.start()
+ polarExecutor.execute {
wakeLock = (service.getSystemService(POWER_SERVICE) as PowerManager?)?.let { pm ->
pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.radarcns.polar:PolarManager")
.also { it.acquire() }
@@ -163,9 +179,9 @@ class PolarManager(
val batteryLevel = level.toFloat() / 100.0f
state.batteryLevel = batteryLevel
logger.debug("Battery level $level%, which is $batteryLevel at $currentTime")
- mHandler.execute {
+ polarExecutor.execute {
send(
- batteryLevelTopic,
+ batteryLevelTopic.await(),
PolarBatteryLevel(name, currentTime, currentTime, batteryLevel)
)
}
@@ -210,7 +226,7 @@ class PolarManager(
}
disconnectToPolarSDK(deviceId)
- mHandler.stop {
+ polarExecutor.stop {
wakeLock?.release()
}
@@ -302,9 +318,9 @@ class PolarManager(
"contactStatus: ${sample.contactStatus} " +
"contactStatusSupported: ${sample.contactStatusSupported}"
)
- mHandler.execute {
+ polarExecutor.execute {
send(
- heartRateTopic,
+ heartRateTopic.await(),
PolarHeartRate(
name,
getTimeNano(),
@@ -353,9 +369,9 @@ class PolarManager(
PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)
} currentTime: $currentTime PolarTimeStamp: ${data.timeStamp}"
)
- mHandler.execute {
+ polarExecutor.execute {
send(
- ecgTopic,
+ ecgTopic.await(),
PolarEcg(
name,
PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp),
@@ -396,9 +412,9 @@ class PolarManager(
PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)
} currentTime: $currentTime PolarTimeStamp: ${data.timeStamp}"
)
- mHandler.execute {
+ polarExecutor.execute {
send(
- accelerationTopic,
+ accelerationTopic.await(),
PolarAcceleration(
name,
PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp),
@@ -445,9 +461,9 @@ class PolarManager(
PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp)
} currentTime: $currentTime PolarTimeStamp: ${data.timeStamp}"
)
- mHandler.execute {
+ polarExecutor.execute {
send(
- ppgTopic,
+ ppgTopic.await(),
PolarPpg(
name,
PolarUtils.convertEpochPolarToUnixEpoch(data.timeStamp),
@@ -486,9 +502,9 @@ class PolarManager(
"errorEstimate: ${sample.errorEstimate} " +
"currentTime: $currentTime"
)
- mHandler.execute {
+ polarExecutor.execute {
send(
- ppIntervalTopic,
+ ppIntervalTopic.await(),
PolarPpInterval(
name,
currentTime,
diff --git a/plugins/radar-android-weather/src/main/java/net/aksingh/owmjapis/OpenWeatherMap.kt b/plugins/radar-android-weather/src/main/java/net/aksingh/owmjapis/OpenWeatherMap.kt
index 8dfb149ca..6b17d0adf 100755
--- a/plugins/radar-android-weather/src/main/java/net/aksingh/owmjapis/OpenWeatherMap.kt
+++ b/plugins/radar-android-weather/src/main/java/net/aksingh/owmjapis/OpenWeatherMap.kt
@@ -21,9 +21,14 @@
*/
package net.aksingh.owmjapis
-import okhttp3.CacheControl
-import okhttp3.OkHttpClient
-import okhttp3.Request
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.header
+import io.ktor.client.request.prepareRequest
+import io.ktor.client.request.url
+import io.ktor.http.HttpHeaders
+import io.ktor.http.isSuccess
+import kotlinx.coroutines.runBlocking
import org.json.JSONException
import org.json.JSONObject
import org.slf4j.LoggerFactory
@@ -66,7 +71,7 @@ import java.net.URLEncoder
* @since 2.5.0.1
*/
@Suppress("unused")
-class OpenWeatherMap(units: String, lang: String, apiKey: String, client: OkHttpClient) {
+class OpenWeatherMap(units: String, lang: String, apiKey: String, client: HttpClient) {
private val owmAddressInstance: OWMAddress = OWMAddress(units, lang, apiKey)
private val owmResponse: OWMResponse = OWMResponse(client, owmAddressInstance)
@@ -327,7 +332,7 @@ class OpenWeatherMap(units: String, lang: String, apiKey: String, client: OkHttp
* @since 2.5.0.3
*/
private class OWMResponse(
- private val client: OkHttpClient,
+ private val client: HttpClient,
private val owmAddress: OWMAddress,
) {
/*
@@ -412,29 +417,31 @@ class OpenWeatherMap(units: String, lang: String, apiKey: String, client: OkHttp
* @return Response if successful, else `null`
* @see [HTTP -
](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) */
- private fun httpGet(requestAddress: String): String? {
- val request: Request = Request.Builder()
- .get()
- .url(requestAddress)
- .cacheControl(CacheControl.FORCE_NETWORK)
- .header("Accept-Encoding", "gzip, deflate")
- .build()
- return try {
- client.newCall(request).execute().use { response ->
- val responseString = response.body?.string()
- if (response.isSuccessful && responseString != null) {
+ private fun httpGet(requestAddress: String): String? = runBlocking{
+
+ val request = client.prepareRequest {
+ url(requestAddress)
+ header(HttpHeaders.AcceptEncoding, "gzip, deflate")
+ header(HttpHeaders.CacheControl, "no-cache")
+ header(HttpHeaders.Pragma, "no-cache")
+ }
+
+ try {
+ request.execute { response ->
+ val responseString = response.body()
+ if (response.status.isSuccess() && responseString.isNotEmpty()) {
responseString
} else {
logger.error(
"Failed to request body (HTTP code {}): {}",
- response.code,
+ response.status.value,
responseString,
)
null
}
}
- } catch (e: IOException) {
- logger.error("Failed to call OpenWeatherMap API", e)
+ } catch (e: Exception) {
+ logger.error("Failed to call API", e)
null
}
}
diff --git a/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/OpenWeatherMapApi.kt b/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/OpenWeatherMapApi.kt
index dc88a0bd9..ad25cc227 100644
--- a/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/OpenWeatherMapApi.kt
+++ b/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/OpenWeatherMapApi.kt
@@ -16,16 +16,16 @@
package org.radarbase.passive.weather
+import io.ktor.client.HttpClient
import net.aksingh.owmjapis.CurrentWeather
import net.aksingh.owmjapis.OpenWeatherMap
-import okhttp3.OkHttpClient
import org.json.JSONException
import org.radarcns.passive.weather.WeatherCondition
import java.io.IOException
import java.math.BigDecimal
import java.util.*
-internal class OpenWeatherMapApi(apiKey: String, client: OkHttpClient) : WeatherApi {
+internal class OpenWeatherMapApi(apiKey: String, client: HttpClient) : WeatherApi {
private val owm: OpenWeatherMap = OpenWeatherMap(OpenWeatherMap.UNITS_METRIC,
OpenWeatherMap.LANGUAGE_ENGLISH, apiKey, client)
diff --git a/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiManager.kt b/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiManager.kt
index a96c7aaac..f4468a9d2 100755
--- a/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiManager.kt
+++ b/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiManager.kt
@@ -21,25 +21,38 @@ import android.location.Location
import android.location.LocationManager
import android.location.LocationManager.GPS_PROVIDER
import android.location.LocationManager.NETWORK_PROVIDER
-import okhttp3.OkHttpClient
+import androidx.lifecycle.lifecycleScope
+import io.ktor.client.HttpClient
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import org.radarbase.android.data.DataCache
import org.radarbase.android.source.AbstractSourceManager
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceStatusListener
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.NetworkConnectedReceiver
import org.radarbase.android.util.OfflineProcessor
import org.radarbase.passive.weather.WeatherApiService.Companion.WEATHER_QUERY_INTERVAL_DEFAULT
+import org.radarcns.kafka.ObservationKey
import org.radarcns.passive.weather.LocalWeather
import org.radarcns.passive.weather.LocationType
import org.slf4j.LoggerFactory
import java.io.IOException
import java.util.concurrent.TimeUnit
-class WeatherApiManager(service: WeatherApiService, private val client: OkHttpClient) : AbstractSourceManager(service) {
+class WeatherApiManager(service: WeatherApiService, private val client: HttpClient) : AbstractSourceManager(service) {
private val processor: OfflineProcessor
- private val weatherTopic = createCache("android_local_weather", LocalWeather())
+ private val weatherTopic: Deferred> = service.lifecycleScope.async(
+ Dispatchers.Default) {
+ createCache("android_local_weather", LocalWeather())
+ }
private val networkReceiver: NetworkConnectedReceiver
+ private val weatherTaskExecutor: CoroutineTaskExecutor = CoroutineTaskExecutor(this::class.simpleName!!)
+
private val locationManager: LocationManager? = service.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
@get:Synchronized
@@ -79,14 +92,16 @@ class WeatherApiManager(service: WeatherApiService, private val client: OkHttpCl
}
logger.info("Starting WeatherApiManager")
- networkReceiver.register()
+ weatherTaskExecutor.execute {
+ networkReceiver.monitor()
+ }
processor.start()
-
+ weatherTaskExecutor.start(SupervisorJob())
status = SourceStatusListener.Status.CONNECTED
}
- private fun processWeather() {
- if (!networkReceiver.state.isConnected) {
+ private suspend fun processWeather() {
+ if (networkReceiver.latestState !is NetworkConnectedReceiver.NetworkState.Connected) {
logger.warn("No internet connection. Skipping weather query.")
}
@@ -125,7 +140,7 @@ class WeatherApiManager(service: WeatherApiService, private val client: OkHttpCl
)
logger.info("Weather: {} {} {}", result, result.sunRise, result.sunSet)
- send(weatherTopic, weatherData)
+ send(weatherTopic.await(), weatherData)
} catch (e: IOException) {
logger.error("Could not get weather from {} API.", api)
}
@@ -158,8 +173,9 @@ class WeatherApiManager(service: WeatherApiService, private val client: OkHttpCl
}
override fun onClose() {
- networkReceiver.unregister()
- processor.close()
+ weatherTaskExecutor.stop {
+ processor.stop()
+ }
}
companion object {
diff --git a/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiService.kt b/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiService.kt
index 8a72d598c..0e1532b3a 100644
--- a/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiService.kt
+++ b/plugins/radar-android-weather/src/main/java/org/radarbase/passive/weather/WeatherApiService.kt
@@ -16,28 +16,25 @@
package org.radarbase.passive.weather
-import okhttp3.OkHttpClient
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.cio.CIO
import org.radarbase.android.config.SingleRadarConfiguration
import org.radarbase.android.source.BaseSourceState
import org.radarbase.android.source.SourceManager
import org.radarbase.android.source.SourceService
import org.radarbase.config.ServerConfig
import org.radarbase.passive.weather.WeatherApiManager.Companion.SOURCE_OPENWEATHERMAP
-import org.radarbase.producer.rest.RestClient
import java.util.concurrent.TimeUnit
class WeatherApiService : SourceService() {
- private lateinit var client: OkHttpClient
+ private lateinit var client: HttpClient
override val defaultState: BaseSourceState
get() = BaseSourceState()
override fun onCreate() {
super.onCreate()
- client = RestClient.global()
- .server(ServerConfig())
- .build()
- .httpClient // global OkHttpClient
+ client = HttpClient(CIO)
}
override fun createSourceManager() = WeatherApiManager(this, client)
@@ -51,6 +48,11 @@ class WeatherApiService : SourceService() {
)
}
+ override fun onDestroy() {
+ super.onDestroy()
+ client.close()
+ }
+
companion object {
private const val WEATHER_QUERY_INTERVAL = "weather_query_interval_seconds"
private const val WEATHER_API_SOURCE = "weather_api_source"
diff --git a/radar-commons-android/build.gradle b/radar-commons-android/build.gradle
index bd80e628f..c3ba5e101 100644
--- a/radar-commons-android/build.gradle
+++ b/radar-commons-android/build.gradle
@@ -15,6 +15,7 @@
*/
apply from: "$rootDir/gradle/android.gradle"
+apply plugin: 'kotlinx-serialization'
android {
namespace "org.radarbase.android"
@@ -30,28 +31,42 @@ android {
description = "Kafka backend for processing device data."
dependencies {
+ api(project(":avro-android"))
api ("org.radarbase:radar-commons:$radar_commons_version") {
- exclude group: "org.json", module: "json"
- exclude group: "org.apache.avro", module: "avro"
+ exclude group: 'org.json', module: 'json'
+ exclude group: 'org.apache.avro', module: 'avro'
}
+ api ("org.radarbase:radar-commons-kotlin:$radar_commons_version")
+ implementation('org.radarbase:radar-app-config-core:0.5.0')
api ("org.radarbase:radar-schemas-commons:$radar_schemas_commons_version") {
- exclude group: "org.apache.avro", module: "avro"
+ exclude group: 'org.apache.avro', module: 'avro'
+ exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core'
+ exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
}
- api(project(":avro-android"))
api("org.slf4j:slf4j-api:$slf4j_api_version")
api("androidx.appcompat:appcompat:$appcompat_version")
- implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
+ implementation("org.radarbase:managementportal-client:2.0.1-SNAPSHOT")
+
+ def lifecycleVersion = "2.6.1"
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
+
implementation "androidx.localbroadcastmanager:localbroadcastmanager:$localbroadcastmanager_version"
implementation "androidx.legacy:legacy-support-v4:$legacy_support_version"
api "androidx.lifecycle:lifecycle-service:$lifecycle_service_version"
+ implementation "androidx.lifecycle:lifecycle-livedata:2.6.1"
implementation platform("com.google.firebase:firebase-bom:$firebase_bom_version")
implementation "com.google.firebase:firebase-analytics"
implementation "com.google.firebase:firebase-config"
implementation "com.google.firebase:firebase-crashlytics"
implementation "com.gitlab.mvysny.slf4j:slf4j-handroid:$slf4j_handroid_version"
+
+ implementation("io.ktor:ktor-client-content-negotiation")
+ implementation("io.ktor:ktor-serialization-kotlinx-json")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
+ androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
apply from: "$rootDir/gradle/test.gradle"
diff --git a/radar-commons-android/src/androidTest/java/org/radarbase/android/auth/SharedPreferencesAuthSerializationTest.kt b/radar-commons-android/src/androidTest/java/org/radarbase/android/auth/SharedPreferencesAuthSerializationTest.kt
index 2577e53b9..f40101486 100644
--- a/radar-commons-android/src/androidTest/java/org/radarbase/android/auth/SharedPreferencesAuthSerializationTest.kt
+++ b/radar-commons-android/src/androidTest/java/org/radarbase/android/auth/SharedPreferencesAuthSerializationTest.kt
@@ -3,6 +3,7 @@ package org.radarbase.android.auth
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@@ -38,7 +39,7 @@ class SharedPreferencesAuthSerializationTest {
}
@Test
- fun addToPreferences() {
+ fun addToPreferences() = runTest{
val readState = ApplicationProvider.getApplicationContext().let { context ->
val authSerializer = SharedPreferencesAuthSerialization(context)
authSerializer.store(state)
@@ -57,7 +58,7 @@ class SharedPreferencesAuthSerializationTest {
assertTrue(state.isValidFor(9, TimeUnit.SECONDS))
assertFalse(state.isValidFor(11, TimeUnit.SECONDS))
assertEquals(LoginManager.AUTH_TYPE_BEARER.toLong(), state.tokenType.toLong())
- assertEquals("Bearer abcd", state.headers[0].value)
+ assertEquals("Bearer abcd", state.headers[0].second)
assertEquals(sources, state.sourceMetadata)
}
}
diff --git a/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTest.kt b/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTest.kt
index eb454ca2d..5ae6c0364 100644
--- a/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTest.kt
+++ b/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTest.kt
@@ -18,6 +18,8 @@ package org.radarbase.android.data
import android.os.Process.THREAD_PRIORITY_BACKGROUND
import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
import org.apache.avro.generic.GenericRecord
import org.junit.After
import org.junit.Assert.*
@@ -39,7 +41,6 @@ import java.util.concurrent.ThreadLocalRandom
@RunWith(AndroidJUnit4::class)
class TapeCacheTest {
- private lateinit var handler: SafeHandler
private lateinit var tapeCache: TapeCache
private lateinit var key: ObservationKey
private lateinit var value: ApplicationUptime
@@ -59,7 +60,7 @@ class TapeCacheTest {
Any::class.java, Any::class.java)
return TapeCache(
- folder.newFile(), topic, outputTopic, handler, serializationFactory,
+ folder.newFile(), topic, outputTopic, serializationFactory,
CacheConfiguration(100L))
}
@@ -74,11 +75,8 @@ class TapeCacheTest {
ObservationKey.getClassSchema(), ApplicationUptime.getClassSchema(),
Any::class.java, Any::class.java)
- handler = SafeHandler("TapeCacheTest", THREAD_PRIORITY_BACKGROUND).apply {
- start()
- }
tapeCache = TapeCache(folder.newFile(), topic,
- outputTopic, handler, serializationFactory,
+ outputTopic, serializationFactory,
CacheConfiguration(100, 4096, CacheConfiguration.QueueFileFactory.DIRECT))
key = ObservationKey("test", "a", "b")
@@ -88,64 +86,63 @@ class TapeCacheTest {
@After
@Throws(IOException::class)
- fun tearDown() {
- tapeCache.close()
- handler.stop { }
+ fun tearDown() = runTest{
+ tapeCache.stop()
}
- fun addMultipleMeasurements() {
+ fun addMultipleMeasurements() = runTest{
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
assertNull(tapeCache.getRecords(100))
repeat(50) {
tapeCache.addMeasurement(key, value)
}
- Thread.sleep(100)
+ Thread.sleep(150)
- assertEquals(50L, tapeCache.numberOfRecords)
+ assertEquals(50L, tapeCache.numberOfRecords.value)
var unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(50, unsent.size().toLong())
tapeCache.remove(50)
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
repeat(50) {
tapeCache.addMeasurement(key, value)
}
tapeCache.flush()
- assertEquals(50L, tapeCache.numberOfRecords)
+ assertEquals(50L, tapeCache.numberOfRecords.value)
unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(50, unsent.size().toLong())
tapeCache.remove(50)
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
}
@Test
@Throws(Exception::class)
- fun addMeasurement() {
+ fun addMeasurement() = runTest{
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
assertNull(tapeCache.getRecords(100))
tapeCache.addMeasurement(key, value)
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
- Thread.sleep(100)
+ Thread.sleep(150)
var unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(1, unsent.size().toLong())
- assertEquals(1L, tapeCache.numberOfRecords)
+ assertEquals(1L, tapeCache.numberOfRecords.value)
unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(1, unsent.size().toLong())
- assertEquals(1L, tapeCache.numberOfRecords)
+ assertEquals(1L, tapeCache.numberOfRecords.value)
val actualValue = unsent.iterator().next() as GenericRecord
assertEquals(key.getSourceId(), (unsent.key as GenericRecord).get("sourceId"))
assertEquals(value.getUptime(), actualValue.get("uptime"))
tapeCache.remove(1)
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
val emptyRecords = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)
if (emptyRecords != null) {
assertTrue("Contains ${emptyRecords.key}-${emptyRecords.toList()}", false)
@@ -154,16 +151,16 @@ class TapeCacheTest {
tapeCache.addMeasurement(key, value)
tapeCache.addMeasurement(key, value)
- Thread.sleep(100)
+ Thread.sleep(150)
unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(2, unsent.size().toLong())
- assertEquals(2L, tapeCache.numberOfRecords)
+ assertEquals(2L, tapeCache.numberOfRecords.value)
}
@Test
@Throws(IOException::class)
- fun testBinaryObject() {
+ fun testBinaryObject() = runTest{
val localTapeCache = audioCache
val localValue = getRecording(176482)
@@ -190,7 +187,7 @@ class TapeCacheTest {
@Test
@Throws(IOException::class)
- fun testMaxUnsentObject() {
+ fun testMaxUnsentObject() = runTest{
val localTapeCache = audioCache
localTapeCache.addMeasurement(key, getRecording(100000))
@@ -209,22 +206,22 @@ class TapeCacheTest {
@Test
@Throws(Exception::class)
- fun flush() {
+ fun flush() = runTest{
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
assertNull(tapeCache.getRecords(100))
tapeCache.addMeasurement(key, value)
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
tapeCache.flush()
val unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)
assertNotNull(unsent)
assertEquals(1, unsent?.size())
- assertEquals(1L, tapeCache.numberOfRecords)
+ assertEquals(1L, tapeCache.numberOfRecords.value)
}
companion object {
diff --git a/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTestDirect.kt b/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTestDirect.kt
index 152ae3740..bd114d073 100644
--- a/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTestDirect.kt
+++ b/radar-commons-android/src/androidTest/java/org/radarbase/android/data/TapeCacheTestDirect.kt
@@ -18,6 +18,7 @@ package org.radarbase.android.data
import android.os.Process.THREAD_PRIORITY_BACKGROUND
import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.test.runTest
import org.apache.avro.generic.GenericRecord
import org.junit.After
import org.junit.Assert.*
@@ -39,7 +40,6 @@ import java.util.concurrent.ThreadLocalRandom
@RunWith(AndroidJUnit4::class)
class TapeCacheTestDirect {
- private lateinit var handler: SafeHandler
private lateinit var tapeCache: TapeCache
private lateinit var key: ObservationKey
private lateinit var value: ApplicationUptime
@@ -59,7 +59,7 @@ class TapeCacheTestDirect {
Any::class.java, Any::class.java)
return TapeCache(
- folder.newFile(), topic, outputTopic, handler, serializationFactory,
+ folder.newFile(), topic, outputTopic, serializationFactory,
CacheConfiguration(100L))
}
@@ -74,11 +74,8 @@ class TapeCacheTestDirect {
ObservationKey.getClassSchema(), ApplicationUptime.getClassSchema(),
Any::class.java, Any::class.java)
- handler = SafeHandler("TapeCacheTest", THREAD_PRIORITY_BACKGROUND).apply {
- start()
- }
tapeCache = TapeCache(folder.newFile(), topic,
- outputTopic, handler, serializationFactory,
+ outputTopic, serializationFactory,
CacheConfiguration(100, 4096, CacheConfiguration.QueueFileFactory.DIRECT))
key = ObservationKey("test", "a", "b")
@@ -88,64 +85,63 @@ class TapeCacheTestDirect {
@After
@Throws(IOException::class)
- fun tearDown() {
- tapeCache.close()
- handler.stop { }
+ fun tearDown() = runTest {
+ tapeCache.stop()
}
- fun addMultipleMeasurements() {
+ fun addMultipleMeasurements() = runTest {
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
assertNull(tapeCache.getRecords(100))
repeat(50) {
tapeCache.addMeasurement(key, value)
}
- Thread.sleep(100)
+ Thread.sleep(150)
- assertEquals(50L, tapeCache.numberOfRecords)
+ assertEquals(50L, tapeCache.numberOfRecords.value)
var unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(50, unsent.size().toLong())
tapeCache.remove(50)
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
repeat(50) {
tapeCache.addMeasurement(key, value)
}
tapeCache.flush()
- assertEquals(50L, tapeCache.numberOfRecords)
+ assertEquals(50L, tapeCache.numberOfRecords.value)
unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(50, unsent.size().toLong())
tapeCache.remove(50)
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
}
@Test
@Throws(Exception::class)
- fun addMeasurement() {
+ fun addMeasurement() = runTest {
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
assertNull(tapeCache.getRecords(100))
tapeCache.addMeasurement(key, value)
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
- Thread.sleep(100)
+ Thread.sleep(150)
var unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(1, unsent.size().toLong())
- assertEquals(1L, tapeCache.numberOfRecords)
+ assertEquals(1L, tapeCache.numberOfRecords.value)
unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(1, unsent.size().toLong())
- assertEquals(1L, tapeCache.numberOfRecords)
+ assertEquals(1L, tapeCache.numberOfRecords.value)
val actualValue = unsent.iterator().next() as GenericRecord
assertEquals(key.getSourceId(), (unsent.key as GenericRecord).get("sourceId"))
assertEquals(value.getUptime(), actualValue.get("uptime"))
tapeCache.remove(1)
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
val emptyRecords = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)
if (emptyRecords != null) {
assertTrue("Contains ${emptyRecords.key}-${emptyRecords.toList()}", false)
@@ -154,16 +150,16 @@ class TapeCacheTestDirect {
tapeCache.addMeasurement(key, value)
tapeCache.addMeasurement(key, value)
- Thread.sleep(100)
+ Thread.sleep(150)
unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)!!
assertEquals(2, unsent.size().toLong())
- assertEquals(2L, tapeCache.numberOfRecords)
+ assertEquals(2L, tapeCache.numberOfRecords.value)
}
@Test
@Throws(IOException::class)
- fun testBinaryObject() {
+ fun testBinaryObject() = runTest {
val localTapeCache = audioCache
val localValue = getRecording(176482)
@@ -190,7 +186,7 @@ class TapeCacheTestDirect {
@Test
@Throws(IOException::class)
- fun testMaxUnsentObject() {
+ fun testMaxUnsentObject() = runTest {
val localTapeCache = audioCache
localTapeCache.addMeasurement(key, getRecording(100000))
@@ -209,22 +205,22 @@ class TapeCacheTestDirect {
@Test
@Throws(Exception::class)
- fun flush() {
+ fun flush() = runTest {
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
assertNull(tapeCache.getRecords(100))
tapeCache.addMeasurement(key, value)
assertNull(tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT))
- assertEquals(0L, tapeCache.numberOfRecords)
+ assertEquals(0L, tapeCache.numberOfRecords.value)
tapeCache.flush()
val unsent = tapeCache.getUnsentRecords(100, SIZE_LIMIT_DEFAULT)
assertNotNull(unsent)
assertEquals(1, unsent?.size())
- assertEquals(1L, tapeCache.numberOfRecords)
+ assertEquals(1L, tapeCache.numberOfRecords.value)
}
companion object {
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/AbstractRadarApplication.kt b/radar-commons-android/src/main/java/org/radarbase/android/AbstractRadarApplication.kt
index d5f1f8230..e67e51f4a 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/AbstractRadarApplication.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/AbstractRadarApplication.kt
@@ -19,22 +19,33 @@ package org.radarbase.android
import android.app.Application
import android.os.Bundle
import androidx.annotation.CallSuper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import org.radarbase.android.config.CombinedRadarConfig
import org.radarbase.android.config.LocalConfiguration
import org.radarbase.android.config.RemoteConfig
import org.radarbase.android.source.SourceService
import org.radarbase.android.util.NotificationHandler
import org.slf4j.impl.HandroidLoggerAdapter
+import java.util.concurrent.atomic.AtomicBoolean
/** Provides the name and some metadata of the main activity */
abstract class AbstractRadarApplication : Application(), RadarApplication {
- private lateinit var innerNotificationHandler: NotificationHandler
- override val notificationHandler: NotificationHandler
- get() = innerNotificationHandler
+ override lateinit var notificationHandler: NotificationHandler
override lateinit var configuration: RadarConfiguration
+ /**
+ * A boolean value indicating whether a plugin exists that utilizes the Google Places API's PlacesClient.
+ * This is used in the application class because it is efficient to create exactly one instance of PlacesClient
+ * for a single application instance.
+ */
+ val placesClientCreated: AtomicBoolean = AtomicBoolean(false)
+
override fun configureProvider(bundle: Bundle) {}
override fun onSourceServiceInvocation(service: SourceService<*>, bundle: Bundle, isNew: Boolean) {}
override fun onSourceServiceDestroy(service: SourceService<*>) {}
@@ -43,8 +54,16 @@ abstract class AbstractRadarApplication : Application(), RadarApplication {
override fun onCreate() {
super.onCreate()
setupLogging()
- configuration = createConfiguration()
- innerNotificationHandler = NotificationHandler(this)
+
+ runBlocking(Dispatchers.Default) {
+ launch {
+ notificationHandler = NotificationHandler(this@AbstractRadarApplication)
+ notificationHandler.onCreate()
+ }
+ launch {
+ configuration = createConfiguration()
+ }
+ }
}
protected open fun setupLogging() {
@@ -60,11 +79,14 @@ abstract class AbstractRadarApplication : Application(), RadarApplication {
*
* @return configured RadarConfiguration
*/
- protected open fun createConfiguration(): RadarConfiguration {
- return CombinedRadarConfig(
- LocalConfiguration(this),
- createRemoteConfiguration(),
- ::createDefaultConfiguration
+ protected open suspend fun createConfiguration(): RadarConfiguration = coroutineScope {
+ val localConfigurationJob = async(Dispatchers.IO) { LocalConfiguration(this@AbstractRadarApplication) }
+ val remoteConfigJob = async { createRemoteConfiguration() }
+ val defaultConfigJob = async { createDefaultConfiguration() }
+ CombinedRadarConfig(
+ localConfigurationJob.await(),
+ remoteConfigJob.await(),
+ defaultConfigJob.await(),
)
}
@@ -74,10 +96,10 @@ abstract class AbstractRadarApplication : Application(), RadarApplication {
*
* @return configured RadarConfiguration
*/
- protected open fun createRemoteConfiguration(): List = listOf()
+ protected open suspend fun createRemoteConfiguration(): List = listOf()
/**
* Create default configuration for the app.
*/
- protected open fun createDefaultConfiguration(): Map = mapOf()
+ protected open suspend fun createDefaultConfiguration(): Map = mapOf()
}
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/IRadarBinder.kt b/radar-commons-android/src/main/java/org/radarbase/android/IRadarBinder.kt
index 387177854..11eb01701 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/IRadarBinder.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/IRadarBinder.kt
@@ -17,16 +17,17 @@
package org.radarbase.android
import android.os.IBinder
+import kotlinx.coroutines.flow.StateFlow
import org.apache.avro.specific.SpecificRecord
import org.radarbase.android.data.DataHandler
-import org.radarbase.android.kafka.ServerStatusListener
+import org.radarbase.android.kafka.ServerStatus
import org.radarbase.android.source.SourceProvider
import org.radarbase.android.source.SourceServiceConnection
import org.radarbase.android.util.TimedLong
import org.radarcns.kafka.ObservationKey
interface IRadarBinder : IBinder {
- val serverStatus: ServerStatusListener.Status
+ val serverStatus: StateFlow
val latestNumberOfRecordsSent: TimedLong
@@ -36,8 +37,11 @@ interface IRadarBinder : IBinder {
fun setAllowedSourceIds(connection: SourceServiceConnection<*>, allowedIds: Collection)
- fun startScanning()
- fun stopScanning()
+ suspend fun startScanning()
+ suspend fun stopScanning()
fun needsBluetooth(): Boolean
+
+ fun flushCaches(successCallback: () -> Unit, errorCallback: () -> Unit)
+ fun permissionGranted(permissions: Array, grantResults: IntArray)
}
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/MainActivity.kt b/radar-commons-android/src/main/java/org/radarbase/android/MainActivity.kt
index d6920f40e..4c506ab30 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/MainActivity.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/MainActivity.kt
@@ -19,42 +19,70 @@ package org.radarbase.android
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.os.Process
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import com.google.firebase.analytics.FirebaseAnalytics
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import org.radarbase.android.RadarApplication.Companion.radarApp
import org.radarbase.android.RadarApplication.Companion.radarConfig
import org.radarbase.android.RadarConfiguration.Companion.PROJECT_ID_KEY
-import org.radarbase.android.RadarConfiguration.Companion.RADAR_CONFIGURATION_CHANGED
import org.radarbase.android.RadarConfiguration.Companion.UI_REFRESH_RATE_KEY
import org.radarbase.android.RadarConfiguration.Companion.USER_ID_KEY
+import org.radarbase.android.RadarConfiguration.RemoteConfigStatus.INITIAL
import org.radarbase.android.RadarService.Companion.ACTION_CHECK_PERMISSIONS
-import org.radarbase.android.RadarService.Companion.ACTION_PROVIDERS_UPDATED
import org.radarbase.android.RadarService.Companion.EXTRA_PERMISSIONS
+import org.radarbase.android.auth.AppAuthState
import org.radarbase.android.auth.AuthService
-import org.radarbase.android.config.CombinedRadarConfig
-import org.radarbase.android.util.*
+import org.radarbase.android.auth.AuthServiceStateReactor
+import org.radarbase.android.auth.LoginListener
+import org.radarbase.android.auth.LoginManager
+import org.radarbase.android.config.SingleRadarConfiguration
+import org.radarbase.android.util.BindState
+import org.radarbase.android.util.BluetoothEnforcer
+import org.radarbase.android.util.CoroutineTaskExecutor
+import org.radarbase.android.util.ManagedServiceConnection
+import org.radarbase.android.util.ManagedServiceConnection.Companion.serviceConnection
+import org.radarbase.android.util.PermissionBroadcast
+import org.radarbase.android.util.PermissionHandler
+import org.radarbase.kotlin.coroutines.launchJoin
import org.slf4j.LoggerFactory
+import java.io.File
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+typealias RadarServiceStateReactor = suspend (IRadarBinder) -> Unit
/** Base MainActivity class. It manages the services to collect the data and starts up a view. To
* create an application, extend this class and override the abstract methods. */
-abstract class MainActivity : AppCompatActivity() {
+@Suppress("MemberVisibilityCanBePrivate")
+abstract class MainActivity : AppCompatActivity(), LoginListener {
/** Time between refreshes. */
private var uiRefreshRate: Long = 0
/** Hander in the background. It is set to null whenever the activity is not running. */
- private lateinit var mHandler: SafeHandler
+ private lateinit var mainExecutor: CoroutineTaskExecutor
/** The UI to show the service data. */
@get:Synchronized
var view: MainActivityView? = null
private set
- private var configurationBroadcastReceiver: BroadcastRegistration? = null
private lateinit var permissionHandler: PermissionHandler
protected lateinit var authConnection: ManagedServiceConnection
protected lateinit var radarConnection: ManagedServiceConnection
@@ -62,13 +90,22 @@ abstract class MainActivity : AppCompatActivity() {
private lateinit var bluetoothEnforcer: BluetoothEnforcer
protected lateinit var configuration: RadarConfiguration
- private var connectionsUpdatedReceiver: BroadcastRegistration? = null
- protected open val requestPermissionTimeoutMs: Long
- get() = REQUEST_PERMISSION_TIMEOUT_MS
+ protected var radarServiceBinder: IRadarBinder? = null
+ private set
+ protected var authServiceBinder: AuthService.AuthServiceBinder? = null
+ private set
+
+ val radarBinder: IRadarBinder?
+ get() = radarServiceBinder
- val radarService: IRadarBinder?
- get() = radarConnection.binder
+ private var mainAuthRegistry: AuthService.LoginListenerRegistry? = null
+
+ protected open val requestPermissionTimeout: Duration
+ get() = REQUEST_PERMISSION_TIMEOUT
+
+ val radarService: StateFlow>
+ get() = radarConnection.state
val userId: String?
get() = configuration.latestConfig.optString(USER_ID_KEY)
@@ -76,6 +113,53 @@ abstract class MainActivity : AppCompatActivity() {
val projectId: String?
get() = configuration.latestConfig.optString(PROJECT_ID_KEY)
+ private val mutexCreateView: Mutex = Mutex()
+ private var connectionBound: Boolean = false
+
+ private var radarConnectionJob: Job? = null
+ private var authConnectionJob: Job? = null
+
+ protected var mainActivityView: MainActivityView? = null
+
+ private val radarServiceBoundActions: MutableList = mutableListOf(
+ IRadarBinder::startScanning,
+ {binder -> view?.onRadarServiceBound(binder)},
+ {
+ logger.debug("Radar Service bound to {}", this::class.simpleName)
+ }
+ )
+ private val radarServiceUnboundActions: MutableList = mutableListOf(
+ IRadarBinder::stopScanning,
+ {
+ logger.debug("Radar Service unbound from {}", this::class.simpleName)
+ }
+ )
+
+ private val authServiceBoundActions: MutableList =
+ mutableListOf({ binder ->
+ mainAuthRegistry = binder.addLoginListener(this)
+ binder.refreshIfOnline()
+ }, { binder ->
+ binder.applyState {
+ if (userId == null) {
+ this@MainActivity.logoutSucceeded(null, this)
+ }
+ }
+ }, {
+ logger.debug("Auth Service bound to {}", this::class.simpleName)
+ }
+ )
+
+ private val authServiceUnboundActions: MutableList = mutableListOf(
+ { binder ->
+ mainAuthRegistry?.let {
+ binder.removeLoginListener(it)
+ }
+ }, {
+ logger.debug("Auth Service unbound from {}", this::class.simpleName)
+ }
+ )
+
@CallSuper
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
@@ -86,20 +170,27 @@ abstract class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configuration = radarConfig
- mHandler = SafeHandler.getInstance("Main background handler", Process.THREAD_PRIORITY_BACKGROUND)
- permissionHandler = PermissionHandler(this, mHandler, requestPermissionTimeoutMs)
+ mainExecutor = CoroutineTaskExecutor(this::class.simpleName!!, Dispatchers.Default)
+ permissionHandler = PermissionHandler(
+ this,
+ mainExecutor,
+ _permissionsBroadcastReceiver,
+ requestPermissionTimeout.inWholeMilliseconds,
+ activityResultRegistry
+ )
savedInstanceState?.also { permissionHandler.restoreInstanceState(it) }
- radarConnection = ManagedServiceConnection(this@MainActivity, radarApp.radarService).apply {
- bindFlags = Context.BIND_ABOVE_CLIENT or Context.BIND_AUTO_CREATE
- onBoundListeners += IRadarBinder::startScanning
- onBoundListeners += { binder -> view?.onRadarServiceBound(binder) }
- onUnboundListeners += IRadarBinder::stopScanning
+ radarConnection = ManagedServiceConnection(
+ this,
+ radarApp.radarService,
+ IRadarBinder::class.java
+ ).apply {
+ bindFlags = Context.BIND_AUTO_CREATE or Context.BIND_ABOVE_CLIENT
}
- bluetoothEnforcer = BluetoothEnforcer(this, radarConnection)
- authConnection = ManagedServiceConnection(this, radarApp.authService)
+ bluetoothEnforcer = BluetoothEnforcer(this, radarConnection, radarServiceBoundActions, activityResultRegistry)
+ authConnection = serviceConnection(radarApp.authService)
create()
}
@@ -108,18 +199,23 @@ abstract class MainActivity : AppCompatActivity() {
logger.info("RADAR configuration at create: {}", configuration)
onConfigChanged()
- configurationBroadcastReceiver = LocalBroadcastManager.getInstance(this)
- .register(RADAR_CONFIGURATION_CHANGED) { _, _ -> onConfigChanged() }
+ connectionBound = false
// Start the UI thread
uiRefreshRate = configuration.latestConfig.getLong(UI_REFRESH_RATE_KEY, 250L)
- }
- @CallSuper
- override fun onDestroy() {
- super.onDestroy()
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ (application as RadarApplication).radarServiceImpl.actionProvidersUpdated.collect {
+ logger.trace("NewBroadcastTrace: ActionProviderUpdated: {}", it)
+ mutexCreateView.withLock {
+ logger.debug("Source providers updated, creating a new view")
+ view = createView()
+ }
+ }
+ }
+ }
- configurationBroadcastReceiver?.unregister()
}
/**
@@ -134,11 +230,11 @@ abstract class MainActivity : AppCompatActivity() {
/** Create a view to show the data of this activity. */
protected abstract fun createView(): MainActivityView
- private var uiUpdater: SafeHandler.HandlerFuture? = null
+ private var uiUpdater: CoroutineTaskExecutor.CoroutineFutureHandle? = null
override fun onResume() {
super.onResume()
- uiUpdater = mHandler.repeat(uiRefreshRate) {
+ uiUpdater = mainExecutor.repeat(uiRefreshRate) {
try {
// Update all rows in the UI with the data from the connections
view?.update()
@@ -159,8 +255,9 @@ abstract class MainActivity : AppCompatActivity() {
@CallSuper
public override fun onStart() {
super.onStart()
- mHandler.start()
- authConnection.bind()
+ mainExecutor.start()
+
+ mainActivityView?.stopLogoutProgress()
bluetoothEnforcer.start()
val radarServiceCls = radarApp.radarService
@@ -171,20 +268,84 @@ abstract class MainActivity : AppCompatActivity() {
logger.error("Failed to start RadarService: activity is in background.", ex)
}
- radarConnection.bind()
-
+ setUpConnectionJobs()
+
+ with(lifecycleScope) {
+ launch {
+ try {
+ connectionBound = true
+ authConnection.bind()
+ radarConnection.bind()
+ } catch (ex: Exception) {
+ connectionBound = false
+ throw ex
+ }
+ }
+ }
permissionHandler.invalidateCache()
+ radarConnectionJob?.start()
+ authConnectionJob?.start()
- LocalBroadcastManager.getInstance(this).apply {
- connectionsUpdatedReceiver = register(ACTION_PROVIDERS_UPDATED) { _, _ ->
- synchronized(this@MainActivity) {
- view = createView()
- }
+ lifecycleScope.launch {
+ mutexCreateView.withLock {
+ logger.trace("Creating a new view")
+ view = createView()
}
}
- synchronized(this@MainActivity) {
- view = createView()
+ }
+
+ private fun setUpConnectionJobs() {
+ with(lifecycleScope) {
+ radarConnectionJob = launch(start = CoroutineStart.LAZY) {
+ radarConnection.state
+ .collect { bindState: BindState ->
+ when (bindState) {
+ is ManagedServiceConnection.BoundService -> {
+ radarServiceBinder = bindState.binder
+ .also { binder ->
+ radarServiceBoundActions.launchJoin { action ->
+ action(binder)
+ }
+ }
+ }
+
+ is ManagedServiceConnection.Unbound -> {
+ radarServiceBinder?.also { binder ->
+ radarServiceUnboundActions.launchJoin { action ->
+ action(binder)
+ }
+ radarServiceBinder = null
+ }
+ }
+ }
+ }
+ }
+
+ authConnectionJob = launch(start = CoroutineStart.LAZY) {
+ authConnection.state
+ .collect { bindState ->
+ when (bindState) {
+ is ManagedServiceConnection.BoundService -> {
+ authServiceBinder = bindState.binder.also { binder ->
+ authServiceBoundActions.launchJoin {
+ it(binder)
+ }
+ }
+ }
+
+ is ManagedServiceConnection.Unbound -> {
+ authServiceBinder?.also { binder ->
+ authServiceUnboundActions.launchJoin {
+ it(binder)
+ }
+ authServiceBinder = null
+ }
+ }
+ }
+ }
+ }
}
+
}
override fun onNewIntent(intent: Intent) {
@@ -197,24 +358,27 @@ abstract class MainActivity : AppCompatActivity() {
@CallSuper
public override fun onStop() {
- super.onStop()
+ mainExecutor.stop { view = null }
- mHandler.stop { view = null }
-
- radarConnection.unbind()
- authConnection.unbind()
+ if (connectionBound) {
+ connectionBound = false
+ radarConnection.unbind()
+ authConnection.unbind()
+ }
bluetoothEnforcer.stop()
- connectionsUpdatedReceiver?.unregister()
- }
-
- public override fun onActivityResult(requestCode: Int, resultCode: Int, result: Intent?) {
- super.onActivityResult(requestCode, resultCode, result)
- bluetoothEnforcer.onActivityResult(requestCode, resultCode)
- permissionHandler.onActivityResult(requestCode, resultCode)
+ radarConnectionJob?.cancel()
+ authConnectionJob?.cancel()
+ radarConnectionJob = null
+ authConnectionJob = null
+ super.onStop()
}
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
permissionHandler.permissionsGranted(requestCode, permissions, grantResults)
}
@@ -225,15 +389,75 @@ abstract class MainActivity : AppCompatActivity() {
* the access token but allow the same user to automatically log in again if it is
* still valid.
*/
- protected fun logout(disableRefresh: Boolean) {
- authConnection.applyBinder { invalidate(null, disableRefresh) }
+ protected suspend fun logout(disableRefresh: Boolean) {
+ mainActivityView?.showLogoutProgress()
+ lifecycleScope.launch {
+ authConnection.applyBinder { invalidate(null, disableRefresh) }
+ }
logger.debug("Disabling Firebase Analytics")
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
}
+ override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) = Unit
+
+ override suspend fun logoutSucceeded(manager: LoginManager?, authState: AppAuthState) {
+ if (!connectionBound) {
+ connectionBound = false
+ radarConnection.unbind()
+ authConnection.unbind()
+ delay(400)
+ }
+ clearAppData(this)
+ delay(300)
+ logger.info("Starting SplashActivity")
+ val applicationPackage = packageName
+ withContext(Dispatchers.Main) {
+ mainActivityView?.stopLogoutProgress()
+ }
+ val intent = packageManager.getLaunchIntentForPackage(applicationPackage) ?: return
+ logger.debug("Starting splash activity with intent {}", intent)
+ startActivity(intent.apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ })
+ finish()
+ }
+
+ private fun clearAppData(context: Context) {
+ clearCache(context)
+ clearFilesDir(context)
+ }
+
+ private fun clearFilesDir(context: Context) {
+ val filesDir = context.filesDir
+ deleteFilesInDirectory(filesDir)
+ }
+
+ private fun clearCache(context: Context) {
+ val cacheDir = context.cacheDir
+ deleteFilesInDirectory(cacheDir)
+ }
+
+ private fun deleteFilesInDirectory(directory: File) {
+ if (directory.isDirectory) {
+ val children = directory.listFiles()
+ if (children != null) {
+ for (child in children) {
+ if (child.absolutePath.toString().contains("firebase")) return
+ deleteFilesInDirectory(child)
+ }
+ }
+ }
+ directory.delete()
+ }
+
companion object {
private val logger = LoggerFactory.getLogger(MainActivity::class.java)
- private const val REQUEST_PERMISSION_TIMEOUT_MS = 86_400_000L // 1 day
+ private val REQUEST_PERMISSION_TIMEOUT = 86_400_000.milliseconds // 1 day
+
+ private var _permissionsBroadcastReceiver: MutableSharedFlow = MutableSharedFlow()
+
+ val LifecycleService.permissionsBroadcastReceiver: SharedFlow
+ get() = _permissionsBroadcastReceiver
}
}
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/MainActivityView.kt b/radar-commons-android/src/main/java/org/radarbase/android/MainActivityView.kt
index a3daf9e38..ae96c4222 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/MainActivityView.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/MainActivityView.kt
@@ -22,4 +22,6 @@ interface MainActivityView {
*/
fun update()
fun onRadarServiceBound(binder: IRadarBinder)
+ fun showLogoutProgress()
+ fun stopLogoutProgress()
}
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/RadarApplication.kt b/radar-commons-android/src/main/java/org/radarbase/android/RadarApplication.kt
index ad747983a..65831293d 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/RadarApplication.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/RadarApplication.kt
@@ -19,7 +19,6 @@ package org.radarbase.android
import android.app.Activity
import android.app.Service
import android.content.Context
-import android.graphics.Bitmap
import android.os.Bundle
import org.radarbase.android.source.SourceService
import org.radarbase.android.util.NotificationHandler
@@ -39,6 +38,8 @@ interface RadarApplication {
val radarService: Class
get() = RadarService::class.java
+ val radarServiceImpl: RadarService
+
fun configureProvider(bundle: Bundle)
fun onSourceServiceInvocation(service: SourceService<*>, bundle: Bundle, isNew: Boolean)
fun onSourceServiceDestroy(service: SourceService<*>)
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/RadarConfiguration.kt b/radar-commons-android/src/main/java/org/radarbase/android/RadarConfiguration.kt
index 92309b63e..8185da9bf 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/RadarConfiguration.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/RadarConfiguration.kt
@@ -18,15 +18,15 @@ package org.radarbase.android
import android.annotation.SuppressLint
import android.content.Context
-import androidx.lifecycle.LiveData
+import kotlinx.coroutines.flow.StateFlow
import org.radarbase.android.auth.AppAuthState
import org.radarbase.android.config.SingleRadarConfiguration
import java.util.*
interface RadarConfiguration {
- val status: RemoteConfigStatus
+ val status: StateFlow
val latestConfig: SingleRadarConfiguration
- val config: LiveData
+ val config: StateFlow
enum class RemoteConfigStatus {
UNAVAILABLE, INITIAL, ERROR, READY, FETCHING, FETCHED, PARTIALLY_FETCHED
@@ -44,29 +44,31 @@ interface RadarConfiguration {
*/
fun put(key: String, value: Any): String?
- fun persistChanges()
+ suspend fun persistChanges()
/**
* Reset configuration to remote config values. If no keys are given, all local
* settings are reset, otherwise only the given keys are reset.
* @param keys configuration names
*/
- fun reset(vararg keys: String)
+ suspend fun reset(vararg keys: String)
/**
* Fetch the remote configuration from server if it is outdated.
*/
- fun fetch()
+ suspend fun fetch()
/**
* Force fetching the remote configuration from server, even if it is not outdated.
*/
- fun forceFetch()
+ suspend fun forceFetch(callAfterLogout: Boolean = false)
/**
* Adds base URL from auth state to configuration.
*/
- fun updateWithAuthState(context: Context, appAuthState: AppAuthState?)
+ suspend fun updateWithAuthState(context: Context, appAuthState: AppAuthState?, isLogoutCall: Boolean = false)
+
+ suspend fun resetConfigs()
companion object {
const val RADAR_CONFIGURATION_CHANGED = "org.radarcns.android.RadarConfiguration.CHANGED"
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt
index ae78175f5..a9968adfb 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt
@@ -34,13 +34,27 @@ import android.os.Build.VERSION_CODES
import android.os.Build.VERSION_CODES.Q
import android.os.Build.VERSION_CODES.S
import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
-import android.os.Process.THREAD_PRIORITY_BACKGROUND
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.annotation.RequiresApi
import androidx.lifecycle.LifecycleService
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import org.apache.avro.specific.SpecificRecord
+import org.radarbase.android.MainActivity.Companion.permissionsBroadcastReceiver
import org.radarbase.android.RadarApplication.Companion.radarApp
import org.radarbase.android.RadarApplication.Companion.radarConfig
import org.radarbase.android.RadarConfiguration.Companion.FETCH_TIMEOUT_MS_DEFAULT
@@ -50,22 +64,22 @@ import org.radarbase.android.config.SingleRadarConfiguration
import org.radarbase.android.data.CacheStore
import org.radarbase.android.data.DataHandler
import org.radarbase.android.data.TableDataHandler
+import org.radarbase.android.kafka.ServerStatus
import org.radarbase.android.kafka.ServerStatusListener
+import org.radarbase.android.kafka.TopicSendReceipt
import org.radarbase.android.source.*
-import org.radarbase.android.source.SourceService.Companion.SERVER_RECORDS_SENT_NUMBER
-import org.radarbase.android.source.SourceService.Companion.SERVER_RECORDS_SENT_TOPIC
-import org.radarbase.android.source.SourceService.Companion.SERVER_STATUS_CHANGED
-import org.radarbase.android.source.SourceService.Companion.SOURCE_CONNECT_FAILED
import org.radarbase.android.util.*
+import org.radarbase.android.util.ManagedServiceConnection.Companion.serviceConnection
import org.radarbase.android.util.NotificationHandler.Companion.NOTIFICATION_CHANNEL_INFO
import org.radarbase.android.util.PermissionHandler.Companion.isPermissionGranted
+import org.radarbase.kotlin.coroutines.launchJoin
import org.radarcns.kafka.ObservationKey
import org.slf4j.LoggerFactory
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
abstract class RadarService : LifecycleService(), ServerStatusListener, LoginListener {
- private var configurationUpdateFuture: SafeHandler.HandlerFuture? = null
+ private var configurationUpdateFuture: Job? = null
private val fetchTimeout = ChangeRunner()
private lateinit var mainHandler: Handler
private var binder: IBinder? = null
@@ -76,7 +90,12 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
open val cacheStore: CacheStore = CacheStore()
- private lateinit var mHandler: SafeHandler
+ private val radarConfigureMutex: Mutex = Mutex()
+ private lateinit var radarExecutor: CoroutineTaskExecutor
+ private var recordTrackerJob: Job? = null
+ private var statusTrackerJob: Job? = null
+ private var failedSourceObserverJob: Job? = null
+
private var needsBluetooth = ChangeRunner(false)
protected lateinit var configuration: RadarConfiguration
@@ -84,10 +103,7 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
private val sourceFilters: MutableMap, Set> = HashMap()
private lateinit var providerLoader: SourceProviderLoader
- private lateinit var authConnection: AuthServiceConnection
- private lateinit var permissionsBroadcastReceiver: BroadcastRegistration
- private lateinit var sourceFailedReceiver: BroadcastRegistration
- private lateinit var serverStatusReceiver: BroadcastRegistration
+ private lateinit var authConnection: ManagedServiceConnection
private var sourceRegistrar: SourceProviderRegistrar? = null
private val configuredProviders = ChangeRunner>>()
@@ -102,10 +118,55 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
/** An overview of how many records have been sent throughout the application. */
private var latestNumberOfRecordsSent = TimedLong(0)
+ private val needsPermissions = LinkedHashSet()
+
+ private var authListenerRegistry: AuthService.LoginListenerRegistry? = null
+ private var authServiceBinder: AuthService.AuthServiceBinder? = null
+
+ private val authConnectionBoundActions: MutableList = mutableListOf(
+ {
+ authListenerRegistry = it.addLoginListener(this)
+ it.refreshIfOnline()
+ }
+ )
+
+ private val startServiceMutex: Mutex = Mutex()
+ private val providerUpdateMutex: Mutex = Mutex()
+ private var configCollectorJob: Job? = null
+
+ private val authConnectionUnboundActions: MutableList = mutableListOf(
+ {
+ radarExecutor.execute {
+ sourceRegistrar?.let {
+ it.stop()
+ sourceRegistrar = null
+ }
+ }
+ },
+ { binder ->
+ authListenerRegistry?.let {
+ binder.removeLoginListener(it)
+ }
+ authListenerRegistry = null
+ }
+ )
+
+ private val _recordsSent: MutableSharedFlow = MutableSharedFlow(
+ replay = 1000,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
/** Current server status. */
- private lateinit var serverStatus: ServerStatusListener.Status
+ private val _serverStatus: MutableStateFlow =
+ MutableStateFlow(ServerStatus.DISCONNECTED)
- private val needsPermissions = LinkedHashSet()
+ override val recordsSent: SharedFlow = _recordsSent.asSharedFlow()
+ override val serverStatus: StateFlow = _serverStatus
+
+ private var _actionBluetoothNeeded: MutableStateFlow = MutableStateFlow(BluetoothNeeded)
+ val actionBluetoothNeeded: StateFlow = _actionBluetoothNeeded
+
+ private var _actionProvidersUpdated: MutableSharedFlow = MutableStateFlow(false)
+ val actionProvidersUpdated: SharedFlow = _actionProvidersUpdated
protected open val servicePermissions: List = buildList(4) {
add(ACCESS_NETWORK_STATE)
@@ -124,8 +185,6 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
return binder
}
- private lateinit var broadcaster: LocalBroadcastManager
-
private var bluetoothNotification: NotificationHandler.NotificationRegistration? = null
@RequiresApi(Q)
@@ -142,57 +201,68 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
/** Defines callbacks for service binding, passed to bindService() */
private lateinit var bluetoothReceiver: BluetoothStateReceiver
+ private val bluetoothRequiredValues: MutableList = mutableListOf()
+
override fun onCreate() {
super.onCreate()
- serverStatus = ServerStatusListener.Status.DISABLED
+ logger.trace("Creating Radar Service")
notificationHandler = NotificationHandler(this)
binder = createBinder()
- mHandler = SafeHandler.getInstance("RadarService", THREAD_PRIORITY_BACKGROUND).apply {
+ radarExecutor = CoroutineTaskExecutor(this::class.simpleName!!, Dispatchers.Default).apply {
start()
}
mainHandler = Handler(Looper.getMainLooper())
configuration = radarConfig
providerLoader = SourceProviderLoader(plugins)
- broadcaster = LocalBroadcastManager.getInstance(this)
+ lifecycleScope.launch {
+ permissionsBroadcastReceiver.collect { permissions: PermissionBroadcast? ->
+ permissions ?: return@collect
+ logger.trace(
+ "NewBroadcastTrace: onPermissionUpdated: permissions: {}, grants: {}",
+ permissions.extraPermissions,
+ permissions.extraGrants
+ )
+ val extraPermissions = permissions.extraPermissions
+ val extraGrants = permissions.extraGrants
- broadcaster.run {
- permissionsBroadcastReceiver = register(ACTION_PERMISSIONS_GRANTED) { _, intent ->
- val extraPermissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS) ?: return@register
- val extraGrants = intent.getIntArrayExtra(EXTRA_GRANT_RESULTS) ?: return@register
onPermissionsGranted(extraPermissions, extraGrants)
}
- sourceFailedReceiver = register(SOURCE_CONNECT_FAILED) { context, intent ->
- Boast.makeText(context,
- getString(R.string.cannot_connect_device,
- intent.getStringExtra(SourceService.SOURCE_STATUS_NAME)),
- Toast.LENGTH_SHORT).show()
+ }
+
+ authConnection = serviceConnection(radarApp.authService)
+
+ with(lifecycleScope) {
+ launch {
+ authConnection.bind()
}
- serverStatusReceiver = register(SERVER_STATUS_CHANGED) { _, intent ->
- val serverStatusChanged = intent.getIntExtra(SERVER_STATUS_CHANGED, 0)
- serverStatus = ServerStatusListener.Status.values()[serverStatusChanged]
- if (serverStatus == ServerStatusListener.Status.UNAUTHORIZED) {
- logger.debug("Status unauthorized")
- authConnection.applyBinder {
- if (isMakingAuthRequest.compareAndSet(false, true)) {
- invalidate(null, false)
- refresh()
+ launch {
+ authConnection.state
+ .collect { bindState: BindState ->
+ when (bindState) {
+ is ManagedServiceConnection.BoundService -> {
+ authServiceBinder = bindState.binder.also { bound ->
+ authConnectionBoundActions.launchJoin {
+ it(bound)
+ }
+ }
+ }
+
+ is ManagedServiceConnection.Unbound -> {
+ authServiceBinder?.also { unbound ->
+ authConnectionUnboundActions.launchJoin {
+ it(unbound)
+ }
+ }
+ authServiceBinder = null
+ }
}
}
- }
}
- }
-
- configuration.config.observe(this, ::configure)
-
- authConnection = AuthServiceConnection(this, this).apply {
- bind()
- }
- authConnection.onUnboundListeners += {
- mHandler.execute {
- sourceRegistrar?.let {
- it.close()
- sourceRegistrar = null
+ configCollectorJob = launch {
+ configuration.config.collect {
+ logger.trace("New Configuration received")
+ doConfigure(it)
}
}
}
@@ -203,18 +273,20 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
bluetoothNotification?.cancel()
startScanning()
} else {
- mConnections.asSequence()
- .map { it.connection }
- .filter { it.needsBluetooth() }
- .forEach { it.stopRecording() }
-
- bluetoothNotification = notificationHandler.notify(
- BLUETOOTH_NOTIFICATION,
- NotificationHandler.NOTIFICATION_CHANNEL_ALERT,
- false
- ) {
- setContentTitle(getString(R.string.notification_bluetooth_needed_title))
- setContentText(getString(R.string.notification_bluetooth_needed_text))
+ lifecycleScope.launch {
+ mConnections.asSequence()
+ .map { it.connection }
+ .filter { it.needsBluetooth() }
+ .forEach { it.stopRecording() }
+
+ bluetoothNotification = notificationHandler.notify(
+ BLUETOOTH_NOTIFICATION,
+ NotificationHandler.NOTIFICATION_CHANNEL_ALERT,
+ false
+ ) {
+ setContentTitle(getString(R.string.notification_bluetooth_needed_title))
+ setContentText(getString(R.string.notification_bluetooth_needed_text))
+ }
}
}
}
@@ -226,6 +298,7 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
+ logger.trace("OnStartCommand Radar Service")
configure(configuration.latestConfig)
checkPermissions()
@@ -305,57 +378,104 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
}
override fun onDestroy() {
+ configCollectorJob?.cancel()
+ authConnection.unbind()
if (needsBluetooth.value) {
bluetoothReceiver.unregister()
}
- permissionsBroadcastReceiver.unregister()
- sourceFailedReceiver.unregister()
- serverStatusReceiver.unregister()
-
- mHandler.stop {
- sourceRegistrar?.let {
- it.close()
- sourceRegistrar = null
- }
+ configurationUpdateFuture?.cancel()
+ detachAllSources()
+ radarExecutor.stop()
+ sourceRegistrar?.let {
+ it.stop()
+ sourceRegistrar = null
}
- authConnection.unbind()
+ lifecycleScope.launch(Dispatchers.Default) {
+ (dataHandler as TableDataHandler).stop()
+ dataHandler = null
+ }
+ logger.debug("Destroying RadarService")
+ recordTrackerJob?.cancel()
+ statusTrackerJob?.cancel()
+ super.onDestroy()
+ }
+ private fun detachAllSources() {
mConnections.asSequence()
.filter(SourceProvider<*>::isBound)
- .forEach(SourceProvider<*>::unbind)
-
- super.onDestroy()
+ .forEach {
+ it.unbind()
+ }
+ mConnections.forEach {
+ it.stopService()
+ }
}
@CallSuper
protected open fun configure(config: SingleRadarConfiguration) {
- mHandler.executeReentrant {
- doConfigure(config)
-
- fetchTimeout.applyIfChanged(config.getLong(FETCH_TIMEOUT_MS_KEY, FETCH_TIMEOUT_MS_DEFAULT)) { timeout ->
- configurationUpdateFuture?.cancel()
- configurationUpdateFuture = mHandler.repeat(timeout) {
- configuration.fetch()
- }
- }
- }
+ radarExecutor.executeReentrant {
+ doConfigure(config)
+
+ fetchTimeout.applyIfChanged(config.getLong(FETCH_TIMEOUT_MS_KEY, FETCH_TIMEOUT_MS_DEFAULT)) { timeout ->
+ configurationUpdateFuture?.cancel()
+ configurationUpdateFuture = lifecycleScope.launch(Dispatchers.Default) {
+ while (isActive){
+ kotlinx.coroutines.delay(timeout)
+ configuration.fetch()
+ }
+ }
+ }
+ }
}
@CallSuper
- protected open fun doConfigure(config: SingleRadarConfiguration) {
- synchronized(this) {
+ protected open suspend fun doConfigure(config: SingleRadarConfiguration) {
+ val prevDataHandler = dataHandler
+ radarConfigureMutex.withLock {
dataHandler ?: TableDataHandler(this, cacheStore)
- .also {
- dataHandler = it
- it.statusListener = this
- }
+ .also {
+ dataHandler = it
+ }
}.handler {
configure(config)
}
+ if (prevDataHandler != dataHandler) {
+ with(lifecycleScope) {
+ recordTrackerJob?.cancel()
+ recordTrackerJob = launch {
+ dataHandler?.let { handler ->
+ handler.recordsSent
+ .collect {
+ this@RadarService._recordsSent.emit(it)
+ }
+ }
+ }
+
+ statusTrackerJob?.cancel()
+ statusTrackerJob = launch {
+ dataHandler?.let { handler ->
+ handler.serverStatus
+ .collectLatest {
+ _serverStatus.value = it
+ if (it == ServerStatus.UNAUTHORIZED) {
+ logger.debug("Status unauthorized")
+ authConnection.applyBinder {
+ if (isMakingAuthRequest.compareAndSet(false, true)) {
+ invalidate(null, false)
+ refresh()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
authConnection.applyBinder {
applyState {
- mHandler.execute {
+ radarExecutor.execute {
updateProviders(this, config)
}
}
@@ -369,8 +489,8 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
}
private fun requestPermissions(permissions: Collection) {
- mainHandler.post {
- startActivity(Intent(this, radarApp.mainActivity).apply {
+ lifecycleScope.launch(Dispatchers.Main.immediate) {
+ startActivity(Intent(this@RadarService, radarApp.mainActivity).apply {
action = ACTION_CHECK_PERMISSIONS
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -379,43 +499,44 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
}
}
- private fun onPermissionsGranted(permissions: Array, grantResults: IntArray) {
- val grantedPermissions = buildSet {
- grantResults.indices.forEach { index ->
- if (grantResults[index] == PERMISSION_GRANTED) {
- add(permissions[index])
- } else {
- logger.info("Denied permission {}", permissions[index])
+ private suspend fun onPermissionsGranted(permissions: Array, grantResults: IntArray) {
+ withContext(Dispatchers.Default) {
+ val grantedPermissions = buildSet {
+ grantResults.indices.forEach { index ->
+ if (grantResults[index] == PERMISSION_GRANTED) {
+ add(permissions[index])
+ } else {
+ logger.info("Denied permission {}", permissions[index])
+ }
}
}
- }
- if (grantedPermissions.isNotEmpty()) {
- startForegroundIfNeeded(grantedPermissions)
- mHandler.execute {
- logger.info("Granted permissions {}", grantedPermissions)
- // Permission granted.
- needsPermissions -= grantedPermissions
- startScanning()
+ if (grantedPermissions.isNotEmpty()) {
+ startForegroundIfNeeded(grantedPermissions)
+ radarExecutor.execute {
+ logger.info("Granted permissions {}", grantedPermissions)
+ // Permission granted.
+ needsPermissions -= grantedPermissions
+ startScanning()
+ }
+ checkPermissions()
}
- checkPermissions()
}
}
fun serviceConnected(connection: SourceServiceConnection<*>) {
- mHandler.execute {
+ radarExecutor.execute {
+ val connectionProvider = getConnectionProvider(connection)
if (!isScanningEnabled) {
- getConnectionProvider(connection)?.also { provider ->
+ connectionProvider?.also { provider ->
if (!provider.mayBeConnectedInBackground) {
provider.unbind()
}
}
}
- connection.serverStatus
- ?.also { logger.debug("Initial server status: {}", it) }
- ?.also(::updateServerStatus)
-
- updateBluetoothNeeded(needsBluetooth.value || connection.needsBluetooth())
+ val providerRequiresBluetooth = connectionProvider?.doesRequireBluetooth() ?: false
+ bluetoothRequiredValues.add(providerRequiresBluetooth)
+ updateBluetoothNeeded(needsBluetooth.value || bluetoothRequiredValues.any { it })
startScanning()
}
}
@@ -424,22 +545,16 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
needsBluetooth.applyIfChanged(newValue) {
if (newValue) {
bluetoothReceiver.register()
-
- broadcaster.send(ACTION_BLUETOOTH_NEEDED_CHANGED) {
- putExtra(ACTION_BLUETOOTH_NEEDED_CHANGED, BLUETOOTH_NEEDED)
- }
+ _actionBluetoothNeeded.value = BluetoothNeeded
} else {
bluetoothReceiver.unregister()
-
- broadcaster.send(ACTION_BLUETOOTH_NEEDED_CHANGED) {
- putExtra(ACTION_BLUETOOTH_NEEDED_CHANGED, BLUETOOTH_NOT_NEEDED)
- }
+ _actionBluetoothNeeded.value = BluetoothNotNeeded
}
}
}
fun serviceDisconnected(connection: SourceServiceConnection<*>) {
- mHandler.execute {
+ radarExecutor.execute {
getConnectionProvider(connection)?.also { provider ->
bindServices(listOf(provider), true)
}
@@ -447,10 +562,9 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
}
private fun bindServices(providers: Collection>, unbindFirst: Boolean) {
- mHandler.executeReentrant {
- val authBinder = authConnection.binder
- if (authBinder == null) {
- mHandler.delay(1000) { bindServices(providers, unbindFirst) }
+ radarExecutor.executeReentrant {
+ if (authServiceBinder == null) {
+ radarExecutor.delay(1000) { bindServices(providers, unbindFirst) }
} else {
if (unbindFirst) {
providers.asSequence()
@@ -467,30 +581,6 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
}
}
- override fun updateServerStatus(status: ServerStatusListener.Status) {
- if (status == this.serverStatus) {
- return
- }
- this.serverStatus = status
- if (status == ServerStatusListener.Status.DISCONNECTED) {
- this.latestNumberOfRecordsSent = TimedLong(-1)
- }
-
- broadcaster.send(SERVER_STATUS_CHANGED) {
- putExtra(SERVER_STATUS_CHANGED, status.ordinal)
- }
- }
-
- override fun updateRecordsSent(topicName: String, numberOfRecords: Long) {
- this.latestNumberOfRecordsSent = TimedLong(numberOfRecords)
-
- broadcaster.send(SERVER_RECORDS_SENT_TOPIC) {
- // Signal that a certain topic changed, the key of the map retrieved by getRecordsSent().
- putExtra(SERVER_RECORDS_SENT_TOPIC, topicName)
- putExtra(SERVER_RECORDS_SENT_NUMBER, numberOfRecords)
- }
- }
-
fun sourceStatusUpdated(connection: SourceServiceConnection<*>, status: SourceStatusListener.Status) {
logger.info("Source of {} was updated to {}", connection, status)
if (status == SourceStatusListener.Status.CONNECTED) {
@@ -500,12 +590,12 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
startScanning()
}
if (showSourceStatus) {
- mainHandler.post {
+ lifecycleScope.launch(Dispatchers.Main.immediate) {
val showRes = when (status) {
SourceStatusListener.Status.READY -> R.string.device_ready
SourceStatusListener.Status.CONNECTED -> R.string.device_connected
SourceStatusListener.Status.CONNECTING -> R.string.device_connecting
- SourceStatusListener.Status.DISCONNECTING -> return@post // do not show toast
+ SourceStatusListener.Status.DISCONNECTING -> return@launch // do not show toast
SourceStatusListener.Status.DISCONNECTED -> R.string.device_disconnected
SourceStatusListener.Status.UNAVAILABLE -> R.string.device_unavailable
}
@@ -519,24 +609,27 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
}
protected fun startScanning() {
- mHandler.executeReentrant {
- mConnections
- .asSequence()
- .filter { it.isBound &&
- it.connection.hasService() &&
- !it.connection.isRecording &&
- it.checkPermissions()
- }
- .forEach { provider ->
- val connection = provider.connection
- logger.info("Starting recording on connection {}", connection)
- connection.startRecording(sourceFilters[connection] ?: emptySet())
- }
+ radarExecutor.executeReentrant {
+ startServiceMutex.withLock {
+ mConnections
+ .asSequence()
+ .filter {
+ it.isBound &&
+ it.connection.hasService() &&
+ !it.connection.isRecording &&
+ it.checkPermissions()
+ }
+ .forEach { provider ->
+ val connection = provider.connection
+ logger.info("Starting recording on connection {}", connection)
+ connection.startRecording(sourceFilters[connection] ?: emptySet())
+ }
+ }
}
}
protected fun checkPermissions() {
- mHandler.executeReentrant {
+ radarExecutor.executeReentrant {
val permissionsRequired = buildSet {
addAll(servicePermissions)
mConnections.forEach { addAll(it.permissionsNeeded) }
@@ -598,19 +691,37 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) {
isMakingAuthRequest.set(false)
- mHandler.execute {
- updateProviders(authState, configuration.latestConfig)
+ radarExecutor.execute {
+ providerUpdateMutex.withLock {
+ updateProviders(authState, configuration.latestConfig)
+ }
}
}
+ override suspend fun logoutSucceeded(manager: LoginManager?, authState: AppAuthState) {
+ updateProviders(authState, configuration.latestConfig)
+ val oldProviders = mConnections
+ removeProviders(mConnections.toSet())
+ oldProviders.forEach {
+ it.stopService()
+ }
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+
private fun removeProviders(sourceProviders: Set>) {
if (sourceProviders.isEmpty()) {
return
}
logger.info("Removing plugins {}", sourceProviders.map { it.pluginName })
mConnections = mConnections - sourceProviders
+ sourceProviders.forEach {
+ bluetoothRequiredValues -= it.doesRequireBluetooth()
+ }
updateBluetoothNeeded(mConnections.any { it.isConnected && it.connection.needsBluetooth() })
- sourceProviders.forEach(SourceProvider<*>::unbind)
+ sourceProviders.forEach {
+ it.unbind()
+ }
}
private fun addProviders(sourceProviders: List>) {
@@ -632,7 +743,7 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
logger.info("Creating source registration")
sourceRegistrar = SourceProviderRegistrar(
authServiceBinder,
- mHandler,
+ this,
providers
) { unregisteredProviders, registeredProviders ->
logger.info(
@@ -644,16 +755,16 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
removeProviders(unregisteredProviders.intersect(oldConnections))
addProviders(registeredProviders - oldConnections)
if (mConnections.toSet() != oldConnections) {
- broadcaster.send(ACTION_PROVIDERS_UPDATED)
+ _actionProvidersUpdated.emit(true)
}
}
}
- private fun updateProviders(authState: AppAuthState, config: SingleRadarConfiguration) {
+ private suspend fun updateProviders(authState: AppAuthState, config: SingleRadarConfiguration) {
dataHandler?.handler {
logger.info("Setting data submission authentication")
rest {
- headers = authState.okHttpHeaders
+ headers = authState.ktorHeaders
}
submitter {
userId = authState.userId
@@ -666,30 +777,58 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
.filter { hasFeatures(it, packageManager) }
configuredProviders.applyIfChanged(supportedPlugins) { providers ->
- val previousConnections = mConnections
- removeProviders(mConnections.filterNotTo(HashSet(), providers::contains))
+ lifecycleScope.launch(Dispatchers.Default) {
+ val previousConnections = mConnections
+ removeProviders(mConnections.filterNotTo(HashSet(), providers::contains))
- sourceRegistrar?.let {
- it.close()
- sourceRegistrar = null
- }
- authConnection.applyBinder { createRegistrar(this, providers) }
+ sourceRegistrar?.let {
+ it.stop()
+ sourceRegistrar = null
+ }
- if (mConnections != previousConnections) {
- broadcaster.send(ACTION_PROVIDERS_UPDATED)
+ authConnection.applyBinder {
+ createRegistrar(this, providers)
+ }
+
+ if (failedSourceObserverJob == null || mConnections != previousConnections) {
+ failedSourceObserverJob?.cancel()
+
+ failedSourceObserverJob = lifecycleScope.launch {
+ mConnections.forEach {
+ it.connection.sourceConnectFailed
+ ?.collectLatest { info ->
+ Boast.makeText(
+ this@RadarService,
+ info.sourceName,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ _actionProvidersUpdated.emit(true)
+ }
}
}
}
+ fun sourceFailedToConnect(sourceName: String, serviceClass: Class>) {
+ logger.info("Source {} of service class {} failed to connect", sourceName, serviceClass)
+ if (isScanningEnabled) {
+ Boast.makeText(this@RadarService,
+ getString(R.string.cannot_connect_device, sourceName),
+ Toast.LENGTH_SHORT).show()
+ }
+ }
+
override fun loginFailed(manager: LoginManager?, ex: Exception?) {
isMakingAuthRequest.set(false)
}
protected inner class RadarBinder : Binder(), IRadarBinder {
- override fun startScanning() = this@RadarService.startActiveScanning()
- override fun stopScanning() = this@RadarService.stopActiveScanning()
+ override suspend fun startScanning() = this@RadarService.startActiveScanning()
+ override suspend fun stopScanning() = this@RadarService.stopActiveScanning()
- override val serverStatus: ServerStatusListener.Status
+ override val serverStatus: StateFlow
get() = this@RadarService.serverStatus
override val latestNumberOfRecordsSent: TimedLong
@@ -701,18 +840,20 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
override fun setAllowedSourceIds(connection: SourceServiceConnection<*>, allowedIds: Collection) {
sourceFilters[connection] = allowedIds.sanitizeIds()
- mHandler.execute {
+ radarExecutor.execute {
val status = connection.sourceStatus
- if (
- status == SourceStatusListener.Status.READY ||
- status == SourceStatusListener.Status.CONNECTING ||
- (status == SourceStatusListener.Status.CONNECTED &&
- !connection.isAllowedSource(allowedIds))
- ) {
- if (connection.isRecording) {
- connection.stopRecording()
- // will restart recording once the status is set to disconnected.
+ status?.let {
+ if (
+ it.value == SourceStatusListener.Status.READY ||
+ it.value == SourceStatusListener.Status.CONNECTING ||
+ (it.value == SourceStatusListener.Status.CONNECTED &&
+ !connection.isAllowedSource(allowedIds))
+ ) {
+ if (connection.isRecording) {
+ connection.stopRecording()
+ // will restart recording once the status is set to disconnected.
+ }
}
}
}
@@ -722,19 +863,27 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
get() = this@RadarService.dataHandler
override fun needsBluetooth(): Boolean = needsBluetooth.value
+
+// Added by Joris. Not able to find the exact usage of it now, but will figure it out soon.
+ override fun flushCaches(successCallback: () -> Unit, errorCallback: () -> Unit) = Unit
+
+ // A similar function is already happening via flows, keeping it for any future use
+ override fun permissionGranted(permissions: Array, grantResults: IntArray) = Unit
}
private fun stopActiveScanning() {
- mHandler.execute {
+ radarExecutor.execute {
isScanningEnabled = false
mConnections.asSequence()
.filter { it.isConnected && it.connection.mayBeDisabledInBackground() }
- .forEach(SourceProvider<*>::unbind)
+ .forEach {
+ it.unbind()
+ }
}
}
private fun startActiveScanning() {
- mHandler.execute {
+ radarExecutor.execute {
isScanningEnabled = true
bindServices(mConnections, false)
}
@@ -746,23 +895,16 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis
const val ACTION_PROVIDERS_UPDATED = "$RADAR_PACKAGE.ACTION_PROVIDERS_UPDATED"
- const val ACTION_BLUETOOTH_NEEDED_CHANGED = "$RADAR_PACKAGE.BLUETOOTH_NEEDED_CHANGED"
- const val BLUETOOTH_NEEDED = 1
- const val BLUETOOTH_NOT_NEEDED = 2
-
const val ACTION_CHECK_PERMISSIONS = "$RADAR_PACKAGE.ACTION_CHECK_PERMISSIONS"
const val EXTRA_PERMISSIONS = "$RADAR_PACKAGE.EXTRA_PERMISSIONS"
- const val ACTION_PERMISSIONS_GRANTED = "$RADAR_PACKAGE.ACTION_PERMISSIONS_GRANTED"
- const val EXTRA_GRANT_RESULTS = "$RADAR_PACKAGE.EXTRA_GRANT_RESULTS"
-
private const val BLUETOOTH_NOTIFICATION = 521290
- val ACCESS_BACKGROUND_LOCATION_COMPAT = if (SDK_INT >= VERSION_CODES.Q)
+ val ACCESS_BACKGROUND_LOCATION_COMPAT = if (SDK_INT >= Q)
ACCESS_BACKGROUND_LOCATION else "android.permission.ACCESS_BACKGROUND_LOCATION"
private const val BACKGROUND_REQUEST_CODE = 9559
fun Collection.sanitizeIds(): Set = HashSet(mapNotNull(String::takeTrimmedIfNotEmpty))
}
-}
+}
\ No newline at end of file
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/auth/AppAuthState.kt b/radar-commons-android/src/main/java/org/radarbase/android/auth/AppAuthState.kt
index 937306c31..66e76d619 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/auth/AppAuthState.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/auth/AppAuthState.kt
@@ -18,23 +18,24 @@ package org.radarbase.android.auth
import android.os.SystemClock
import androidx.annotation.Keep
-import okhttp3.Headers
-import org.json.JSONArray
-import org.json.JSONException
+import io.ktor.http.Headers
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
import org.radarbase.android.auth.LoginManager.Companion.AUTH_TYPE_UNKNOWN
-import org.radarbase.android.auth.portal.ManagementPortalClient.Companion.SOURCES_PROPERTY
import org.radarbase.android.auth.portal.ManagementPortalClient.Companion.SOURCE_IDS_PROPERTY
-import org.radarcns.android.auth.AppSource
+import org.radarbase.android.util.buildJsonArray
+import org.radarbase.android.util.equalTo
import org.slf4j.LoggerFactory
-import java.io.Serializable
-import java.util.*
+import java.util.Objects
import java.util.concurrent.TimeUnit
-import kotlin.collections.HashMap
/** Authentication state of the application. */
@Keep
@Suppress("unused")
-class AppAuthState private constructor(builder: Builder) {
+class AppAuthState(builder: Builder) {
+ val lastUpdate: Long = builder.lastUpdate
+
val projectId: String? = builder.projectId
val userId: String? = builder.userId
val token: String? = builder.token
@@ -42,18 +43,15 @@ class AppAuthState private constructor(builder: Builder) {
val authenticationSource: String? = builder.authenticationSource
val needsRegisteredSources: Boolean = builder.needsRegisteredSources
val expiration: Long = builder.expiration
- val lastUpdate: Long = builder.lastUpdate
val attributes: Map = HashMap(builder.attributes)
- val headers: List> = ArrayList(builder.headers)
- val sourceMetadata: List = ArrayList(builder.sourceMetadata)
- val sourceTypes: List = ArrayList(builder.sourceTypes)
+ val headers: List> = ArrayList(builder.headers)
+ val sourceMetadata: List
+ val sourceTypes: List
val isPrivacyPolicyAccepted: Boolean = builder.isPrivacyPolicyAccepted
- val okHttpHeaders: Headers = Headers.Builder().apply {
- for (header in headers) {
- add(header.key, header.value)
- }
- }.build()
- val baseUrl: String? = attributes[AuthService.BASE_URL_PROPERTY]?.stripEndSlashes()
+ val ktorHeaders = Headers.build {
+ headers.forEach { (k, v) -> append(k, v) }
+ }
+ val baseUrl: String? = attributes[AuthService.BASE_URL_PROPERTY]?.trimEndSlash()
val isValid: Boolean
get() = isPrivacyPolicyAccepted && expiration > System.currentTimeMillis()
@@ -65,132 +63,141 @@ class AppAuthState private constructor(builder: Builder) {
constructor(initializer: Builder.() -> Unit) : this(Builder().also(initializer))
+ init {
+ sourceTypes = buildList {
+ addAll(builder.sourceTypes)
+ sourceMetadata = builder.sourceMetadata.map { it.deduplicateType(this) }
+ }
+ }
+
fun getAttribute(key: String) = attributes[key]
- fun serializableAttributeList() = serializedMap(attributes.entries)
+ fun serializableAttributeList() = buildJsonArray {
+ attributes.forEach { (k, v) ->
+ put(k)
+ put(v)
+ }
+ }.toString()
- fun serializableHeaderList() = serializedMap(headers)
+ fun serializableHeaderList() = buildJsonArray {
+ headers.forEach { (k, v) ->
+ put(k)
+ put(v)
+ }
+ }.toString()
fun isValidFor(time: Long, unit: TimeUnit) = isPrivacyPolicyAccepted
&& expiration - unit.toMillis(time) > System.currentTimeMillis()
fun isAuthorizedForSource(sourceId: String?): Boolean {
- return !this.needsRegisteredSources
- || (sourceId != null && attributes[SOURCE_IDS_PROPERTY]?.let { sourceId in it } == true)
+ if (!needsRegisteredSources) return true
+ sourceId ?: return false
+ val registeredSourceIds = attributes[SOURCE_IDS_PROPERTY] ?: return false
+ return sourceId in registeredSourceIds
}
val timeSinceLastUpdate: Long
- get() = SystemClock.elapsedRealtime() - lastUpdate
-
- fun alter(changes: Builder.() -> Unit): AppAuthState {
- return Builder().also {
- it.projectId = projectId
- it.userId = userId
- it.token = token
- it.tokenType = tokenType
- it.expiration = expiration
- it.authenticationSource = authenticationSource
- it.isPrivacyPolicyAccepted = isPrivacyPolicyAccepted
-
- it.attributes += attributes
- it.sourceMetadata += sourceMetadata
- it.sourceTypes += sourceTypes
- it.headers += headers
- }.apply(changes).build()
+ get() = SystemClock.elapsedRealtime() - lastUpdate
+
+ fun reset(): AppAuthState {
+ return Builder().build()
}
- class Builder {
+ suspend fun alter(changes: suspend Builder.() -> Unit): AppAuthState {
+ return Builder(this).apply {
+ changes()
+ }.build()
+ }
+
+ class Builder(val original: AppAuthState? = null) {
val lastUpdate = SystemClock.elapsedRealtime()
- val headers: MutableCollection> = mutableListOf()
- val sourceMetadata: MutableCollection = mutableListOf()
- val attributes: MutableMap = mutableMapOf()
+ val headers: MutableCollection> = mutableListOf()
+ val sourceMetadata: MutableCollection = mutableListOf()
+ val attributes: MutableMap = mutableMapOf()
var needsRegisteredSources = true
- var projectId: String? = null
- var userId: String? = null
- var token: String? = null
- var authenticationSource: String? = null
- var tokenType = AUTH_TYPE_UNKNOWN
- var expiration: Long = 0
- var isPrivacyPolicyAccepted = false
- val sourceTypes: MutableCollection = mutableListOf()
-
- @Deprecated("Use safe attributes instead of properties", replaceWith = ReplaceWith("attributes.addAll(properties)"))
- fun properties(properties: Map?): Builder {
- if (properties != null) {
- for ((key, value) in properties) {
- @Suppress("UNCHECKED_CAST", "deprecation")
- when {
- key == SOURCES_PROPERTY -> appSources(value as List)
- value is String -> this.attributes[key] = value
- else -> logger.warn("Property {} no longer mapped in AppAuthState. Value discarded: {}", key, value)
- }
- }
+ var projectId: String? = original?.projectId
+ var userId: String? = original?.userId
+ var token: String? = original?.token
+ var authenticationSource: String? = original?.authenticationSource
+ var tokenType: Int = original?.tokenType ?: AUTH_TYPE_UNKNOWN
+ var expiration: Long = original?.expiration ?: 0
+ var isPrivacyPolicyAccepted: Boolean = original?.isPrivacyPolicyAccepted ?: false
+ val sourceTypes: MutableCollection = mutableListOf()
+
+ init {
+ original?.let { authState ->
+ headers += authState.headers
+ sourceTypes += authState.sourceTypes
+ sourceMetadata += authState.sourceMetadata
+ attributes += authState.attributes
}
- return this
}
- fun parseAttributes(jsonString: String?): Builder = apply {
- jsonString?.also {
- attributes += try {
- deserializedMap(it)
- } catch (e: JSONException) {
- logger.warn("Cannot deserialize AppAuthState attributes: {}", e.toString())
- emptyMap()
- }
- }
+ fun clear() {
+ headers.clear()
+ sourceMetadata.clear()
+ sourceTypes.clear()
+ attributes.clear()
+
+ needsRegisteredSources = true
+ projectId = null
+ userId = null
+ token = null
+ tokenType = AUTH_TYPE_UNKNOWN
+ authenticationSource = null
+ expiration = 0
+ isPrivacyPolicyAccepted = false
}
- fun invalidate(): Builder = apply { expiration = 0L }
-
- fun setHeader(name: String, value: String): Builder = apply {
- headers -= headers.filter { it.key == name }
- addHeader(name, value)
+ fun parseAttributes(jsonString: String?) {
+ jsonString ?: return
+ val attributeList = Json.parseToJsonElement(jsonString)
+ .jsonArray
+ .map { it.jsonPrimitive.content }
+ attributeList.asSequence()
+ .zipWithNext()
+ .filterIndexed { index, _ -> index % 2 == 0 }
+ .toMap(attributes)
}
- fun addHeader(name: String, value: String): Builder = apply {
- headers += AbstractMap.SimpleImmutableEntry(name, value)
+ fun invalidate() {
+ expiration = 0L
}
- fun parseHeaders(jsonString: String?): Builder = apply {
- jsonString?.also {
- this.headers += try {
- deserializedEntryList(it)
- } catch (e: JSONException) {
- logger.warn("Cannot deserialize AppAuthState attributes: {}", e.toString())
- emptyList>()
- }
- }
+ fun setHeader(name: String, value: String) {
+ headers.removeAll { (k, _) -> k == name }
+ addHeader(name, value)
}
- @Deprecated("Use safe sourceMetadata instead of appSources", replaceWith = ReplaceWith("sourceMetadata.addAll(appSources)"))
- @Suppress("deprecation")
- private fun appSources(appSources: List?): Builder = apply {
- appSources?.also { sources ->
- sourceMetadata += sources.map { SourceMetadata(it) }
- }
+ fun addHeader(name: String, value: String) {
+ headers += name to value
}
- @Throws(JSONException::class)
- fun parseSourceTypes(sourceJson: Collection?): Builder = apply {
- sourceJson?.also { types ->
- sourceTypes += types.map { SourceType(it) }
- }
+ fun parseHeaders(jsonString: String?) {
+ jsonString ?: return
+ val headerList = Json.parseToJsonElement(jsonString)
+ .jsonArray
+ .map { it.jsonPrimitive.content }
+
+ headerList.asSequence()
+ .zipWithNext()
+ .filterIndexedTo(this.headers) { i, _ -> i % 2 == 0 }
}
- @Throws(JSONException::class)
- fun parseSourceMetadata(sourceJson: Collection?): Builder = apply {
- sourceJson?.also { sources ->
- sourceMetadata += sources.map { SourceMetadata(it) }
- }
+ fun parseSourceTypes(sourceJson: Collection?) {
+ sourceJson ?: return
+ sourceJson.mapTo(sourceTypes) { Json.decodeFromString(it) }
}
- fun build(): AppAuthState {
- sourceMetadata.forEach { it.deduplicateType(sourceTypes) }
- return AppAuthState(this)
+ fun parseSourceMetadata(sourceJson: Collection?) {
+ sourceJson ?: return
+ sourceJson.mapTo(sourceMetadata) { Json.decodeFromString(it) }
}
+
+ fun build(): AppAuthState = AppAuthState(this)
}
override fun toString(): String {
@@ -212,80 +219,39 @@ class AppAuthState private constructor(builder: Builder) {
""".trimIndent()
}
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as AppAuthState
-
- return projectId == other.projectId
- && userId == other.userId
- && token == other.token
- && tokenType == other.tokenType
- && authenticationSource == other.authenticationSource
- && needsRegisteredSources == other.needsRegisteredSources
- && expiration == other.expiration
- && attributes == other.attributes
- && headers == other.headers
- && sourceMetadata == other.sourceMetadata
- && sourceTypes == other.sourceTypes
- && isPrivacyPolicyAccepted == other.isPrivacyPolicyAccepted
-
- }
+ override fun equals(other: Any?) = equalTo(
+ other,
+ AppAuthState::projectId,
+ AppAuthState::userId,
+ AppAuthState::token,
+ AppAuthState::tokenType,
+ AppAuthState::authenticationSource,
+ AppAuthState::needsRegisteredSources,
+ AppAuthState::expiration,
+ AppAuthState::attributes,
+ AppAuthState::headers,
+ AppAuthState::sourceMetadata,
+ AppAuthState::sourceTypes,
+ AppAuthState::isPrivacyPolicyAccepted,
+ )
override fun hashCode(): Int = Objects.hash(projectId, userId, token)
companion object {
private val logger = LoggerFactory.getLogger(AppAuthState::class.java)
- private fun serializedMap(map: Collection>): String {
- val array = JSONArray()
- for (entry in map) {
- array.put(entry.key)
- array.put(entry.value)
- }
- return array.toString()
- }
-
- @Throws(JSONException::class)
- private fun deserializedMap(jsonString: String): Map {
- val array = JSONArray(jsonString)
- val map = HashMap(array.length() * 4 / 6 + 1)
- var i = 0
- while (i < array.length()) {
- map[array.getString(i)] = array.getString(i + 1)
- i += 2
- }
- return map
- }
-
- @Throws(JSONException::class)
- private fun deserializedEntryList(jsonString: String): List> {
- val array = JSONArray(jsonString)
- val list = ArrayList>(array.length() / 2)
- var i = 0
- while (i < array.length()) {
- list += AbstractMap.SimpleImmutableEntry(array.getString(i), array.getString(i + 1))
- i += 2
- }
- return list
- }
-
/**
* Strips all slashes from the end of a URL.
- * @param url string to strip
+ * @receiver string to strip
* @return stripped URL or null if that would result in an empty or null string.
*/
- private fun String.stripEndSlashes(): String? {
- var lastIndex = length - 1
- while (lastIndex >= 0 && this[lastIndex] == '/') {
- lastIndex--
- }
- if (lastIndex == -1) {
+ private fun String.trimEndSlash(): String? {
+ val result = trimEnd('/')
+ if (result.isEmpty()) {
logger.warn("Base URL '{}' should be a valid URL.", this)
return null
}
- return substring(0, lastIndex + 1)
+ return result
}
}
}
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthSerialization.kt b/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthSerialization.kt
index b3b04eb46..c06357ae7 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthSerialization.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthSerialization.kt
@@ -6,13 +6,13 @@ interface AuthSerialization {
* Load the state from this serialization.
* @return state if it was stored, null otherwise.
*/
- fun load(): AppAuthState?
+ suspend fun load(): AppAuthState?
/**
* Store the auth state to this serialization.
*/
- fun store(state: AppAuthState)
+ suspend fun store(state: AppAuthState)
/**
* Remove the auth state from this serialization.
*/
- fun remove()
+ suspend fun remove()
}
diff --git a/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthService.kt b/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthService.kt
index 1a4467d7f..9cc9ae3ff 100644
--- a/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthService.kt
+++ b/radar-commons-android/src/main/java/org/radarbase/android/auth/AuthService.kt
@@ -1,81 +1,209 @@
package org.radarbase.android.auth
-import android.app.Service
import android.content.Intent
-import android.content.Intent.*
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
+import android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME
import android.os.Binder
import android.os.IBinder
-import android.os.Process.THREAD_PRIORITY_BACKGROUND
import androidx.annotation.Keep
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import org.radarbase.android.RadarApplication
import org.radarbase.android.RadarApplication.Companion.radarConfig
import org.radarbase.android.RadarConfiguration
-import org.radarbase.android.auth.LoginActivity.Companion.ACTION_LOGIN_SUCCESS
+import org.radarbase.android.util.CoroutineTaskExecutor
import org.radarbase.android.util.DelayedRetry
import org.radarbase.android.util.NetworkConnectedReceiver
-import org.radarbase.android.util.SafeHandler
-import org.radarbase.android.util.send
+import org.radarbase.kotlin.coroutines.launchJoin
import org.radarbase.producer.AuthenticationException
import org.slf4j.LoggerFactory
import java.net.ConnectException
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
@Keep
-abstract class AuthService : Service(), LoginListener {
- private lateinit var appAuth: AppAuthState
+abstract class AuthService : LifecycleService(), LoginListener {
+
+ private val latestAppAuth: AppAuthState
+ get() = authState.value
+
lateinit var loginManagers: List
- private val listeners: MutableList = mutableListOf()
lateinit var config: RadarConfiguration
- val handler = SafeHandler.getInstance("AuthService", THREAD_PRIORITY_BACKGROUND)
- var loginListenerId: Long = 0
private lateinit var networkConnectedListener: NetworkConnectedReceiver
- private var configRegistration: LoginListenerRegistration? = null
private var refreshDelay = DelayedRetry(RETRY_MIN_DELAY, RETRY_MAX_DELAY)
private var isConnected: Boolean = false
+ private val _authState: MutableStateFlow = MutableStateFlow(AppAuthState())
+ val authState: StateFlow = _authState.asStateFlow()
+
+ private val executor: CoroutineTaskExecutor = CoroutineTaskExecutor(AuthService::class.simpleName!!, Dispatchers.Default)
+
+ private val serviceMutex: Mutex = Mutex(false)
+ private val registryTweakMutex: Mutex = Mutex(false)
+ private val authUpdateMutex: Mutex = Mutex(false)
+
+ private val needLoadedState: AtomicBoolean = AtomicBoolean(true)
+ val authUpdatesResumed: AtomicBoolean = AtomicBoolean(true)
+
+ private val _authStateFailures: MutableSharedFlow = MutableSharedFlow(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
+
+ private val _authStateSuccess: MutableSharedFlow = MutableSharedFlow(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+
+ private val _authStateLogout: MutableSharedFlow = MutableSharedFlow(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+
private var sourceRegistrationStarted: Boolean = false
- private val successHandlers: MutableList<() -> Unit> = mutableListOf()
+ private val authStateFailures: Flow = _authStateFailures
+ private val authStateSuccess: Flow = _authStateSuccess
+ private val authStateLogout: Flow = _authStateLogout
open val authSerialization: AuthSerialization by lazy {
SharedPreferencesAuthSerialization(this)
}
+ private val successHandlers: MutableList Unit> = mutableListOf()
+
@Volatile
private var isInLoginActivity: Boolean = false
+ private var isNetworkStatusReceived: Boolean = false
+
+ private var loginListenerId: Long = 0L
+ private val listeners: MutableList = mutableListOf()
- private lateinit var broadcaster: LocalBroadcastManager
override fun onCreate() {
super.onCreate()
- broadcaster = LocalBroadcastManager.getInstance(this)
- appAuth = authSerialization.load() ?: AppAuthState()
- config = radarConfig
- config.updateWithAuthState(this, appAuth)
- handler.start()
- loginManagers = createLoginManagers(appAuth)
- networkConnectedListener = NetworkConnectedReceiver(this, object : NetworkConnectedReceiver.NetworkConnectedListener {
- override fun onNetworkConnectionChanged(state: NetworkConnectedReceiver.NetworkState) {
- handler.execute {
- isConnected = state.isConnected
- if (isConnected && !appAuth.isValidFor(5, TimeUnit.MINUTES)) {
- refresh()
- }
- }
- }
- })
- networkConnectedListener.register()
- configRegistration = addLoginListener(object : LoginListener {
- override fun loginFailed(manager: LoginManager?, ex: java.lang.Exception?) {
- // no action required
+ networkConnectedListener = NetworkConnectedReceiver(this)
+ CoroutineTaskExecutor.startRetryScope()
+ authUpdatesResumed.set(true)
+ executor.start()
+ lifecycleScope.launch(Dispatchers.Default) {
+ needLoadedState.set(true)
+ val loadedAuth = authSerialization.load()
+ loadedAuth ?: run {
+ needLoadedState.set(false)
+ return@launch
}
+ _authState.value = loadedAuth
+ needLoadedState.set(false)
+ }
+
+ lifecycleScope.launch {
+ loginManagers = createLoginManagers(_authState)
+ }
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch(Dispatchers.Default) {
+ authState.collectLatest { state ->
+ if (needLoadedState.get()) {
+ for (i in 1..4) {
+ if (needLoadedState.get()) {
+ logger.trace("Storing auth state -- waiting for state to load")
+ delay(50)
+ }
+ }
+ }
+ authSerialization.store(state)
+ }
+ }
+ launch(Dispatchers.Default) {
+ authState
+ .collectLatest { state ->
+ logger.trace("Collected AppAuth: {}", state)
+ radarConfig.updateWithAuthState(this@AuthService, state)
+ }
+ }
+ launch(Dispatchers.Default) {
+ networkConnectedListener.monitor()
+ networkConnectedListener.state!!
+ .map { it != NetworkConnectedReceiver.NetworkState.Disconnected }
+ .distinctUntilChanged()
+ .onEach { isConnected = it }
+ .collectLatest { isConnected ->
+ isNetworkStatusReceived = true
+ if (isConnected && !latestAppAuth.isValidFor(5, TimeUnit.MINUTES)) {
+ refresh()
+ } else {
+ refreshDelay.reset()
+ }
+ }
+ }
+ launch(Dispatchers.Default) {
+ authStateFailures
+ .collectLatest { (manager, ex) ->
+ logger.info("Login failed: {}", ex.toString())
+ when (ex) {
+ is AuthenticationException -> {
+ callListeners {
+ it.listener.loginFailed(manager, ex)
+ }
+ }
+ is ConnectException -> {
+ isConnected = false
+ executor.delay(refreshDelay.nextDelay(), ::refresh)
+ }
+ else -> {
+ executor.delay(refreshDelay.nextDelay(), ::refresh)
+ }
+ }
+ }
+ }
+ launch(Dispatchers.Default) {
+ authStateSuccess
+ .collectLatest { successState: AuthLoginListener.AuthStateSuccess ->
+ logger.info("Log-in succeeded")
+ isConnected = true
+ refreshDelay.reset()
+ callListeners(sinceUpdate = latestAppAuth.lastUpdate) {
+ logger.trace("Login succeeded (AuthService:: CallListener {} with state): AppAuthState: {}",it, authState.value)
+ it.listener.loginSucceeded(successState.manager, latestAppAuth)
+ }
+ }
+ }
+ launch(Dispatchers.Default) {
+ authStateLogout
+ .collectLatest {
+ logger.info("Logout succeeded")
+ authUpdatesResumed.set(false)
+ val clearedState = latestAppAuth.reset()
+ _authState.value = clearedState
+ authSerialization.remove()
+ callListeners {
+ it.listener.logoutSucceeded(null, clearedState)
+ }
+ radarConfig.updateWithAuthState(this@AuthService, clearedState, true)
+ }
+ }
- override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) {
- refreshDelay.reset()
- authSerialization.store(appAuth)
- config.updateWithAuthState(this@AuthService, appAuth)
}
- })
+ }
}
/**
@@ -84,27 +212,37 @@ abstract class AuthService : Service(), LoginListener {
* the current authentication is not valid and it is not possible to refresh the authentication
* this opens the login activity.
*/
- fun refresh() {
- handler.execute {
+ suspend fun refresh() {
+ executor.execute {
+ if (!waitForAuthUpdates()) {
+ logger.debug("Can't refresh now, performing logout")
+ return@execute
+ }
logger.info("Refreshing authentication state")
- if (appAuth.isValidFor(5, TimeUnit.MINUTES)) {
- callListeners(sinceUpdate = appAuth.lastUpdate) {
- it.loginListener.loginSucceeded(null, appAuth)
+ if (needLoadedState.get()) {
+ for (i in 1..4) {
+ if (needLoadedState.get()) {
+ logger.trace("Refreshing auth state -- waiting for state to load")
+ delay(50)
+ }
}
- } else {
+ }
+ if (!authState.value.isValidFor(5, TimeUnit.MINUTES)) {
doRefresh()
+ } else {
+ callListeners(sinceUpdate = latestAppAuth.lastUpdate) {
+ it.listener.loginSucceeded(null, latestAppAuth)
+ }
}
}
}
- fun triggerFlush() {
- handler.executeReentrant {
- authSerialization.store(appAuth)
+ private suspend fun doRefresh() {
+ var canRefresh = false
+ updateState { state ->
+ canRefresh = state.relevantManagers.any { it.refresh(this) }
}
- }
-
- private fun doRefresh() {
- if (relevantManagers.none { it.refresh(appAuth) }) {
+ if (!canRefresh) {
startLogin()
}
}
@@ -115,21 +253,40 @@ abstract class AuthService : Service(), LoginListener {
* there is no network connectivity, just check if the authentication could be refreshed if the
* client was online.
*/
- fun refreshIfOnline() {
- handler.execute {
- when {
- isConnected -> refresh()
- appAuth.isValid || relevantManagers.any { it.isRefreshable(appAuth) } -> {
+ suspend fun refreshIfOnline() {
+ executor.execute {
+ logger.debug("Refreshing if online")
+ if (!waitForAuthUpdates()) {
+ logger.debug("Not refreshing now, logging out")
+ return@execute
+ }
+
+ if (!waitForNetworkStatus()) {
+ logger.debug("Network status not received, cannot proceed")
+ return@execute
+ }
+
+ if (isConnected) {
+ logger.trace("Network is available, now refreshing")
+ refresh()
+ } else {
+ logger.trace("Network is not available, proceeding with other way")
+ val appAuth = latestAppAuth
+ if (appAuth.isValid || appAuth.relevantManagers.any {
+ it.isRefreshable(
+ appAuth
+ )
+ }) {
logger.info("Retrieving active authentication state without refreshing")
callListeners(sinceUpdate = appAuth.lastUpdate) {
- it.loginListener.loginSucceeded(null, appAuth)
+ it.listener.loginSucceeded(null, appAuth)
}
- }
- else -> {
- logger.error("Failed to retrieve authentication state without refreshing",
+ } else {
+ logger.error(
+ "Failed to retrieve authentication state without refreshing",
IllegalStateException(
"Cannot refresh authentication state $appAuth: not online and no" +
- "applicable authentication manager."
+ "applicable authentication manager."
)
)
startLogin()
@@ -152,201 +309,269 @@ abstract class AuthService : Service(), LoginListener {
}
}
- protected abstract fun showLoginNotification()
+ private suspend fun callListeners(call: suspend (LoginListenerRegistry) -> Unit) {
+ serviceMutex.withLock {
+ listeners.launchJoin {
+ call(it)
+ }
+ }
+ }
- override fun loginFailed(manager: LoginManager?, ex: java.lang.Exception?) {
- handler.executeReentrant {
- logger.info("Login failed: {}", ex?.toString())
- when (ex) {
- is ConnectException -> {
- isConnected = false
- handler.delay(refreshDelay.nextDelay(), ::refresh)
- }
- is AuthenticationException -> {
- callListeners {
- it.loginListener.loginFailed(manager, ex)
- }
- }
- else -> handler.delay(refreshDelay.nextDelay(), ::refresh)
+ private suspend fun callListeners(sinceUpdate: Long, call: (LoginListenerRegistry) -> Unit) {
+ serviceMutex.withLock {
+ val obsoleteListeners = listeners.filter { it.lastUpdated < sinceUpdate }
+ obsoleteListeners.launchJoin {
+ logger.debug("Sending login succeeded to {}", it.listener)
+ it.lastUpdated = sinceUpdate
+ call(it)
}
}
}
- private fun callListeners(call: (LoginListenerRegistration) -> Unit) {
- synchronized(listeners) {
- listeners.forEach(call)
+ suspend fun addLoginListener(loginListener: LoginListener): LoginListenerRegistry {
+ executor.execute {
+ val auth = latestAppAuth
+ if (auth.isValid) {
+ loginListener.loginSucceeded(null, auth)
+ }
}
+ authUpdatesResumed.set(true)
+ return LoginListenerRegistry(loginListener)
+ .also {
+ registryTweakMutex.withLock { listeners += it }
+ logger.debug("Added login listener {} to registry ", it.listener)
+ }
}
- private fun callListeners(sinceUpdate: Long, call: (LoginListenerRegistration) -> Unit) {
- synchronized(listeners) {
- listeners.filter { it.lastUpdate < sinceUpdate }
- .forEach { listener ->
- listener.lastUpdate = sinceUpdate
- call(listener)
- }
+ suspend fun removeLoginListener(registry: LoginListenerRegistry) {
+ registryTweakMutex.withLock {
+ logger.debug(
+ "Removing login listener #{}: {} (starting with {} listeners)",
+ registry.id,
+ registry.listener,
+ listeners.size
+ )
+ listeners -= listeners.filter { it.id == registry.id }.toSet()
}
}
- private val relevantManagers: List
- get() = appAuth.authenticationSource?.let { authSource ->
- loginManagers.filter { authSource in it.sourceTypes }
- } ?: loginManagers
- override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) {
- handler.executeReentrant {
- logger.info("Log in succeeded.")
- isConnected = true
- refreshDelay.reset()
- appAuth = authState
+ protected abstract fun showLoginNotification()
- broadcaster.send(ACTION_LOGIN_SUCCESS)
+ override fun loginFailed(manager: LoginManager?, ex: java.lang.Exception?) {
+ lifecycleScope.launch {
+ _authStateFailures.emit(AuthLoginListener.AuthStateFailure(manager, ex))
+ }
+ }
- callListeners(sinceUpdate = appAuth.lastUpdate) {
- it.loginListener.loginSucceeded(manager, appAuth)
- }
+ override fun loginSucceeded(manager: LoginManager?, authState: AppAuthState) {
+ lifecycleScope.launch {
+ _authStateSuccess.emit(AuthLoginListener.AuthStateSuccess(manager))
}
+ _authState.value = authState
}
- override fun onDestroy() {
- networkConnectedListener.unregister()
- configRegistration?.let { removeLoginListener(it) }
- handler.stop {
- loginManagers.forEach { it.onDestroy() }
- authSerialization.store(appAuth)
+ override suspend fun logoutSucceeded(manager: LoginManager?, authState: AppAuthState) {
+ lifecycleScope.launch {
+ _authStateLogout.emit(AuthLoginListener.AuthStateLogout(manager, authState))
}
+ _authState.value = authState
+ }
+
+ private val AppAuthState.relevantManagers: List
+ get() = loginManagers.filter { it.appliesTo(this) }
+
+ private val AppAuthState.manager: LoginManager
+ get() = relevantManagers.first()
+
+ override fun onDestroy() {
+ super.onDestroy()
+ loginManagers.forEach { it.onDestroy() }
+ executor.stop()
+ CoroutineTaskExecutor.shutdownRetryScope()
}
- fun update(manager: LoginManager) {
- handler.execute {
- logger.info("Refreshing manager {}", manager)
- manager.refresh(appAuth)
+ suspend fun update(manager: LoginManager) {
+ logger.info("Refreshing manager {}", manager)
+ updateState {
+ manager.refresh(this)
}
}
- fun addLoginListener(loginListener: LoginListener): LoginListenerRegistration {
- handler.execute {
- if (appAuth.isValid) {
- loginListener.loginSucceeded(null, appAuth)
+ private suspend fun waitForAuthUpdates(maxAttempts: Int = 2, delayMs: Long = 100): Boolean {
+ repeat(maxAttempts) {
+ if (authUpdatesResumed.get()) {
+ return true
}
+ delay(delayMs)
}
-
- return LoginListenerRegistration(loginListener)
- .also {
- synchronized(listeners) { listeners += it }
- }
+ return false
}
- fun removeLoginListener(registration: LoginListenerRegistration) {
- synchronized(listeners) {
- logger.debug("Removing login listener #{}: {} (starting with {} listeners)", registration.id, registration.loginListener, listeners.size)
- listeners -= listeners.filter { it.id == registration.id }
+ private suspend fun waitForNetworkStatus(maxAttempts: Int = 3, delayMs: Long = 100): Boolean {
+ repeat(maxAttempts) {
+ if (isNetworkStatusReceived) {
+ return true
+ }
+ delay(delayMs)
}
+ return false
}
+
/**
* Create your login managers here. Call [LoginManager.start] for the login method that
* a user indicates.
* @param appAuth previous invalid authentication
* @return non-empty list of login managers to use
*/
- protected abstract fun createLoginManagers(appAuth: AppAuthState): List
-
- private fun updateState(update: AppAuthState.Builder.() -> Unit) {
- handler.executeReentrant {
- appAuth = appAuth.alter(update)
+ protected abstract suspend fun createLoginManagers(appAuth: StateFlow): List