diff --git a/CHANGELOG.md b/CHANGELOG.md index 170d9adb..06353d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ Change Log ========== +## Version 1.3.0 + +_2023-07-21_ + +* Fix log file provider authorities. + +## Version 1.2.9 + +_2023-07-18_ + +* Update dependencies. +* Persist logs to disk. + ## Version 1.2.8 _2023-05-28_ diff --git a/README.md b/README.md index 787deb2e..eceff256 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,15 @@ Then add the following dependencies in your app `build.gradle` or `build.gradle. **Groovy** ```groovy -debugImplementation "com.infinum.sentinel:sentinel:1.2.8" -releaseImplementation "com.infinum.sentinel:sentinel-no-op:1.2.8" +debugImplementation "com.infinum.sentinel:sentinel:1.3.0" +releaseImplementation "com.infinum.sentinel:sentinel-no-op:1.3.0" ``` **KotlinDSL** ```kotlin -debugImplementation("com.infinum.sentinel:sentinel:1.2.8") -releaseImplementation("com.infinum.sentinel:sentinel-no-op:1.2.8") +debugImplementation("com.infinum.sentinel:sentinel:1.3.0") +releaseImplementation("com.infinum.sentinel:sentinel-no-op:1.3.0") ``` Basic tools are provided inside the main package but depending on requirements you might want to add @@ -74,27 +74,27 @@ specific tools: **Groovy** ```groovy -debugImplementation "com.infinum.sentinel:tool-chucker:1.2.8" -debugImplementation "com.infinum.sentinel:tool-collar:1.2.8" -debugImplementation "com.infinum.sentinel:tool-dbinspector:1.2.8" -debugImplementation "com.infinum.sentinel:tool-leakcanary:1.2.8" -debugImplementation "com.infinum.sentinel:tool-appgallery:1.2.8" -debugImplementation "com.infinum.sentinel:tool-googleplay:1.2.8" -debugImplementation "com.infinum.sentinel:tool-thimble:1.2.8" -debugImplementation "com.infinum.sentinel:tool-timber:1.2.8" +debugImplementation "com.infinum.sentinel:tool-chucker:1.3.0" +debugImplementation "com.infinum.sentinel:tool-collar:1.3.0" +debugImplementation "com.infinum.sentinel:tool-dbinspector:1.3.0" +debugImplementation "com.infinum.sentinel:tool-leakcanary:1.3.0" +debugImplementation "com.infinum.sentinel:tool-appgallery:1.3.0" +debugImplementation "com.infinum.sentinel:tool-googleplay:1.3.0" +debugImplementation "com.infinum.sentinel:tool-thimble:1.3.0" +debugImplementation "com.infinum.sentinel:tool-timber:1.3.0" ``` **KotlinDSL** ```kotlin -debugImplementation("com.infinum.sentinel:tool-chucker:1.2.8") -debugImplementation("com.infinum.sentinel:tool-collar:1.2.8") -debugImplementation("com.infinum.sentinel:tool-dbinspector:1.2.8") -debugImplementation("com.infinum.sentinel:tool-leakcanary:1.2.8") -debugImplementation("com.infinum.sentinel:tool-appgallery:1.2.8") -debugImplementation("com.infinum.sentinel:tool-googleplay:1.2.8") -debugImplementation("com.infinum.sentinel:tool-thimble:1.2.8") -debugImplementation("com.infinum.sentinel:tool-timber:1.2.8") +debugImplementation("com.infinum.sentinel:tool-chucker:1.3.0") +debugImplementation("com.infinum.sentinel:tool-collar:1.3.0") +debugImplementation("com.infinum.sentinel:tool-dbinspector:1.3.0") +debugImplementation("com.infinum.sentinel:tool-leakcanary:1.3.0") +debugImplementation("com.infinum.sentinel:tool-appgallery:1.3.0") +debugImplementation("com.infinum.sentinel:tool-googleplay:1.3.0") +debugImplementation("com.infinum.sentinel:tool-thimble:1.3.0") +debugImplementation("com.infinum.sentinel:tool-timber:1.3.0") ``` Now you can sync your project. diff --git a/config.gradle b/config.gradle index 19c8275a..3c05f277 100644 --- a/config.gradle +++ b/config.gradle @@ -1,7 +1,7 @@ ext { def major = 1 - def minor = 2 - def patch = 8 + def minor = 3 + def patch = 0 buildConfig = [ "minSdk" : 21, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e27a19e..01168e58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -sentinel = "1.2.8" -gradle = "8.0.1" +sentinel = "1.3.0" +gradle = "8.0.2" desugar = "2.0.3" kotlin = "1.8.21" coroutines = "1.7.1" @@ -25,7 +25,7 @@ ktlintplugin = "11.3.1" ktlint = "0.45.2" cpd = "3.3" dokka = "1.8.10" -chucker = "3.5.2" +chucker = "4.0.0" collar = "1.4.0" dbinspector = "5.4.9" leakcanary = "2.11" diff --git a/sample/build.gradle b/sample/build.gradle index da924821..452b2a59 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -68,9 +68,17 @@ dependencies { implementation libs.material implementation libs.timber - debugImplementation libs.library -// debugImplementation project(":sentinel") +// debugImplementation libs.library + debugImplementation project(":sentinel") releaseImplementation libs.librarynoop - debugImplementation libs.bundles.tools +// debugImplementation libs.bundles.tools + debugImplementation project(":tool-appgallery") + debugImplementation project(":tool-chucker") + debugImplementation project(":tool-collar") + debugImplementation project(":tool-dbinspector") + debugImplementation project(":tool-googleplay") + debugImplementation project(":tool-leakcanary") + debugImplementation project(":tool-thimble") + debugImplementation project(":tool-timber") } diff --git a/sentinel/src/main/kotlin/com/infinum/sentinel/ui/main/SentinelFragment.kt b/sentinel/src/main/kotlin/com/infinum/sentinel/ui/main/SentinelFragment.kt index 4f83d830..58762bdf 100644 --- a/sentinel/src/main/kotlin/com/infinum/sentinel/ui/main/SentinelFragment.kt +++ b/sentinel/src/main/kotlin/com/infinum/sentinel/ui/main/SentinelFragment.kt @@ -22,7 +22,8 @@ import com.infinum.sentinel.ui.shared.edgetreatment.ScissorsEdgeTreatment @Suppress("TooManyFunctions") @RestrictTo(RestrictTo.Scope.LIBRARY) -internal class SentinelFragment : BaseFragment(R.layout.sentinel_fragment) { +internal class SentinelFragment : + BaseFragment(R.layout.sentinel_fragment) { companion object { const val TAG: String = "SentinelFragment" @@ -59,8 +60,8 @@ internal class SentinelFragment : BaseFragment(R.l override fun onEvent(event: SentinelEvent) = when (event) { - is SentinelEvent.Formatted -> ShareCompat.IntentBuilder(requireActivity()) - .shareText(event.value) + is SentinelEvent.Formatted -> + ShareCompat.IntentBuilder(requireActivity()).shareText(event.value) } private fun setupToolbar() { diff --git a/sentinel/src/main/res/values/strings.xml b/sentinel/src/main/res/values/strings.xml index 7a776132..166a0584 100644 --- a/sentinel/src/main/res/values/strings.xml +++ b/sentinel/src/main/res/values/strings.xml @@ -114,7 +114,7 @@ Priority Message - No logs collected. + No new logs collected. Certificate No certificates collected. diff --git a/tool-timber/build.gradle b/tool-timber/build.gradle index ebae7a0a..67a33d0f 100644 --- a/tool-timber/build.gradle +++ b/tool-timber/build.gradle @@ -67,6 +67,7 @@ dokkaJavadoc { } dependencies { + coreLibraryDesugaring libs.desugar implementation libs.kotlin.core implementation libs.coroutines api libs.library diff --git a/tool-timber/src/main/AndroidManifest.xml b/tool-timber/src/main/AndroidManifest.xml index de3456ff..13652de9 100644 --- a/tool-timber/src/main/AndroidManifest.xml +++ b/tool-timber/src/main/AndroidManifest.xml @@ -14,6 +14,17 @@ android:value="false" /> + + + + + + + + diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelTree.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelFileTree.kt similarity index 57% rename from tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelTree.kt rename to tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelFileTree.kt index 787ef34e..b09cb455 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelTree.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelFileTree.kt @@ -1,30 +1,42 @@ package com.infinum.sentinel +import android.content.Context import com.infinum.sentinel.ui.logger.models.BaseEntry import com.infinum.sentinel.ui.logger.models.FlowBuffer import com.infinum.sentinel.ui.logger.models.Level +import com.infinum.sentinel.ui.shared.LogFileResolver +import java.io.File +import java.util.Calendar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -internal class SentinelTree( +internal class SentinelFileTree( + context: Context, val buffer: FlowBuffer ) : Timber.DebugTree() { + private val logFileResolver = LogFileResolver(context) + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { MainScope().launch { withContext(Dispatchers.IO) { - buffer.enqueue( - Entry( - Level.forLogLevel(priority), - System.currentTimeMillis(), - tag, - message, - t?.stackTraceToString() - ) + val entry = Entry( + Level.forLogLevel(priority), + System.currentTimeMillis(), + tag, + message, + t?.stackTraceToString() ) + + buffer.enqueue(entry) + + val file: File = logFileResolver.createOrOpenFile() + val line = entry.asLineString() + + file.appendText(line) } } } diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelLogsProvider.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelLogsProvider.kt new file mode 100644 index 00000000..861a6284 --- /dev/null +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/SentinelLogsProvider.kt @@ -0,0 +1,5 @@ +package com.infinum.sentinel + +import androidx.core.content.FileProvider + +internal class SentinelLogsProvider : FileProvider(R.xml.sentinel_file_paths) diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/TimberInitializer.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/TimberInitializer.kt index f312be2b..8125b821 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/TimberInitializer.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/TimberInitializer.kt @@ -11,7 +11,8 @@ public class TimberInitializer : Initializer> { override fun create(context: Context): Class { Timber.plant( - SentinelTree( + SentinelFileTree( + context, FlowBuffer() ) ) diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerActivity.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerActivity.kt index 096f5711..754a5d8c 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerActivity.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerActivity.kt @@ -1,13 +1,16 @@ package com.infinum.sentinel.ui.logger +import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration +import android.net.Uri import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isGone import androidx.core.view.isVisible @@ -15,12 +18,15 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.infinum.sentinel.R -import com.infinum.sentinel.SentinelTree +import com.infinum.sentinel.SentinelFileTree import com.infinum.sentinel.databinding.SentinelActivityLoggerBinding import com.infinum.sentinel.ui.logger.models.FlowBuffer import com.infinum.sentinel.ui.logger.storage.AllowedTags +import com.infinum.sentinel.ui.logs.LogsActivity import com.infinum.sentinel.ui.shared.BounceEdgeEffectFactory +import com.infinum.sentinel.ui.shared.LogFileResolver import com.infinum.sentinel.ui.shared.setup +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn @@ -30,6 +36,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber + public class LoggerActivity : AppCompatActivity() { private companion object { @@ -38,8 +45,10 @@ public class LoggerActivity : AppCompatActivity() { private lateinit var binding: SentinelActivityLoggerBinding + private lateinit var logFile: File + private val buffer = Timber.forest() - .filterIsInstance() + .filterIsInstance() .firstOrNull()?.buffer ?: FlowBuffer() @@ -49,7 +58,7 @@ public class LoggerActivity : AppCompatActivity() { }, onClick = { ShareCompat.IntentBuilder(this) - .setText(it.asString()) + .setText(it.asJSONString()) .setType(MIME_TYPE_TEXT) .startChooser() } @@ -102,18 +111,18 @@ public class LoggerActivity : AppCompatActivity() { toolbar.setOnMenuItemClickListener { when (it.itemId) { R.id.search -> { - toolbar.menu.findItem(R.id.clear).isVisible = false + toolbar.menu.findItem(R.id.logs).isVisible = false toolbar.menu.findItem(R.id.share).isVisible = false true } - R.id.clear -> { - clearLogger() + R.id.logs -> { + showLogs() true } R.id.share -> { - shareAll() + shareToday() true } @@ -123,7 +132,7 @@ public class LoggerActivity : AppCompatActivity() { (toolbar.menu.findItem(R.id.search)?.actionView as? SearchView)?.setup( hint = getString(R.string.sentinel_search), onSearchClosed = { - toolbar.menu.findItem(R.id.clear).isVisible = true + toolbar.menu.findItem(R.id.logs).isVisible = true toolbar.menu.findItem(R.id.share).isVisible = true data() }, @@ -146,6 +155,10 @@ public class LoggerActivity : AppCompatActivity() { ) } + val logFileResolver = LogFileResolver(this) + logFile = logFileResolver.createOrOpenFile() + binding.logNameView.text = logFile.name + data() } @@ -168,18 +181,25 @@ public class LoggerActivity : AppCompatActivity() { lifecycleScope.launch { withContext(Dispatchers.IO) { buffer.filter(query) } } } - private fun clearLogger() { - lifecycleScope.launch { withContext(Dispatchers.IO) { buffer.clear() } } + private fun showLogs() { + startActivity(Intent(this, LogsActivity::class.java)) } - private fun shareAll() { + private fun shareToday() { lifecycleScope.launch { - val text = withContext(Dispatchers.IO) { - buffer.asString() + val uri: Uri = withContext(Dispatchers.IO) { + FileProvider.getUriForFile( + this@LoggerActivity, + "com.infinum.sentinel.logprovider", + logFile + ) } ShareCompat.IntentBuilder(this@LoggerActivity) - .setText(text) + .addStream(uri) .setType(MIME_TYPE_TEXT) + .apply { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } .startChooser() } } @@ -189,4 +209,4 @@ public class LoggerActivity : AppCompatActivity() { recyclerView.isGone = isEmpty emptyStateLayout.isVisible = isEmpty } -} \ No newline at end of file +} diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerAdapter.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerAdapter.kt index 77242db2..2f49dc15 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerAdapter.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerAdapter.kt @@ -3,13 +3,13 @@ package com.infinum.sentinel.ui.logger import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter -import com.infinum.sentinel.SentinelTree +import com.infinum.sentinel.SentinelFileTree import com.infinum.sentinel.databinding.SentinelItemLogBinding internal class LoggerAdapter( private val onListChanged: (Boolean) -> Unit, - private val onClick: (SentinelTree.Entry) -> Unit -) : ListAdapter(LoggerDiffUtil()) { + private val onClick: (SentinelFileTree.Entry) -> Unit +) : ListAdapter(LoggerDiffUtil()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LoggerViewHolder = LoggerViewHolder( @@ -29,8 +29,8 @@ internal class LoggerAdapter( } override fun onCurrentListChanged( - previousList: MutableList, - currentList: MutableList + previousList: MutableList, + currentList: MutableList ) = onListChanged(currentList.isEmpty()) } \ No newline at end of file diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerDiffUtil.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerDiffUtil.kt index dc10c710..9e344993 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerDiffUtil.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerDiffUtil.kt @@ -1,15 +1,21 @@ package com.infinum.sentinel.ui.logger import androidx.recyclerview.widget.DiffUtil -import com.infinum.sentinel.SentinelTree +import com.infinum.sentinel.SentinelFileTree -internal class LoggerDiffUtil : DiffUtil.ItemCallback() { +internal class LoggerDiffUtil : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: SentinelTree.Entry, newItem: SentinelTree.Entry): Boolean { + override fun areItemsTheSame( + oldItem: SentinelFileTree.Entry, + newItem: SentinelFileTree.Entry + ): Boolean { return oldItem.timestamp == newItem.timestamp } - override fun areContentsTheSame(oldItem: SentinelTree.Entry, newItem: SentinelTree.Entry): Boolean { + override fun areContentsTheSame( + oldItem: SentinelFileTree.Entry, + newItem: SentinelFileTree.Entry + ): Boolean { return oldItem == newItem } } diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerViewHolder.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerViewHolder.kt index 55ebd55f..a938a003 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerViewHolder.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/LoggerViewHolder.kt @@ -4,7 +4,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.infinum.sentinel.R -import com.infinum.sentinel.SentinelTree +import com.infinum.sentinel.SentinelFileTree import com.infinum.sentinel.databinding.SentinelItemLogBinding import com.infinum.sentinel.ui.logger.models.Level import java.text.SimpleDateFormat @@ -14,7 +14,7 @@ internal class LoggerViewHolder( private val binding: SentinelItemLogBinding ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: SentinelTree.Entry?, onClick: (SentinelTree.Entry) -> Unit) { + fun bind(item: SentinelFileTree.Entry?, onClick: (SentinelFileTree.Entry) -> Unit) { item?.let { entry -> with(binding) { levelView.setBackgroundColor( diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/BaseEntry.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/BaseEntry.kt index 22c78704..7ec2e2bc 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/BaseEntry.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/BaseEntry.kt @@ -1,5 +1,6 @@ package com.infinum.sentinel.ui.logger.models +import java.io.File import org.json.JSONObject internal open class BaseEntry( @@ -10,7 +11,7 @@ internal open class BaseEntry( open val stackTrace: String? = null ) { - fun asString(): String = + fun asJSONString(): String = JSONObject() .put("level", level) .put("timestamp", timestamp) @@ -18,4 +19,18 @@ internal open class BaseEntry( .put("message", message) .put("stackTrace", stackTrace) .toString() + + fun asLineString(): String = + buildString { + append(timestamp) + append(" LEVEL: ") + append(level) + append(" TAG: ") + append(tag.orEmpty()) + append(" MESSAGE: ") + append(message.orEmpty()) + append(" STACKTRACE: ") + append(stackTrace.orEmpty()) + append(System.lineSeparator()) + } } diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/FlowBuffer.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/FlowBuffer.kt index 7211cd00..d51ee6d1 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/FlowBuffer.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/FlowBuffer.kt @@ -1,12 +1,8 @@ package com.infinum.sentinel.ui.logger.models -import kotlin.coroutines.resume import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import org.json.JSONArray -import org.json.JSONObject internal class FlowBuffer { @@ -38,12 +34,4 @@ internal class FlowBuffer { } ) } - - suspend fun asString(): String = - suspendCancellableCoroutine { - val result = JSONArray( - queue.map { entry -> JSONObject(entry.asString()) }.toTypedArray() - ).toString() - it.resume(result) - } } diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/Level.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/Level.kt index 2336051b..c50a6099 100644 --- a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/Level.kt +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logger/models/Level.kt @@ -22,5 +22,16 @@ internal enum class Level { Log.WARN -> WARN else -> UNKNOWN } + + fun byLevelName(levelName: String): Level = + when (levelName) { + "ASSERT" -> ASSERT + "DEBUG" -> DEBUG + "ERROR" -> ERROR + "INFO" -> INFO + "VERBOSE" -> VERBOSE + "WARN" -> WARN + else -> UNKNOWN + } } } diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsActivity.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsActivity.kt new file mode 100644 index 00000000..032472c7 --- /dev/null +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsActivity.kt @@ -0,0 +1,162 @@ +package com.infinum.sentinel.ui.logs + +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ShareCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.infinum.sentinel.R +import com.infinum.sentinel.databinding.SentinelActivityLogsBinding +import com.infinum.sentinel.ui.shared.BounceEdgeEffectFactory +import com.infinum.sentinel.ui.shared.LogFileResolver +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +public class LogsActivity : AppCompatActivity() { + + private companion object { + private const val MIME_TYPE_TEXT = "text/plain" + } + + private lateinit var binding: SentinelActivityLogsBinding + + private val adapter = LogsAdapter( + onListChanged = { isEmpty -> + showEmptyState(isEmpty) + }, + onDelete = { + deleteLog(it) + }, + onShare = { + shareLog(it) + } + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> false + Configuration.UI_MODE_NIGHT_NO -> true + else -> null + }?.let { + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + it + } ?: run { + WindowInsetsControllerCompat( + window, + window.decorView + ).isAppearanceLightStatusBars = true + } + } else { + window.statusBarColor = ContextCompat.getColor(this, android.R.color.black) + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = + true + } + + binding = SentinelActivityLogsBinding.inflate(layoutInflater) + setContentView(binding.root) + + with(binding) { + toolbar.setNavigationOnClickListener { finish() } + toolbar.subtitle = ( + packageManager.getApplicationLabel( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + ) + } else { + @Suppress("DEPRECATION") + packageManager.getApplicationInfo( + packageName, + PackageManager.GET_META_DATA + ) + } + ) as? String + ) ?: getString(R.string.sentinel_name) + + recyclerView.layoutManager = LinearLayoutManager( + recyclerView.context, + LinearLayoutManager.VERTICAL, + false + ) + recyclerView.adapter = adapter + recyclerView.edgeEffectFactory = BounceEdgeEffectFactory() + recyclerView.addItemDecoration( + DividerItemDecoration( + recyclerView.context, + LinearLayoutManager.VERTICAL + ) + ) + } + + data() + } + + private fun data() { + LogFileResolver(this) + .logsDir() + .listFiles() + .orEmpty() + .asFlow() + .flowOn(Dispatchers.IO) + .onEach { files -> + val allFiles = adapter.currentList + files + adapter.submitList(allFiles.sortedByDescending { it.lastModified() }) + } + .launchIn(lifecycleScope) + } + + private fun deleteLog(logFile: File) { + val ok = logFile.delete() + if (ok) { + val allFiles = adapter.currentList - logFile + adapter.submitList(allFiles.sortedByDescending { it.lastModified() }) + } + } + + private fun shareLog(logFile: File) { + lifecycleScope.launch { + val uri: Uri = withContext(Dispatchers.IO) { + FileProvider.getUriForFile( + this@LogsActivity, + "com.infinum.sentinel.logprovider", + logFile + ) + } + ShareCompat.IntentBuilder(this@LogsActivity) + .addStream(uri) + .setType(MIME_TYPE_TEXT) + .apply { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + .startChooser() + } + } + + private fun showEmptyState(isEmpty: Boolean) = + with(binding) { + recyclerView.isGone = isEmpty + emptyStateLayout.isVisible = isEmpty + } +} diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsAdapter.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsAdapter.kt new file mode 100644 index 00000000..ea5b4b9b --- /dev/null +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsAdapter.kt @@ -0,0 +1,39 @@ +package com.infinum.sentinel.ui.logs + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.infinum.sentinel.SentinelFileTree +import com.infinum.sentinel.databinding.SentinelItemLogBinding +import com.infinum.sentinel.databinding.SentinelItemLogFileBinding +import java.io.File + +internal class LogsAdapter( + private val onListChanged: (Boolean) -> Unit, + private val onDelete: (File) -> Unit, + private val onShare: (File) -> Unit +) : ListAdapter(LogsDiffUtil()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LogsViewHolder = + LogsViewHolder( + SentinelItemLogFileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: LogsViewHolder, position: Int) { + holder.bind(getItem(position), onDelete, onShare) + } + + override fun onViewRecycled(holder: LogsViewHolder) { + holder.unbind() + } + + override fun onCurrentListChanged( + previousList: MutableList, + currentList: MutableList + ) = + onListChanged(currentList.isEmpty()) +} \ No newline at end of file diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsDiffUtil.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsDiffUtil.kt new file mode 100644 index 00000000..4c655bb0 --- /dev/null +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsDiffUtil.kt @@ -0,0 +1,22 @@ +package com.infinum.sentinel.ui.logs + +import androidx.recyclerview.widget.DiffUtil +import com.infinum.sentinel.SentinelFileTree +import java.io.File + +internal class LogsDiffUtil : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: File, + newItem: File + ): Boolean { + return oldItem.name == newItem.name + } + + override fun areContentsTheSame( + oldItem: File, + newItem: File + ): Boolean { + return oldItem == newItem + } +} diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsViewHolder.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsViewHolder.kt new file mode 100644 index 00000000..c9a7acf5 --- /dev/null +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/logs/LogsViewHolder.kt @@ -0,0 +1,36 @@ +package com.infinum.sentinel.ui.logs + +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.infinum.sentinel.R +import com.infinum.sentinel.SentinelFileTree +import com.infinum.sentinel.databinding.SentinelItemLogFileBinding +import com.infinum.sentinel.ui.logger.models.Level +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date + +internal class LogsViewHolder( + private val binding: SentinelItemLogFileBinding +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: File?, onDelete: (File) -> Unit, onShare: (File) -> Unit) { + item?.let { entry -> + with(binding) { + messageView.text = entry.name + timestampView.text = SimpleDateFormat.getDateTimeInstance().format(Date(entry.lastModified())) + deleteButton.setOnClickListener { onDelete(entry) } + shareButton.setOnClickListener { onShare(entry) } + } + } ?: unbind() + } + + fun unbind() = + with(binding) { + timestampView.text = null + messageView.text = null + deleteButton.setOnClickListener(null) + shareButton.setOnClickListener(null) + } +} \ No newline at end of file diff --git a/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/shared/LogFileResolver.kt b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/shared/LogFileResolver.kt new file mode 100644 index 00000000..4f3964eb --- /dev/null +++ b/tool-timber/src/main/kotlin/com/infinum/sentinel/ui/shared/LogFileResolver.kt @@ -0,0 +1,45 @@ +package com.infinum.sentinel.ui.shared + +import android.content.Context +import java.io.File +import java.util.Calendar + +internal class LogFileResolver( + private val context: Context +) { + + companion object { + private const val LOGS_DIRECTORY = "/logs" + private const val LOG_EXTENSION = ".log" + } + + private val parent = File("${context.filesDir.absolutePath}${LOGS_DIRECTORY}") + + fun logsDir() = parent + + fun createOrOpenFile(): File { + if (parent.exists().not()) { + parent.mkdirs() + } + + val day = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + val month = Calendar.getInstance().get(Calendar.MONTH) + 1 + val year = Calendar.getInstance().get(Calendar.YEAR) + + val filename = buildString { + append(String.format("%02d", day)) + append("-") + append(String.format("%02d", month)) + append("-") + append(String.format("%04d", year)) + append(LOG_EXTENSION) + } + val nowFile = File("${parent.absolutePath}/$filename") + + if (nowFile.exists().not()) { + nowFile.createNewFile() + } + + return nowFile + } +} diff --git a/tool-timber/src/main/res/drawable/sentinel_ic_logs.xml b/tool-timber/src/main/res/drawable/sentinel_ic_logs.xml new file mode 100644 index 00000000..32429876 --- /dev/null +++ b/tool-timber/src/main/res/drawable/sentinel_ic_logs.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/tool-timber/src/main/res/layout/sentinel_activity_logger.xml b/tool-timber/src/main/res/layout/sentinel_activity_logger.xml index ee82867d..000ba8d8 100644 --- a/tool-timber/src/main/res/layout/sentinel_activity_logger.xml +++ b/tool-timber/src/main/res/layout/sentinel_activity_logger.xml @@ -22,6 +22,16 @@ app:navigationIcon="@drawable/sentinel_ic_close" app:title="@string/sentinel_logger" /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tool-timber/src/main/res/layout/sentinel_item_log_file.xml b/tool-timber/src/main/res/layout/sentinel_item_log_file.xml new file mode 100644 index 00000000..747efac8 --- /dev/null +++ b/tool-timber/src/main/res/layout/sentinel_item_log_file.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tool-timber/src/main/res/menu/sentinel_logger.xml b/tool-timber/src/main/res/menu/sentinel_logger.xml index 2088cdc4..5aa93aa5 100644 --- a/tool-timber/src/main/res/menu/sentinel_logger.xml +++ b/tool-timber/src/main/res/menu/sentinel_logger.xml @@ -11,9 +11,9 @@ app:showAsAction="always" /> Sentinel Search Logger + Logs %1$s: %2$s \ No newline at end of file diff --git a/tool-timber/src/main/res/xml/sentinel_file_paths.xml b/tool-timber/src/main/res/xml/sentinel_file_paths.xml new file mode 100644 index 00000000..d3c4a1d3 --- /dev/null +++ b/tool-timber/src/main/res/xml/sentinel_file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file