diff --git a/sdk/build.gradle b/sdk/build.gradle index fb60176e3..6f66f731b 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -10,7 +10,7 @@ apply plugin: "org.jetbrains.dokka" apply plugin: 'io.radar.mvnpublish' ext { - radarVersion = '3.8.18' + radarVersion = '3.8.18-beta.2' } String buildNumber = ".${System.currentTimeMillis()}" diff --git a/sdk/src/main/java/io/radar/sdk/Radar.kt b/sdk/src/main/java/io/radar/sdk/Radar.kt index 8f2e50c1f..847e6017b 100644 --- a/sdk/src/main/java/io/radar/sdk/Radar.kt +++ b/sdk/src/main/java/io/radar/sdk/Radar.kt @@ -474,7 +474,7 @@ object Radar { } if (!this::logBuffer.isInitialized) { - this.logBuffer = RadarSimpleLogBuffer() + this.logBuffer = RadarSimpleLogBuffer(this.context) } if (!this::replayBuffer.isInitialized) { @@ -547,6 +547,10 @@ object Radar { } }) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + this.logger.logPastTermination() + } + this.initialized = true logger.i("📍️ Radar initialized") @@ -3088,6 +3092,29 @@ object Radar { RadarSettings.setLogLevel(context, level) } + /** + Log application resigning active. + */ + @JvmStatic + fun logResigningActive() { + if (!initialized) { + return + } + this.logger.logResigningActive() + } + + /** + Log application entering background and flush logs in memory buffer into persistent buffer. + */ + @JvmStatic + fun logBackgrounding() { + if (!initialized) { + return + } + this.logger.logBackgrounding() + this.logBuffer.persistLogs() + } + /** * Flushes debug logs to the server. */ @@ -3097,7 +3124,7 @@ object Radar { return } - val flushable = logBuffer.getFlushableLogsStash() + val flushable = logBuffer.getFlushableLogs() val logs = flushable.get() if (logs.isNotEmpty()) { apiClient.log(logs, object : RadarApiClient.RadarLogCallback { @@ -3378,11 +3405,15 @@ object Radar { logger.e("📍️ Radar error received | status = $status", RadarLogType.SDK_ERROR) } - internal fun sendLog(level: RadarLogLevel, message: String, type: RadarLogType?) { + internal fun sendLog(level: RadarLogLevel, message: String, type: RadarLogType?, createdAt: Date = Date()) { receiver?.onLog(context, message) if (isTestKey()) { - logBuffer.write(level, message, type) + logBuffer.write(level, type, message, createdAt) } } + internal fun setLogPersistenceFeatureFlag(enabled: Boolean) { + this.logBuffer.setPersistentLogFeatureFlag(enabled) + } + } diff --git a/sdk/src/main/java/io/radar/sdk/RadarActivityLifecycleCallbacks.kt b/sdk/src/main/java/io/radar/sdk/RadarActivityLifecycleCallbacks.kt index d7732c8b2..80763c64b 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarActivityLifecycleCallbacks.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarActivityLifecycleCallbacks.kt @@ -10,7 +10,6 @@ import android.view.InputDevice import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import io.radar.sdk.Radar.RadarStatus import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import io.radar.sdk.model.RadarConfig @@ -93,6 +92,7 @@ internal class RadarActivityLifecycleCallbacks( foreground = count > 0 updatePermissionsDenied(activity) + Radar.logResigningActive() } override fun onActivityStarted(activity: Activity) { @@ -101,6 +101,7 @@ internal class RadarActivityLifecycleCallbacks( override fun onActivityStopped(activity: Activity) { updatePermissionsDenied(activity) + Radar.logBackgrounding() } override fun onActivityDestroyed(activity: Activity) { diff --git a/sdk/src/main/java/io/radar/sdk/RadarLogger.kt b/sdk/src/main/java/io/radar/sdk/RadarLogger.kt index 9bb14adf4..903592cc4 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarLogger.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarLogger.kt @@ -1,9 +1,18 @@ package io.radar.sdk +import android.app.ActivityManager +import java.text.SimpleDateFormat import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import io.radar.sdk.Radar.RadarLogLevel import io.radar.sdk.Radar.RadarLogType +import java.util.Date +import java.util.Locale internal class RadarLogger( private val context: Context @@ -49,4 +58,50 @@ internal class RadarLogger( } } + @RequiresApi(Build.VERSION_CODES.R) + fun logPastTermination(){ + val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val sharedPreferences = this.context.getSharedPreferences("RadarSDK", Context.MODE_PRIVATE) + val previousTimestamp = sharedPreferences.getLong("last_timestamp", 0) + val currentTimestamp = System.currentTimeMillis() + with(sharedPreferences.edit()) { + putLong("last_timestamp", currentTimestamp) + apply() + } + val batteryLevel = this.getBatteryLevel() + + val crashLists = activityManager.getHistoricalProcessExitReasons(null, 0, 10) + if (crashLists.isNotEmpty()) { + for (crashInfo in crashLists) { + if (crashInfo.timestamp > previousTimestamp) { + Radar.sendLog(RadarLogLevel.INFO, "App terminating | with reason: ${crashInfo.getDescription()} | at ${dateFormat.format(Date(crashInfo.timestamp))} | with ${batteryLevel * 100}% battery", null, Date(crashInfo.timestamp)) + break + } + } + } + } + + fun getBatteryLevel(): Float { + val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter -> + this.context.registerReceiver(null, ifilter) + } + val level: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 + val scale: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 + val batteryPct: Float = level / scale.toFloat() + return batteryPct + } + + fun logBackgrounding() { + val batteryLevel = this.getBatteryLevel() + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + this.i("App entering background | at ${dateFormat.format(Date())} | with ${batteryLevel * 100}% battery") + } + + fun logResigningActive() { + val batteryLevel = this.getBatteryLevel() + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + this.i("App resigning active | at ${dateFormat.format(Date())} | with ${batteryLevel * 100}% battery") + } + } \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/RadarSettings.kt b/sdk/src/main/java/io/radar/sdk/RadarSettings.kt index c0479ca40..6737b1efa 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarSettings.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarSettings.kt @@ -294,6 +294,7 @@ internal object RadarSettings { } fun setFeatureSettings(context: Context, featureSettings: RadarFeatureSettings) { + Radar.setLogPersistenceFeatureFlag(featureSettings.useLogPersistence) val optionsJson = featureSettings.toJson().toString() getSharedPreferences(context).edit { putString(KEY_FEATURE_SETTINGS, optionsJson) } @@ -301,7 +302,13 @@ internal object RadarSettings { fun getFeatureSettings(context: Context): RadarFeatureSettings { val sharedPrefFeatureSettings = getSharedPreferences(context).getString(KEY_FEATURE_SETTINGS, null) - Radar.logger.d("getFeatureSettings | featureSettings = $sharedPrefFeatureSettings") + // The log buffer singleton is initialized before the logger, but requires calling this method + // to obtain its feature flag. Thus we cannot call the logger yet as its not yet initialized. + try { + Radar.logger.d("getFeatureSettings | featureSettings = $sharedPrefFeatureSettings") + } catch (e: Exception) { + // Do nothing for now + } val optionsJson = sharedPrefFeatureSettings ?: return RadarFeatureSettings.default() return RadarFeatureSettings.fromJson(JSONObject(optionsJson)) } diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarFeatureSettings.kt b/sdk/src/main/java/io/radar/sdk/model/RadarFeatureSettings.kt index 049692989..d4792f24e 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarFeatureSettings.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarFeatureSettings.kt @@ -9,7 +9,8 @@ internal data class RadarFeatureSettings( val maxConcurrentJobs: Int, val schedulerRequiresNetwork: Boolean, val usePersistence: Boolean, - val extendFlushReplays: Boolean + val extendFlushReplays: Boolean, + val useLogPersistence: Boolean ) { companion object { private const val MAX_CONCURRENT_JOBS = "maxConcurrentJobs" @@ -17,6 +18,7 @@ internal data class RadarFeatureSettings( private const val USE_PERSISTENCE = "usePersistence" private const val SCHEDULER_REQUIRES_NETWORK = "networkAny" private const val EXTEND_FLUSH_REPLAYS = "extendFlushReplays" + private const val USE_LOG_PERSISTENCE = "useLogPersistence" fun fromJson(json: JSONObject?): RadarFeatureSettings { return if (json == null) { @@ -26,7 +28,8 @@ internal data class RadarFeatureSettings( json.optInt(MAX_CONCURRENT_JOBS, DEFAULT_MAX_CONCURRENT_JOBS), json.optBoolean(SCHEDULER_REQUIRES_NETWORK), json.optBoolean(USE_PERSISTENCE), - json.optBoolean(EXTEND_FLUSH_REPLAYS) + json.optBoolean(EXTEND_FLUSH_REPLAYS), + json.optBoolean(USE_LOG_PERSISTENCE) ) } } @@ -36,7 +39,8 @@ internal data class RadarFeatureSettings( DEFAULT_MAX_CONCURRENT_JOBS, false, // networkAny false, // usePersistence - false // extendFlushReplays + false, // extendFlushReplays + false // useLogPersistence ) } } @@ -47,6 +51,7 @@ internal data class RadarFeatureSettings( putOpt(SCHEDULER_REQUIRES_NETWORK, schedulerRequiresNetwork) putOpt(USE_PERSISTENCE, usePersistence) putOpt(EXTEND_FLUSH_REPLAYS, extendFlushReplays) + putOpt(USE_LOG_PERSISTENCE, useLogPersistence) } } } diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarLog.kt b/sdk/src/main/java/io/radar/sdk/model/RadarLog.kt index 3c9e65a56..b4385633d 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarLog.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarLog.kt @@ -22,9 +22,11 @@ internal data class RadarLog( @JvmStatic fun fromJson(json: JSONObject): RadarLog { + val levelString = json.optString(LEVEL) + val typeString = json.optString(TYPE) return RadarLog( - level = Radar.RadarLogLevel.valueOf(json.optString(LEVEL)), - type = Radar.RadarLogType.valueOf(json.optString(TYPE)), + level = if (levelString.isNotBlank()) Radar.RadarLogLevel.valueOf(levelString) else Radar.RadarLogLevel.INFO, + type = if (typeString.isNotBlank() && typeString!="NONE") Radar.RadarLogType.valueOf(typeString) else null, message = json.optString(MESSAGE), createdAt = Date(json.optLong(CREATED_AT)) ) diff --git a/sdk/src/main/java/io/radar/sdk/util/RadarFileStorage.kt b/sdk/src/main/java/io/radar/sdk/util/RadarFileStorage.kt new file mode 100644 index 000000000..6bee1309b --- /dev/null +++ b/sdk/src/main/java/io/radar/sdk/util/RadarFileStorage.kt @@ -0,0 +1,62 @@ +package io.radar.sdk.util +import android.content.Context +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream + +class RadarFileStorage(private val context: Context) { + fun writeData(subDir: String = "", filename: String, content: String) { + var fileOutputStream: FileOutputStream + try { + val directory = if (subDir.isNotEmpty()) File(context.filesDir, subDir) else context.filesDir + directory.mkdirs() + val file = File(directory, filename) + fileOutputStream = FileOutputStream(file) + fileOutputStream.use { it.write(content.toByteArray()) } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun readFileAtPath(subDir: String = "", filePath: String): String { + var fileInputStream: FileInputStream? = null + try { + val directory = if (subDir.isNotEmpty()) File(context.filesDir, subDir) else context.filesDir + val file = File(directory, filePath) + fileInputStream = FileInputStream(file) + val inputStreamReader = fileInputStream?.reader() + return inputStreamReader?.readText() ?: "" + } catch (e: FileNotFoundException) { + return "" + } catch (e: Exception) { + e.printStackTrace() + } finally { + fileInputStream?.close() + } + return "" + } + + fun deleteFileAtPath(subDir: String = "", filePath: String): Boolean { + try { + val directory = if (subDir.isNotEmpty()) File(context.filesDir, subDir) else context.filesDir + val file = File(directory, filePath) + return file.delete() + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + fun sortedFilesInDirectory(directoryPath: String, comparator: Comparator): Array? { + try { + val directory = File(context.filesDir, directoryPath) + var files = directory.listFiles() + files?.sortWith(comparator) + return files + } catch (e: Exception) { + e.printStackTrace() + } + return null + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/util/RadarLogBuffer.kt b/sdk/src/main/java/io/radar/sdk/util/RadarLogBuffer.kt index 98b6a02cc..4f9222909 100644 --- a/sdk/src/main/java/io/radar/sdk/util/RadarLogBuffer.kt +++ b/sdk/src/main/java/io/radar/sdk/util/RadarLogBuffer.kt @@ -1,17 +1,26 @@ package io.radar.sdk.util +import android.content.Context import io.radar.sdk.Radar import io.radar.sdk.model.RadarLog +import java.util.Date internal interface RadarLogBuffer { + abstract val context: Context + /** * Write a log to the buffer * * @param[level] log level * @param[message] log message */ - fun write(level: Radar.RadarLogLevel, message: String, type: Radar.RadarLogType?) + fun write( + level: Radar.RadarLogLevel, + type: Radar.RadarLogType?, + message: String, + createdAt: Date = Date() + ) /** * Creates a stash of the logs currently in the buffer and returns them as a [Flushable] so that a successful @@ -19,5 +28,12 @@ internal interface RadarLogBuffer { * * @return a [Flushable] containing all stored logs */ - fun getFlushableLogsStash(): Flushable + fun getFlushableLogs(): Flushable + + /** + * Persist the logs to disk + */ + fun persistLogs() + + fun setPersistentLogFeatureFlag(persistentLogFeatureFlag: Boolean) } \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/util/RadarSimpleLogBuffer.kt b/sdk/src/main/java/io/radar/sdk/util/RadarSimpleLogBuffer.kt index 8aa6edfed..fb70c4ce8 100644 --- a/sdk/src/main/java/io/radar/sdk/util/RadarSimpleLogBuffer.kt +++ b/sdk/src/main/java/io/radar/sdk/util/RadarSimpleLogBuffer.kt @@ -3,32 +3,157 @@ package io.radar.sdk.util import io.radar.sdk.Radar import io.radar.sdk.model.RadarLog import java.util.concurrent.LinkedBlockingDeque +import android.content.Context +import io.radar.sdk.RadarSettings +import org.json.JSONObject +import org.json.JSONException +import java.io.File +import java.lang.Integer.min +import java.util.Date +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit /** - * Basic Log Buffer implementation that is backed by a field. + * Log Buffer implementation that is backed by an in-memory buffer and files on disk. */ -internal class RadarSimpleLogBuffer : RadarLogBuffer { +internal class +RadarSimpleLogBuffer(override val context: Context): RadarLogBuffer { private companion object { - const val MAXIMUM_CAPACITY = 1000 - const val HALF_CAPACITY = MAXIMUM_CAPACITY / 2 + + const val MAX_MEMORY_BUFFER_SIZE = 200 + const val MAX_PERSISTED_BUFFER_SIZE = 500 + const val PURGE_AMOUNT = 250 + const val logFileDir = "radar_logs" + var fileCounter = 0 + const val KEY_PURGED_LOG_LINE = "----- purged oldest logs -----" + + } + private var persistentLogFeatureFlag = false + + private val lock = Any() + + private val timer = Executors.newScheduledThreadPool(1) + + private val logBuffer = LinkedBlockingDeque() + + init { + persistentLogFeatureFlag = RadarSettings.getFeatureSettings(context).useLogPersistence + val file = File(context.filesDir, logFileDir) + if (!file.exists()) { + file.mkdir() + } + timer.scheduleAtFixedRate({ persistLogs() }, 2, 2, TimeUnit.SECONDS) + } + + override fun setPersistentLogFeatureFlag(persistentLogFeatureFlag: Boolean){ + this.persistentLogFeatureFlag = persistentLogFeatureFlag + } + + override fun write( + level: Radar.RadarLogLevel, + type: Radar.RadarLogType?, + message: String, + createdAt: Date + ) { + synchronized(lock) { + val radarLog = RadarLog(level, message, type, createdAt) + logBuffer.put(radarLog) + if (persistentLogFeatureFlag) { + if (logBuffer.size > MAX_MEMORY_BUFFER_SIZE) { + persistLogs() + } + } else { + if (logBuffer.size > MAX_PERSISTED_BUFFER_SIZE) { + purgeOldestLogs() + } + } + } + } + + + override fun persistLogs() { + synchronized(lock) { + if (persistentLogFeatureFlag) { + if (logBuffer.size > 0) { + writeToFileStorage(logBuffer) + logBuffer.clear() + } + } + } + } + + private fun getLogFilesInTimeOrder(): Array? { + val compareTimeStamps = Comparator { file1, file2 -> + val number1 = file1.name.replace("_","").toLongOrNull() ?: 0L + val number2 = file2.name.replace("_","").toLongOrNull() ?: 0L + number1.compareTo(number2) + } + + return RadarFileStorage(context).sortedFilesInDirectory(logFileDir, compareTimeStamps) + } + + private fun isValidJson(json: String): Boolean { + return try { + JSONObject(json) + true + } catch (ex: JSONException) { + false + } } /** - * Concurrency-safe list of logs with a maximum capacity of 1000 + * Gets logs from disk. */ - private val list = LinkedBlockingDeque(MAXIMUM_CAPACITY) + private fun readFromFileStorage(): LinkedBlockingDeque { - override fun write(level: Radar.RadarLogLevel, message: String, type: Radar.RadarLogType?) { - if (!list.offer(RadarLog(level, message, type))) { - purgeOldestLogs() - list.put(RadarLog(level, message, type)) + val files = getLogFilesInTimeOrder() + val logs = LinkedBlockingDeque() + if (files.isNullOrEmpty()) { + return logs + } + + for (file in files) { + val jsonString = RadarFileStorage(context).readFileAtPath(logFileDir, file.name) + if (jsonString.isNullOrEmpty() || !isValidJson(jsonString)) { + file.delete() + continue + } + val log = RadarLog.fromJson(JSONObject(jsonString)) + if (log != null) { + logs.add(log) + } } + return logs } - override fun getFlushableLogsStash(): Flushable { + /** + * Writes logs to disk. + */ + private fun writeToFileStorage(logs: Collection) { + for (log in logs) { + val counterString = String.format("%04d", fileCounter++) + val fileName = "${log.createdAt.time / 1000}_${counterString}" + RadarFileStorage(context).writeData(logFileDir, fileName, log.toJson().toString()) + } + } + + + override fun getFlushableLogs(): Flushable { val logs = mutableListOf() - list.drainTo(logs) + synchronized(lock) { + if (persistentLogFeatureFlag) { + persistLogs() + purgeOldestLogs() + readFromFileStorage().drainTo(logs) + val files = getLogFilesInTimeOrder() + for (i in 0 until min(logs.size,files?.size ?:0)){ + files?.get(i)?.delete() + } + } else { + logBuffer.drainTo(logs) + } + } return object : Flushable { override fun get(): List { @@ -36,26 +161,51 @@ internal class RadarSimpleLogBuffer : RadarLogBuffer { } override fun onFlush(success: Boolean) { + // clear the logs from disk if (!success) { - // Reverse order to ensure the logs will purge correctly (oldest logs purged first) - logs.reverse() - logs.forEach { - if (!list.offerFirst(it)) { - purgeOldestLogs() - } - } + if (persistentLogFeatureFlag) { + writeToFileStorage(logs) + purgeOldestLogs() + } else { + logs.reverse() + logs.forEach { + if (!logBuffer.offerFirst(it)) { + purgeOldestLogs() + } + } + } } } - } } + /** - * Clears oldest logs and adds a "purged" log line + * Clears oldest logs and adds a "purged" log line. */ private fun purgeOldestLogs() { - val logs = mutableListOf() - list.drainTo(logs, HALF_CAPACITY) - write(Radar.RadarLogLevel.DEBUG, "----- purged oldest logs -----", null) + if (persistentLogFeatureFlag) { + var files = getLogFilesInTimeOrder() + if (files.isNullOrEmpty()) { + return + } + var printedPurgedLogs = false + while (files?.size ?: 0 > MAX_PERSISTED_BUFFER_SIZE) { + val numberToPurge = min(PURGE_AMOUNT,files?.size ?: 0) + for (i in 0 until numberToPurge) { + files?.get(i)?.delete() + } + if (!printedPurgedLogs) { + writeToFileStorage(listOf(RadarLog(Radar.RadarLogLevel.DEBUG, KEY_PURGED_LOG_LINE, null))) + printedPurgedLogs = true + } + files = getLogFilesInTimeOrder() + } + } else { + val oldLogs = mutableListOf() + logBuffer.drainTo(oldLogs, PURGE_AMOUNT) + write(Radar.RadarLogLevel.DEBUG, null, KEY_PURGED_LOG_LINE) + } } + } diff --git a/sdk/src/test/java/io/radar/sdk/model/RadarFeatureSettingsTest.kt b/sdk/src/test/java/io/radar/sdk/model/RadarFeatureSettingsTest.kt index 498cba91f..b9c5e37ee 100644 --- a/sdk/src/test/java/io/radar/sdk/model/RadarFeatureSettingsTest.kt +++ b/sdk/src/test/java/io/radar/sdk/model/RadarFeatureSettingsTest.kt @@ -19,6 +19,7 @@ class RadarFeatureSettingsTest { private var requiresNetwork = false private var usePersistence = true private var extendFlushReplays = false + private var useLogPersistence = true private lateinit var jsonString: String @Before @@ -30,6 +31,7 @@ class RadarFeatureSettingsTest { "networkAny":$requiresNetwork, "maxConcurrentJobs":$maxConcurrentJobs, "usePersistence":$usePersistence, + "useLogPersistence":$useLogPersistence, "extendFlushReplays":$extendFlushReplays }""".trimIndent() } @@ -38,7 +40,7 @@ class RadarFeatureSettingsTest { fun testToJson() { assertEquals( jsonString.removeWhitespace(), - RadarFeatureSettings(maxConcurrentJobs, requiresNetwork, usePersistence, extendFlushReplays).toJson().toString().removeWhitespace() + RadarFeatureSettings(maxConcurrentJobs, requiresNetwork, usePersistence, extendFlushReplays, useLogPersistence).toJson().toString().removeWhitespace() ) } @@ -49,6 +51,7 @@ class RadarFeatureSettingsTest { assertEquals(requiresNetwork, settings.schedulerRequiresNetwork) assertEquals(usePersistence, settings.usePersistence) assertEquals(extendFlushReplays, settings.extendFlushReplays) + assertEquals(useLogPersistence, settings.useLogPersistence) } @Test @@ -58,6 +61,7 @@ class RadarFeatureSettingsTest { assertFalse(settings.schedulerRequiresNetwork) assertFalse(settings.usePersistence) assertFalse(settings.extendFlushReplays) + assertFalse(settings.useLogPersistence) } private fun String.removeWhitespace(): String = replace("\\s".toRegex(), "") diff --git a/sdk/src/test/java/io/radar/sdk/util/RadarFileStorageTest.kt b/sdk/src/test/java/io/radar/sdk/util/RadarFileStorageTest.kt new file mode 100644 index 000000000..af2a53901 --- /dev/null +++ b/sdk/src/test/java/io/radar/sdk/util/RadarFileStorageTest.kt @@ -0,0 +1,55 @@ +package io.radar.sdk.util + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.Assert.* +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.io.File + +@RunWith(AndroidJUnit4::class) +@Config(sdk=[Build.VERSION_CODES.P]) +class RadarFileStorageTest { + companion object { + private val context: Context = ApplicationProvider.getApplicationContext() + private val radarFileStorage: RadarFileStorage = RadarFileStorage(context) + private val testPath:String = "Test.txt" + } + + @Test + fun testWriteToFile() { + radarFileStorage.writeData("test1", testPath, "testing text1") + radarFileStorage.writeData("test1", testPath, "testing text2") + assertEquals(radarFileStorage.readFileAtPath("test1", testPath), "testing text2") + radarFileStorage.deleteFileAtPath("test1", testPath) + assertEquals(radarFileStorage.readFileAtPath("test1", testPath), "") + + radarFileStorage.writeData( filename = testPath, content = "testing text1") + radarFileStorage.writeData( filename = testPath, content = "testing text3") + assertEquals(radarFileStorage.readFileAtPath( filePath = testPath), "testing text3") + radarFileStorage.deleteFileAtPath(filePath = testPath) + assertEquals(radarFileStorage.readFileAtPath(filePath = testPath), "") + + + } + + @Test + fun testAllFilesInDirectory(){ + radarFileStorage.writeData("testDir", "578", "testing text1") + radarFileStorage.writeData("testDir", "456", "testing text2") + val comparator = Comparator { file1, file2 -> + val number1 = file1.name.toLongOrNull() ?: 0L + val number2 = file2.name.toLongOrNull() ?: 0L + number1.compareTo(number2) + } + val files = radarFileStorage.sortedFilesInDirectory("testDir", comparator) + assertNotNull(files) + assertEquals(files?.size, 2) + assertEquals(files?.get(0)?.name ?: "" , "456") + + } + +} \ No newline at end of file diff --git a/sdk/src/test/java/io/radar/sdk/util/RadarSimpleLogBufferTest.kt b/sdk/src/test/java/io/radar/sdk/util/RadarSimpleLogBufferTest.kt index 257f3191c..e209477d3 100644 --- a/sdk/src/test/java/io/radar/sdk/util/RadarSimpleLogBufferTest.kt +++ b/sdk/src/test/java/io/radar/sdk/util/RadarSimpleLogBufferTest.kt @@ -1,6 +1,8 @@ package io.radar.sdk.util +import android.content.Context import android.os.Build +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.radar.sdk.Radar import io.radar.sdk.matchers.RangeMatcher.Companion.isBetween @@ -20,28 +22,31 @@ class RadarSimpleLogBufferTest { fun testLifecycle() { // Create the log buffer - val logBuffer = RadarSimpleLogBuffer() + val context: Context = ApplicationProvider.getApplicationContext() + val logBuffer = RadarSimpleLogBuffer(context) + logBuffer.setPersistentLogFeatureFlag(true) + // Preconditions - var flushable = logBuffer.getFlushableLogsStash() + var flushable = logBuffer.getFlushableLogs() assertTrue(flushable.get().isEmpty()) // Log max number of logs before purging val beforeLog = Date() val logs = mutableListOf>() - repeat(500) { + repeat(250) { val level = Radar.RadarLogLevel.fromInt(it % 5) val message = "$it" - logBuffer.write(level, message, null) + logBuffer.write(level, null, message) logs += level to message } - assertEquals(500, logs.size) + assertEquals(250, logs.size) val afterLog = Date() // Verify the log contents - flushable = logBuffer.getFlushableLogsStash() + flushable = logBuffer.getFlushableLogs() var contents = flushable.get() - assertEquals(500, contents.size) + assertEquals(250, contents.size) contents.forEachIndexed { index, radarLog -> assertEquals(logs[index].second, radarLog.message) assertEquals(logs[index].first, radarLog.level) @@ -50,49 +55,51 @@ class RadarSimpleLogBufferTest { // Put logs back flushable.onFlush(false) // Verify the order was preserved - flushable = logBuffer.getFlushableLogsStash() + flushable = logBuffer.getFlushableLogs() contents = flushable.get() - assertEquals(500, logs.size) - assertEquals(500, contents.size) + assertEquals(250, logs.size) + assertEquals(250, contents.size) contents.forEachIndexed { index, radarLog -> assertEquals(logs[index].second, radarLog.message) assertEquals(logs[index].first, radarLog.level) } - - // Log 600 more, then put flushed logs back. This will trigger a purge. The most-recent files from the flushable + flushable.onFlush(false) + // Log 250 more, then put flushed logs back. This will trigger a purge. The most-recent files from the flushable // contents should return to the log buffer. - repeat(500) { + repeat(250) { val level = Radar.RadarLogLevel.fromInt(it % 5) - val message = "$it" - logBuffer.write(level, message, null) + val newVal = it+250 + val message = "$newVal" + logBuffer.write(level, null, message) logs += level to message } - flushable.onFlush(false) - flushable = logBuffer.getFlushableLogsStash() + + flushable = logBuffer.getFlushableLogs() contents = flushable.get() - assertEquals(1000, logs.size) - assertEquals(1000, contents.size) + assertEquals(500, logs.size) + assertEquals(500, contents.size) flushable.onFlush(false) // One more log will cause a purge val level = Radar.RadarLogLevel.DEBUG var message = UUID.randomUUID().toString() - logBuffer.write(level, message, null) - flushable = logBuffer.getFlushableLogsStash() + logBuffer.write(level, null, message) + flushable = logBuffer.getFlushableLogs() contents = flushable.get() - // There should be 502 logs remaining - the extras are from the purge message and the log that was being written. - assertEquals(502, contents.size) - contents.take(500).forEachIndexed { index, radarLog -> - assertEquals(logs[index + 500].second, radarLog.message) - assertEquals(logs[index + 500].first, radarLog.level) + // There should be 252 logs remaining - the extras are from the purge message and the log that was being written. + assertEquals(252, contents.size) + contents.take(250).forEachIndexed { index, radarLog -> + assertEquals(logs[index + 250].second, radarLog.message) + assertEquals(logs[index + 250].first, radarLog.level) } - assertEquals("----- purged oldest logs -----", contents[500].message) - assertEquals(message, contents[501].message) + assertEquals(message, contents[250].message) + assertEquals("----- purged oldest logs -----", contents[251].message) + // Test behavior of successful log flush message = UUID.randomUUID().toString() - logBuffer.write(Radar.RadarLogLevel.DEBUG, message, Radar.RadarLogType.SDK_CALL) + logBuffer.write(Radar.RadarLogLevel.DEBUG, Radar.RadarLogType.SDK_CALL, message) flushable.onFlush(true) - flushable = logBuffer.getFlushableLogsStash() + flushable = logBuffer.getFlushableLogs() contents = flushable.get() assertEquals(1, contents.size) assertEquals(message, contents[0].message)