Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.6.1'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
implementation "androidx.datastore:datastore-preferences:${versions.dataStore}"
}
25 changes: 18 additions & 7 deletions app/src/main/java/com/steamclock/steamclogsample/App.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.steamclock.steamclogsample

import android.app.Application
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import com.steamclock.steamclog.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* steamclog
Expand All @@ -10,13 +18,16 @@ import com.steamclock.steamclog.*
class App : Application() {
override fun onCreate() {
super.onCreate()
clog.initWith(Config(
isDebug = BuildConfig.DEBUG,
fileWritePath = externalCacheDir,
autoRotateConfig = AutoRotateConfig(10L), // Short rotate so we can more easily test
filtering = appFiltering,
detailedLogsOnUserReports = true
))
clog.initWith(
context = applicationContext,
config = Config(
isDebug = BuildConfig.DEBUG,
fileWritePath = externalCacheDir,
autoRotateConfig = AutoRotateConfig(10L), // Short rotate so we can more easily test
filtering = App.appFiltering,
detailedLogsOnUserReports = true
)
)
}

companion object {
Expand Down
81 changes: 78 additions & 3 deletions app/src/main/java/com/steamclock/steamclogsample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@ import android.view.View
import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.steamclock.steamclog.*
import com.steamclock.steamclogsample.databinding.ActivityMainBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date

import java.util.*

class MainActivity : AppCompatActivity() {

Expand Down Expand Up @@ -60,6 +71,14 @@ class MainActivity : AppCompatActivity() {
binding.enableExtraConfigInfo.setOnCheckedChangeListener { _, checked ->
clog.config.extraInfo = if (checked) { this::getExtraInfo } else null
}
binding.forceInvalidPathCheck.setOnCheckedChangeListener { _, checked ->
val filePath = if (checked) File("Idontexist") else externalCacheDir
val updatedConfig = updateConfigFilePath(filePath)
clog.initWith(
context = applicationContext,
config = updatedConfig
)
}

binding.addUserId.setOnClickListener { clog.setUserId("1234") }

Expand Down Expand Up @@ -106,13 +125,32 @@ class MainActivity : AppCompatActivity() {
clog.warn("LogLevel changed to ${clog.config.logLevel.title}")
}
}

// Testing app DataStore to make sure having a Steamclog datastore won't interfere with an
// app's local DataStore; this test won't be visible in app, devs can verify datastore read/write
// in console logs after the app is launched.
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
// Doesn't need to be lifecycle aware for this test
val timestamp =
AppDataStore(baseContext).apply {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious to know why baseContext is passed in here here, but applicationContext is passed into SClogDataStore

Copy link
Collaborator Author

@ssawchenko ssawchenko Aug 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm. Honestly I think that was a discrepancy I missed. Because we're using the context to lookup the datastore instance I don't think it matters which we use (although I could be wrong), but you're right, to be safe we should probably ensure we are using the same context. The examples I have found all use Context, but seem to be passing in the application context. To make this completely explicit I have changed my code to request the Application instead of the Context (Application is a ContextWrapper); I don't know if there's ramifications to this, but it seems to behave the same.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing in Application is the right call. Activity Context is tied to the Activity lifecycle, so we don't want any potential issues from carrying out operations tied to an Activity lifecycle.

var testValueBefore = getTestValue.firstOrNull()
setTestValue("UpdatedValue @ ${Date().time}")
var testValueAfter = getTestValue.firstOrNull()
clog.debug("Testing app DataStore functionality")
clog.debug("Before: $testValueBefore, After: $testValueAfter")
}
}
}
}
}

private fun showMessageIfCrashReportingNotEnabled() {
if (clog.config.logLevel.remote == LogLevel.None) {
Toast.makeText(applicationContext,
"Set Log Level to Release or Release Advanced to enable crash reporting",
Toast.LENGTH_LONG).show()

}
}

Expand Down Expand Up @@ -245,6 +283,21 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(applicationContext, "Copied to clipboard", Toast.LENGTH_LONG).show()
}

private fun updateConfigFilePath(newFileWritePath: File?): Config {
// Only the fileWrite path is changed, everything else retains current config values.
return Config(
isDebug = clog.config.isDebug,
fileWritePath = newFileWritePath,
keepLogsForDays = clog.config.keepLogsForDays,
autoRotateConfig = clog.config.autoRotateConfig,
requireRedacted = clog.config.requireRedacted,
filtering = clog.config.filtering,
logLevel = clog.config.logLevel,
detailedLogsOnUserReports = clog.config.detailedLogsOnUserReports,
extraInfo = clog.config.extraInfo
)
}

// Test logging objects
class RedactableParent : Any(), Redactable {
val safeProp = "name"
Expand Down Expand Up @@ -272,4 +325,26 @@ class MainActivity : AppCompatActivity() {
object TestButtonPressed: AnalyticEvent("test_button_pressed", mapOf())
object TestButtonPressedWithRedactable: AnalyticEvent("test_button_pressed", mapOf("redactableObject" to RedactableChild()))
}
}

