diff --git a/.idea/dictionaries/ninovanhooff.xml b/.idea/dictionaries/ninovanhooff.xml
index fcc16c4..ebc2e28 100644
--- a/.idea/dictionaries/ninovanhooff.xml
+++ b/.idea/dictionaries/ninovanhooff.xml
@@ -1,6 +1,7 @@
 <component name="ProjectDictionaryState">
   <dictionary name="ninovanhooff">
     <words>
+      <w>allprojects</w>
       <w>talkback</w>
     </words>
   </dictionary>
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 2370474..35ffc9d 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -26,5 +26,10 @@
       <option name="name" value="maven" />
       <option name="url" value="https://jitpack.io" />
     </remote-repository>
+    <remote-repository>
+      <option name="id" value="MavenRepo" />
+      <option name="name" value="MavenRepo" />
+      <option name="url" value="https://repo.maven.apache.org/maven2/" />
+    </remote-repository>
   </component>
 </project>
\ No newline at end of file
diff --git a/README.md b/README.md
index 5f4930c..ecc9c16 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 [![](https://jitci.com/gh/Q42/Q42Stats.Android/svg)](https://jitci.com/gh/Q42/Q42Stats.Android)
 
 
-Collect stats for Q42 internal usage, shared accross multiple Android projects.
+Collect stats for Q42 internal usage, shared across multiple Android projects.
 
 An iOS version is also available: https://github.com/Q42/Q42Stats
 
@@ -32,8 +32,8 @@ class SampleApplication : Application() {
         super.onCreate()
         Q42Stats(
             Q42StatsConfig(
-                fireBaseProject = "theProject",
-                firebaseCollection = "theCollection",
+                firebaseProjectId = "theProject",
+                firestoreCollectionId = "theCollection",
                 // wait at least 7.5 days between data collections. the extra .5 is for time-of-day randomization
                 minimumSubmitInterval = (60 * 60 * 24 * 7.5).toLong()
             )
@@ -45,6 +45,12 @@ This can be safely called from the main thread since all work (both collecting s
 
 It is safe to call this function multiple times, as it will exit immediately if it is already running or when a data collection interval has not passed yet.
 
+By default, Q42Stats only logs errors. For debugging purposes, set the log level before using Q42Stats:
+
+```
+Q42Stats.logLevel = Q42StatsLogLevel.Debug
+```
+
 ## Data collected
 
 Not all fields are supported on all versions of Android. If unsupported, the corresponding value may be false, "unknown" or the key may be completely omitted.
diff --git a/app/src/main/java/com/q42/q42stats/sample/SampleApplication.kt b/app/src/main/java/com/q42/q42stats/sample/SampleApplication.kt
index 1dbad8d..1d4e2fe 100644
--- a/app/src/main/java/com/q42/q42stats/sample/SampleApplication.kt
+++ b/app/src/main/java/com/q42/q42stats/sample/SampleApplication.kt
@@ -10,10 +10,10 @@ class SampleApplication : Application() {
         super.onCreate()
         Q42Stats(
             Q42StatsConfig(
-                fireBaseProject = "theProject",
-                firebaseCollection = "theCollection",
+                firebaseProjectId = "theProject",
+                firebaseCollectionId = "theCollection",
                 // wait at least 7.5 days between data collections. the extra .5 is for time-of-day randomization
-                minimumSubmitInterval = (60 * 60 * 24 * 7.5).toLong()
+                minimumSubmitIntervalSeconds = (60 * 60 * 24 * 7.5).toLong()
             )
         ).runAsync(this.applicationContext)
     }
diff --git a/build.gradle b/build.gradle
index 0f24d8b..e245a33 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,7 +3,7 @@ buildscript {
     ext.kotlin_version = "1.4.32"
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
     dependencies {
         classpath "com.android.tools.build:gradle:4.1.3"
@@ -17,7 +17,7 @@ buildscript {
 allprojects {
     repositories {
         google()
-        jcenter()
+        mavenCentral()
         maven { url "https://jitpack.io" }
     }
 }
diff --git a/library/build.gradle b/library/build.gradle
index 9bff5b9..3d2aca4 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -48,6 +48,8 @@ dependencies {
 
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
+    implementation "androidx.annotation:annotation:1.2.0"
+
     testImplementation 'junit:junit:4.13.2'
     //json is included in Android, but not in Kotlin. This import makes it available for unit tests
     testImplementation 'org.json:json:20201115'
diff --git a/library/src/main/java/com/q42/q42stats/library/FireStoreUtils.kt b/library/src/main/java/com/q42/q42stats/library/FireStoreUtils.kt
index 351d5a9..a136cf6 100644
--- a/library/src/main/java/com/q42/q42stats/library/FireStoreUtils.kt
+++ b/library/src/main/java/com/q42/q42stats/library/FireStoreUtils.kt
@@ -15,7 +15,7 @@ import java.io.Serializable
  *     }
  * ```
  */
-fun Map<String, Serializable>.toFireStoreFormat(): JSONObject {
+internal fun Map<String, Serializable>.toFireStoreFormat(): JSONObject {
     val fireStoreMap = mapOf(
         "fields" to this.mapValues {
             mapOf("stringValue" to it.value.toString())
diff --git a/library/src/main/java/com/q42/q42stats/library/HttpService.kt b/library/src/main/java/com/q42/q42stats/library/HttpService.kt
index 321d00e..aa65e7b 100644
--- a/library/src/main/java/com/q42/q42stats/library/HttpService.kt
+++ b/library/src/main/java/com/q42/q42stats/library/HttpService.kt
@@ -1,48 +1,53 @@
 package com.q42.q42stats.library
 
-import org.json.JSONException
+import androidx.annotation.WorkerThread
 import org.json.JSONObject
 import java.io.BufferedWriter
-import java.io.IOException
 import java.io.OutputStreamWriter
 import java.net.HttpURLConnection
 import java.net.URL
 import javax.net.ssl.HttpsURLConnection
 
-object HttpService {
-    /** Synchronously send the stats. Make sure to run this on a worker thread */
+@WorkerThread
+internal object HttpService {
+
     fun sendStatsSync(config: Q42StatsConfig, data: JSONObject) {
         sendStatsSync(
-            "https://firestore.googleapis.com/v1/projects/${config.fireBaseProject}/" +
-                    "databases/(default)/documents/${config.firebaseCollection}?mask.fieldPaths=_",
+            "https://firestore.googleapis.com/v1/projects/${config.firebaseProjectId}/" +
+                    "databases/(default)/documents/${config.firebaseCollectionId}?mask.fieldPaths=_",
             data
         )
     }
 
-    /** Synchronously send the stats. Make sure to run this on a worker thread */
     private fun sendStatsSync(url: String, data: JSONObject) {
         httpPost(url, data)
     }
 
-    @Throws(IOException::class, JSONException::class)
     private fun httpPost(url: String, jsonObject: JSONObject) {
         val conn = URL(url).openConnection() as HttpsURLConnection
-        conn.requestMethod = "POST"
-        conn.setRequestProperty("Content-Type", "application/json; charset=utf-8")
-        setPostRequestContent(conn, jsonObject)
-        conn.connect()
-        Q42StatsLogger.d(TAG, "Response: ${conn.responseCode} ${conn.responseMessage}")
+        try {
+            conn.requestMethod = "POST"
+            conn.setRequestProperty("Content-Type", "application/json; charset=utf-8")
+            sendPostRequestContent(conn, jsonObject)
+        } catch (e: Throwable) {
+            Q42StatsLogger.e(TAG, "Could not send stats to server", e)
+        } finally {
+            conn.disconnect()
+        }
     }
 
-    @Throws(IOException::class)
-    private fun setPostRequestContent(conn: HttpURLConnection, jsonObject: JSONObject) {
-
-        val os = conn.outputStream
-        val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8"))
-        writer.write(jsonObject.toString())
-        Q42StatsLogger.d(TAG, "Sending JSON: $jsonObject")
-        writer.flush()
-        writer.close()
-        os.close()
+    private fun sendPostRequestContent(conn: HttpURLConnection, jsonObject: JSONObject) {
+        try {
+            conn.outputStream.use { os ->
+                BufferedWriter(OutputStreamWriter(os, "UTF-8")).use { writer ->
+                    writer.write(jsonObject.toString())
+                    Q42StatsLogger.d(TAG, "Sending JSON: $jsonObject")
+                    writer.flush()
+                }
+            }
+            Q42StatsLogger.d(TAG, "Response: ${conn.responseCode} ${conn.responseMessage}")
+        } catch (e: Throwable) {
+            Q42StatsLogger.e(TAG, "Could not add data to POST request", e)
+        }
     }
 }
\ No newline at end of file
diff --git a/library/src/main/java/com/q42/q42stats/library/Q42Stats.kt b/library/src/main/java/com/q42/q42stats/library/Q42Stats.kt
index 3189048..ffd684f 100644
--- a/library/src/main/java/com/q42/q42stats/library/Q42Stats.kt
+++ b/library/src/main/java/com/q42/q42stats/library/Q42Stats.kt
@@ -1,45 +1,42 @@
 package com.q42.q42stats.library
 
 import android.content.Context
+import androidx.annotation.AnyThread
+import androidx.annotation.WorkerThread
 import com.q42.q42stats.library.collector.AccessibilityCollector
 import com.q42.q42stats.library.collector.PreferencesCollector
 import com.q42.q42stats.library.collector.SystemCollector
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.*
 import java.io.Serializable
-import java.util.concurrent.atomic.AtomicBoolean
 
 const val TAG = "Q42Stats"
 
-private val MUTEX = Unit
-
 class Q42Stats(private val config: Q42StatsConfig) {
-    private val isRunning = AtomicBoolean(false)
 
     /* Collects stats and sends it to the server. This method is safe to be called from anywhere
-    in your app and will do nothing if it has already run before */
-    fun runAsync(context: Context) {
+    in your app and will do nothing if it is running or has already run before */
+    @AnyThread
+    fun runAsync(context: Context, coroutineScope: CoroutineScope = MainScope()) {
         Q42StatsLogger.d(TAG, "Q42Stats: Checking Preconditions")
-        if (isRunning.get()) {
-            Q42StatsLogger.i(
-                TAG,
-                "Q42Stats is already running. Exit."
-            )
-            return
+        // check preconditions on the main thread to prevent concurrency issues
+        coroutineScope.launch(Dispatchers.Main) {
+            if (job?.isActive != true) { // job is null or not active
+                // Do the actual work on a worker thread
+                job = coroutineScope.launch(Dispatchers.IO) { runSync(context) }
+            } else {
+                Q42StatsLogger.i(TAG, "Q42Stats is already running. Exit.")
+            }
         }
-        GlobalScope.launch(Dispatchers.IO) { synchronized(MUTEX) { runSync(context) } }
     }
 
-    /** This should be run on a worker thread */
+    @WorkerThread
     private fun runSync(context: Context) {
         try {
-            isRunning.set(true)
             val prefs = Q42StatsPrefs(context)
-            if (prefs.withinSubmitInterval(config.minimumSubmitInterval * 1000L) && !BuildConfig.DEBUG) {
+            if (prefs.withinSubmitInterval(config.minimumSubmitIntervalSeconds * 1000L)) {
                 Q42StatsLogger.i(
                     TAG,
-                    "Q42Stats were already sent in the last ${config.minimumSubmitInterval} seconds."
+                    "Q42Stats were already sent in the last ${config.minimumSubmitIntervalSeconds} seconds."
                 )
                 return
             }
@@ -47,7 +44,6 @@ class Q42Stats(private val config: Q42StatsConfig) {
             val collected = collect(context, prefs).toFireStoreFormat()
             HttpService.sendStatsSync(config, collected)
             prefs.updateSubmitTimestamp()
-
         } catch (e: Throwable) {
             Q42StatsLogger.e(TAG, "Q42Stats encountered an error", e)
             if (BuildConfig.DEBUG) {
@@ -55,7 +51,6 @@ class Q42Stats(private val config: Q42StatsConfig) {
             }
         } finally {
             Q42StatsLogger.i(TAG, "Q42Stats: Exit")
-            isRunning.set(false)
         }
     }
 
@@ -71,4 +66,16 @@ class Q42Stats(private val config: Q42StatsConfig) {
         collected += SystemCollector.collect()
         return collected
     }
+
+    companion object {
+        @Suppress("unused")
+        var logLevel: Q42StatsLogLevel
+            get() = Q42StatsLogger.logLevel
+            set(value) {
+                Q42StatsLogger.logLevel = value
+            }
+
+        /** A static job ensures that only a single instance of Q42Stats can be running */
+        private var job: Job? = null
+    }
 }
\ No newline at end of file
diff --git a/library/src/main/java/com/q42/q42stats/library/Q42StatsConfig.kt b/library/src/main/java/com/q42/q42stats/library/Q42StatsConfig.kt
index 1a68b87..ae71f0d 100644
--- a/library/src/main/java/com/q42/q42stats/library/Q42StatsConfig.kt
+++ b/library/src/main/java/com/q42/q42stats/library/Q42StatsConfig.kt
@@ -1,9 +1,9 @@
 package com.q42.q42stats.library
 
 data class Q42StatsConfig(
-    val fireBaseProject: String,
-    val firebaseCollection: String,
+    val firebaseProjectId: String,
+    val firebaseCollectionId: String,
     /** Data collection is skipped when less than this many seconds have passed
      * since the previous run */
-    val minimumSubmitInterval: Long
+    val minimumSubmitIntervalSeconds: Long
 )
\ No newline at end of file
diff --git a/library/src/main/java/com/q42/q42stats/library/Q42StatsLogger.kt b/library/src/main/java/com/q42/q42stats/library/Q42StatsLogger.kt
index df81f2d..839f3c6 100644
--- a/library/src/main/java/com/q42/q42stats/library/Q42StatsLogger.kt
+++ b/library/src/main/java/com/q42/q42stats/library/Q42StatsLogger.kt
@@ -2,42 +2,42 @@ package com.q42.q42stats.library
 
 import android.util.Log
 
-object Q42StatsLogger {
+internal object Q42StatsLogger {
     /** logs with lower importance will be ignored */
-    private val logLevel = if (BuildConfig.DEBUG) LogLevel.Verbose else LogLevel.Info
+    var logLevel = if (BuildConfig.DEBUG) Q42StatsLogLevel.Verbose else Q42StatsLogLevel.Error
 
     fun v(tag: String, message: String) {
-        if (logLevel <= LogLevel.Verbose) {
+        if (logLevel <= Q42StatsLogLevel.Verbose) {
             Log.v(tag, message)
         }
     }
 
     fun d(tag: String, message: String) {
-        if (logLevel <= LogLevel.Debug) {
+        if (logLevel <= Q42StatsLogLevel.Debug) {
             Log.d(tag, message)
         }
     }
 
     fun i(tag: String, message: String) {
-        if (logLevel <= LogLevel.Info) {
+        if (logLevel <= Q42StatsLogLevel.Info) {
             Log.i(tag, message)
         }
     }
 
     fun w(tag: String, message: String) {
-        if (logLevel <= LogLevel.Warn) {
+        if (logLevel <= Q42StatsLogLevel.Warn) {
             Log.w(tag, message)
         }
     }
 
     fun e(tag: String, message: String, e: Throwable? = null) {
-        if (logLevel <= LogLevel.Error) {
-            Log.e(tag, message, e)
+        if (logLevel <= Q42StatsLogLevel.Error) {
+            Log.e(tag, "$message: ${e?.message}", e)
         }
     }
 }
 
-enum class LogLevel {
+enum class Q42StatsLogLevel {
     //Log levels in order of importance
     Verbose,
     Debug,
diff --git a/library/src/main/java/com/q42/q42stats/library/Q42StatsPrefs.kt b/library/src/main/java/com/q42/q42stats/library/Q42StatsPrefs.kt
index b1db488..3b453fd 100644
--- a/library/src/main/java/com/q42/q42stats/library/Q42StatsPrefs.kt
+++ b/library/src/main/java/com/q42/q42stats/library/Q42StatsPrefs.kt
@@ -8,7 +8,7 @@ private const val SHARED_PREFS_NAME = "Q42StatsPrefs"
 private const val LAST_SUBMIT_TIMESTAMP_KEY = "lastSubmitTimestamp"
 private const val INSTALLATION_ID_KEY = "installationId"
 
-class Q42StatsPrefs(context: Context) {
+internal class Q42StatsPrefs(context: Context) {
     private val prefs: SharedPreferences =
         context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
 
@@ -21,20 +21,16 @@ class Q42StatsPrefs(context: Context) {
         System.currentTimeMillis() <
                 prefs.getLong(LAST_SUBMIT_TIMESTAMP_KEY, 0) + interval
 
-    fun updateSubmitTimestamp() {
-        prefs.edit().apply() {
-            putLong(LAST_SUBMIT_TIMESTAMP_KEY, System.currentTimeMillis())
-            apply()
-        }
+    fun updateSubmitTimestamp() = with(prefs.edit()) {
+        putLong(LAST_SUBMIT_TIMESTAMP_KEY, System.currentTimeMillis())
+        apply()
     }
 
-    private fun createInstallationId(): String {
-        prefs.edit().apply() {
-            val uuid = UUID.randomUUID().toString()
-            putString(INSTALLATION_ID_KEY, uuid)
-            apply()
-            return uuid
-        }
+    private fun createInstallationId(): String = with(prefs.edit()) {
+        val uuid = UUID.randomUUID().toString()
+        putString(INSTALLATION_ID_KEY, uuid)
+        apply()
+        return uuid
     }
 
 }
\ No newline at end of file
diff --git a/library/src/main/java/com/q42/q42stats/library/collector/AccessibilityCollector.kt b/library/src/main/java/com/q42/q42stats/library/collector/AccessibilityCollector.kt
index 28fe552..7783db0 100644
--- a/library/src/main/java/com/q42/q42stats/library/collector/AccessibilityCollector.kt
+++ b/library/src/main/java/com/q42/q42stats/library/collector/AccessibilityCollector.kt
@@ -14,7 +14,7 @@ import java.io.Serializable
 import java.util.*
 
 /** Collects Accessibility-related settings and preferences, such as font scaling */
-object AccessibilityCollector {
+internal object AccessibilityCollector {
 
     fun collect(context: Context) = mutableMapOf<String, Serializable>().apply {
         val accessibilityManager =
diff --git a/library/src/main/java/com/q42/q42stats/library/collector/PreferencesCollector.kt b/library/src/main/java/com/q42/q42stats/library/collector/PreferencesCollector.kt
index 8d1e7f7..7dff7c7 100644
--- a/library/src/main/java/com/q42/q42stats/library/collector/PreferencesCollector.kt
+++ b/library/src/main/java/com/q42/q42stats/library/collector/PreferencesCollector.kt
@@ -6,7 +6,7 @@ import com.q42.q42stats.library.util.DayTimeUtil
 import java.io.Serializable
 
 /** Collects System settings such as default locale */
-object PreferencesCollector {
+internal object PreferencesCollector {
 
     fun collect(context: Context) = mutableMapOf<String, Serializable>().apply {
         val configuration = context.resources.configuration
diff --git a/library/src/main/java/com/q42/q42stats/library/collector/SystemCollector.kt b/library/src/main/java/com/q42/q42stats/library/collector/SystemCollector.kt
index b64307a..9c06c4c 100644
--- a/library/src/main/java/com/q42/q42stats/library/collector/SystemCollector.kt
+++ b/library/src/main/java/com/q42/q42stats/library/collector/SystemCollector.kt
@@ -4,7 +4,7 @@ import java.io.Serializable
 import java.util.*
 
 /** Collects System settings such as default locale */
-object SystemCollector {
+internal object SystemCollector {
 
     fun collect() = mutableMapOf<String, Serializable>().apply {
         put("defaultLanguage", Locale.getDefault().language) // language code like en or nl
diff --git a/library/src/main/java/com/q42/q42stats/library/util/DayTimeUtil.kt b/library/src/main/java/com/q42/q42stats/library/util/DayTimeUtil.kt
index 0c78377..01eb5c3 100644
--- a/library/src/main/java/com/q42/q42stats/library/util/DayTimeUtil.kt
+++ b/library/src/main/java/com/q42/q42stats/library/util/DayTimeUtil.kt
@@ -4,9 +4,9 @@ import org.jetbrains.annotations.TestOnly
 import java.util.*
 import kotlin.math.abs
 
-object DayTimeUtil {
+internal object DayTimeUtil {
 
-    // Real hacky function for determening day/night/twilight
+    // Real hacky function for determining day/night/twilight
     // With very wide margin (3 hours), because we don't know how
     // Android implements this for Dark mode switch
     // Source: https://github.com/Q42/Q42Stats/blob/master/Sources/Q42Stats/Q42Stats.swift