Logger for debug logs

pull/300/head
Koitharu 3 years ago
parent 6b08074a70
commit c09dd92cff
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -0,0 +1,128 @@
package org.koitharu.kotatsu.core.logs
import android.content.Context
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.subdir
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.ConcurrentLinkedQueue
private const val DIR = "logs"
private const val FLUSH_DELAY = 2_000L
private const val MAX_SIZE_BYTES = 1024 * 1024 // 1 MB
class FileLogger(
context: Context,
private val settings: AppSettings,
name: String,
) {
val file by lazy {
val dir = context.getExternalFilesDir(DIR) ?: context.filesDir.subdir(DIR)
File(dir, "$name.log")
}
val isEnabled: Boolean
get() = settings.isLoggingEnabled
private val dateFormat = SimpleDateFormat.getDateTimeInstance(
SimpleDateFormat.SHORT,
SimpleDateFormat.SHORT,
Locale.ROOT,
)
private val buffer = ConcurrentLinkedQueue<String>()
private val mutex = Mutex()
private var flushJob: Job? = null
fun log(message: String, e: Throwable? = null) {
if (!isEnabled) {
return
}
val text = buildString {
append(dateFormat.format(Date()))
append(": ")
if (e != null) {
append("E!")
}
append(message)
if (e != null) {
append(' ')
append(e.stackTraceToString())
appendLine()
}
}
buffer.add(text)
postFlush()
}
suspend fun flush() {
if (!isEnabled) {
return
}
flushJob?.cancelAndJoin()
flushImpl()
}
private fun postFlush() {
if (flushJob?.isActive == true) {
return
}
flushJob = processLifecycleScope.launch(Dispatchers.Default) {
delay(FLUSH_DELAY)
runCatchingCancellable {
flushImpl()
}.onFailure {
it.printStackTraceDebug()
}
}
}
private suspend fun flushImpl() {
mutex.withLock {
if (buffer.isEmpty()) {
return
}
runInterruptible(Dispatchers.IO) {
if (file.length() > MAX_SIZE_BYTES) {
rotate()
}
FileOutputStream(file, true).use {
while (true) {
val message = buffer.poll() ?: break
it.write(message.toByteArray())
it.write('\n'.code)
}
it.flush()
}
}
}
}
@WorkerThread
private fun rotate() {
val length = file.length()
val bakFile = File(file.parentFile, file.name + ".bak")
file.renameTo(bakFile)
bakFile.inputStream().use { input ->
input.skip(length - MAX_SIZE_BYTES / 2)
file.outputStream().use { output ->
input.copyTo(output)
output.flush()
}
}
bakFile.delete()
}
}

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.logs
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TrackerLogger

@ -0,0 +1,31 @@
package org.koitharu.kotatsu.core.logs
import android.content.Context
import androidx.collection.arraySetOf
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import org.koitharu.kotatsu.core.prefs.AppSettings
@Module
@InstallIn(SingletonComponent::class)
object LoggersModule {
@Provides
@TrackerLogger
fun provideTrackerLogger(
@ApplicationContext context: Context,
settings: AppSettings,
) = FileLogger(context, settings, "tracker")
@Provides
@ElementsIntoSet
fun provideAllLoggers(
@TrackerLogger trackerLogger: FileLogger,
): Set<@JvmSuppressWildcards FileLogger> = arraySetOf(
trackerLogger,
)
}

@ -159,6 +159,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getString(KEY_APP_PASSWORD, null) get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) } set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) }
val isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
var isBiometricProtectionEnabled: Boolean var isBiometricProtectionEnabled: Boolean
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true) get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) } set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
@ -371,6 +374,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SHELF_SECTIONS = "shelf_sections_2" const val KEY_SHELF_SECTIONS = "shelf_sections_2"
const val KEY_PREFETCH_CONTENT = "prefetch_content" const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale" const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
const val KEY_LOGS_SHARE = "logs_share"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

@ -38,7 +38,10 @@ class MainNavigationDelegate(
} }
override fun onNavigationItemReselected(item: MenuItem) { override fun onNavigationItemReselected(item: MenuItem) {
val fragment = fragmentManager.findFragmentByTag(TAG_PRIMARY) as? RecyclerViewOwner ?: return val fragment = fragmentManager.findFragmentByTag(TAG_PRIMARY)
if (fragment == null || fragment !is RecyclerViewOwner || fragment.view == null) {
return
}
val recyclerView = fragment.recyclerView val recyclerView = fragment.recyclerView
recyclerView.smoothScrollToPosition(0) recyclerView.smoothScrollToPosition(0)
} }

@ -78,6 +78,7 @@ class SettingsActivity :
startActivity(intent) startActivity(intent)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@ -132,6 +133,7 @@ class SettingsActivity :
ACTION_SOURCE -> SourceSettingsFragment.newInstance( ACTION_SOURCE -> SourceSettingsFragment.newInstance(
intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL, intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL,
) )
ACTION_MANAGE_SOURCES -> SourcesSettingsFragment() ACTION_MANAGE_SOURCES -> SourcesSettingsFragment()
else -> SettingsHeadersFragment() else -> SettingsHeadersFragment()
} }