/**
* Verifying that we can have a DataStore in the app "separate" from the Steamclog DataStore
*/
private class AppDataStore(private val context: Context) {
companion object {
private val Context.AppDataStore: DataStore<Preferences> by preferencesDataStore(name = "AppDataStore")
private val testKey = stringPreferencesKey("testKey")
}

/**
* hasReportedFilepathError indicates if Steamclog has reported a Sentry error regarding
* it's inability to use the given filePath to store logs.
*/
val getTestValue: Flow<String>
get() = context.AppDataStore.data.map {
it[testKey] ?: "DefaultText"
}
suspend fun setTestValue(value: String) {
context.AppDataStore.edit { it[testKey] = value }
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<CheckBox
android:text="Force invalid file write path"
android:id="@+id/force_invalid_path_check"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<View
android:layout_width="match_parent"
android:layout_height="20dp" />
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ buildscript {
"compileSdk": 33,
"kotlin": "1.8.10",
"timber": "5.0.1",
"sentry": "6.23.0"
"sentry": "6.23.0",
"dataStore" : "1.0.0"
]

repositories {
Expand Down
1 change: 1 addition & 0 deletions steamclog/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ dependencies {
implementation "com.jakewharton.timber:timber:${versions.timber}"
// https://github.com/getsentry/sentry-java/releases
implementation "io.sentry:sentry-android:${versions.sentry}"
implementation "androidx.datastore:datastore-preferences:${versions.dataStore}"
}
32 changes: 32 additions & 0 deletions steamclog/src/main/java/com/steamclock/steamclog/SClogDataStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.steamclock.steamclog

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.preferencesDataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* https://developer.android.com/topic/libraries/architecture/datastore
*/
class SClogDataStore(private val context: Context) {
companion object {
private val Context.SClogDataStore: DataStore<Preferences> by preferencesDataStore(name = "SClogDataStore")
private val hasReportedFilepathErrorKey = booleanPreferencesKey("has_logged_file_creation_failure")
}

/**
* hasReportedFilepathError indicates if Steamclog has reported a Sentry error regarding
* it's inability to use the given filePath to store logs.
*/
val getHasReportedFilepathError: Flow<Boolean>
get() = context.SClogDataStore.data.map {
it[hasReportedFilepathErrorKey] ?: false
}
suspend fun setHasReportedFilepathError(value: Boolean) {
context.SClogDataStore.edit { it[hasReportedFilepathErrorKey] = value }
}
}
39 changes: 35 additions & 4 deletions steamclog/src/main/java/com/steamclock/steamclog/Steamclog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

package com.steamclock.steamclog

import android.content.Context
import io.sentry.Sentry
import io.sentry.protocol.User
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.NonNls
import timber.log.Timber
import java.io.File
Expand Down Expand Up @@ -48,13 +51,41 @@ object SteamcLog {
// Don't plant yet; fileWritePath required before we can start writing to ExternalLogFileDestination
}

fun initWith(config: Config) {
fun initWith(context: Context, config: Config) {
this.config = config
this.config.fileWritePath?.let {

// Setup ExternalLogFileDestination
if (this.config.fileWritePath != null && this.config.fileWritePath!!.exists()) {
updateTree(externalLogFileTree, true)
} ?: run {
logInternal(LogLevel.Warn, "fileWritePath given was null; cannot log to external file")
} else {
// We have seen issues where some devices are failing to support some of the default file
// paths (ie. externalCacheDir); the code below attempts to catch that case and report it
// proactively to Sentry as an error once per app install.
val sentryErrorTitle = "Steamclog could not create external logs"
logInternal(LogLevel.Info, "File path ${this.config.fileWritePath} is invalid")

try {
// Attempt to log error only once to avoid overwhelming Sentry.
// runBlocking usage was recommended in the google docs as the way to synchronously
// call the datastore methods; we need to use this with caution, but
// https://developer.android.com/topic/libraries/architecture/datastore#synchronous
val dataStore = SClogDataStore(context)
val alreadyReportedFailure = runBlocking { dataStore.getHasReportedFilepathError.first() }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause whoever calls initWith() to be blocked.
Can we instead create a new coroutine scope (eg. something like SupervisorJob() + Dispatchers.IO) and wrap the whole try-catch inside a suspend function that can be called inside a coroutine launched by the newly created coroutine scope?
eg.

newCoroutineScope.launch { doInitWith() }
....
private suspend fun doInitWith() { try-catch etc.... }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I didn't make the entire method suspend (Timber documentation for planting/updating trees has historically been called from the main thread I believe, at least it's been that way since first implementation)... but I did move the code that checked for file access errors and called runBlocking on the dataStore to be in a new coroutine scope.

val isSentryEnabled = clog.config.logLevel.remote != LogLevel.None

if (isSentryEnabled && !alreadyReportedFailure) {
logInternal(LogLevel.Error, sentryErrorTitle)
runBlocking { dataStore.setHasReportedFilepathError(true) }
} else {
logInternal(LogLevel.Warn, sentryErrorTitle)
}
} catch (e: Exception) {
logInternal(LogLevel.Info, "Could not read local DataStore")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safeguarding against any error that might occur with DataStore; since this is the first I've worked with it I want to make sure that failures with it do cause the app to crash.

logInternal(LogLevel.Info, e.message ?: "No error message given")
logInternal(LogLevel.Error, sentryErrorTitle)
}
}

logInternal(LogLevel.Info, "Steamclog initialized:\n$this")
}

Expand Down