@ -7,16 +7,24 @@ import androidx.core.net.toUri
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ShareHelper
import javax.inject.Inject
@AndroidEntryPoint
class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
private val viewModel by viewModels<AboutSettingsViewModel>() private val viewModel by viewModels<AboutSettingsViewModel>()
@Inject
lateinit var loggers: Set<@JvmSuppressWildcards FileLogger>
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_about) addPreferencesFromResource(R.xml.pref_about)
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run { findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
@ -39,10 +47,17 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
viewModel.checkForUpdates() viewModel.checkForUpdates()
true true
} }
AppSettings.KEY_APP_TRANSLATION -> { AppSettings.KEY_APP_TRANSLATION -> {
openLink(getString(R.string.url_weblate), preference.title) openLink(getString(R.string.url_weblate), preference.title)
true true
} }
AppSettings.KEY_LOGS_SHARE -> {
ShareHelper(preference.context).shareLogs(loggers)
true
}
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }

@ -12,14 +12,34 @@ import androidx.core.content.ContextCompat
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.work.* import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit import kotlinx.coroutines.Deferred
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -31,6 +51,7 @@ import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.ext.trySetForeground
import java.util.concurrent.TimeUnit
@HiltWorker @HiltWorker
class TrackWorker @AssistedInject constructor( class TrackWorker @AssistedInject constructor(
@ -39,6 +60,7 @@ class TrackWorker @AssistedInject constructor(
private val coil: ImageLoader, private val coil: ImageLoader,
private val settings: AppSettings, private val settings: AppSettings,
private val tracker: Tracker, private val tracker: Tracker,
@TrackerLogger private val logger: FileLogger,
) : CoroutineWorker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
private val notificationManager by lazy { private val notificationManager by lazy {
@ -46,6 +68,20 @@ class TrackWorker @AssistedInject constructor(
} }
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
logger.log("doWork()")
try {
return doWorkImpl()
} catch (e: Throwable) {
logger.log("fatal", e)
throw e
} finally {
withContext(NonCancellable) {
logger.flush()
}
}
}
private suspend fun doWorkImpl(): Result {
if (!settings.isTrackerEnabled) { if (!settings.isTrackerEnabled) {
return Result.success(workDataOf(0, 0)) return Result.success(workDataOf(0, 0))
} }
@ -53,6 +89,7 @@ class TrackWorker @AssistedInject constructor(
trySetForeground() trySetForeground()
} }
val tracks = tracker.getAllTracks() val tracks = tracker.getAllTracks()
logger.log("Total ${tracks.size} tracks")
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
return Result.success(workDataOf(0, 0)) return Result.success(workDataOf(0, 0))
} }
@ -70,6 +107,7 @@ class TrackWorker @AssistedInject constructor(
success++ success++
} }
} }
logger.log("Result: success: $success, failed: $failed")
val resultData = workDataOf(success, failed) val resultData = workDataOf(success, failed)
return if (success == 0 && failed != 0) { return if (success == 0 && failed != 0) {
Result.failure(resultData) Result.failure(resultData)
@ -85,6 +123,8 @@ class TrackWorker @AssistedInject constructor(
async(dispatcher) { async(dispatcher) {
runCatchingCancellable { runCatchingCancellable {
tracker.fetchUpdates(track, commit = true) tracker.fetchUpdates(track, commit = true)
}.onFailure {
logger.log("checkUpdatesAsync", it)
}.onSuccess { updates -> }.onSuccess { updates ->
if (updates.isValid && updates.isNotEmpty()) { if (updates.isValid && updates.isNotEmpty()) {
showNotification( showNotification(

@ -4,10 +4,11 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import java.io.File
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.io.File
private const val TYPE_TEXT = "text/plain" private const val TYPE_TEXT = "text/plain"
private const val TYPE_IMAGE = "image/*" private const val TYPE_IMAGE = "image/*"
@ -79,4 +80,15 @@ class ShareHelper(private val context: Context) {
.setChooserTitle(R.string.share) .setChooserTitle(R.string.share)
.startChooser() .startChooser()
} }
fun shareLogs(loggers: Collection<FileLogger>) {
val intentBuilder = ShareCompat.IntentBuilder(context)
.setType(TYPE_TEXT)
for (logger in loggers) {
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logger.file)
intentBuilder.addStream(uri)
}
intentBuilder.setChooserTitle(R.string.share_logs)
intentBuilder.startChooser()
}
} }

@ -402,4 +402,7 @@
<string name="prefetch_content">Content preloading</string> <string name="prefetch_content">Content preloading</string>
<string name="mark_as_current">Mark as current</string> <string name="mark_as_current">Mark as current</string>
<string name="language">Language</string> <string name="language">Language</string>
<string name="share_logs">Share logs</string>
<string name="enable_logging">Enable logging</string>
<string name="enable_logging_summary">Record some actions for debug purposes</string>
</resources> </resources>

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/app_name"> <PreferenceCategory android:title="@string/app_name">
@ -9,10 +10,22 @@
android:persistent="false" android:persistent="false"
android:summary="@string/check_for_updates" /> android:summary="@string/check_for_updates" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="logging"
android:summary="@string/enable_logging_summary"
android:title="@string/enable_logging" />
<Preference
android:dependency="logging"
android:key="logs_share"
android:title="@string/share_logs" />
<Preference <Preference
android:key="about_app_translation" android:key="about_app_translation"
android:summary="@string/about_app_translation_summary" android:summary="@string/about_app_translation_summary"
android:title="@string/about_app_translation" /> android:title="@string/about_app_translation"
app:allowDividerAbove="true" />
<org.koitharu.kotatsu.settings.utils.AboutLinksPreference /> <org.koitharu.kotatsu.settings.utils.AboutLinksPreference />

Loading…
Cancel
Save