Update file logging

* Add ability to send logs
* Log app exit info
* Redact sensitive info from logs
pull/3104/head
Alex Baker 1 year ago
parent c7d10f69e7
commit baadbef820

7
app/proguard.pro vendored

@ -2,13 +2,6 @@
-keep class org.tasks.** { *; }
# remove logging statements
-assumenosideeffects class timber.log.Timber* {
public static *** v(...);
public static *** d(...);
public static *** i(...);
}
# guava
-dontwarn sun.misc.Unsafe
-dontwarn java.lang.ClassValue

@ -17,16 +17,16 @@ import leakcanary.AppWatcher
import org.tasks.logging.FileLogger
import org.tasks.preferences.Preferences
import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject
class BuildSetup @Inject constructor(
private val context: Application,
private val preferences: Preferences
private val context: Application,
private val preferences: Preferences,
private val fileLogger: FileLogger,
) {
fun setup() {
Timber.plant(DebugTree())
Timber.plant(FileLogger(context))
Timber.plant(Timber.DebugTree())
Timber.plant(fileLogger)
SoLoader.init(context, false)
if (preferences.getBoolean(R.string.p_leakcanary, false)) {
AppWatcher.manualInstall(context)

@ -1,5 +1,6 @@
package org.tasks
import android.app.ActivityManager
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
@ -13,6 +14,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import androidx.work.Configuration
import com.mikepenz.iconics.Iconics
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.service.Upgrader
import dagger.Lazy
import dagger.hilt.android.HiltAndroidApp
@ -85,6 +87,11 @@ class Tasks : Application(), Configuration.Provider {
val lastVersion = preferences.lastSetVersion
val currentVersion = BuildConfig.VERSION_CODE
Timber.i("Astrid Startup. %s => %s", lastVersion, currentVersion)
if (AndroidUtilities.atLeastR()) {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val exitReasons = activityManager.getHistoricalProcessExitReasons(null, 0, 5)
Timber.i(exitReasons.joinToString("\n"))
}
// invoke upgrade service
if (lastVersion != currentVersion) {

@ -161,7 +161,6 @@ class CaldavSynchronizer @Inject constructor(
caldavDao.update(account)
}
val urls = resources.map { it.href.toString() }.toHashSet()
Timber.d("Found calendars: %s", urls)
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, ArrayList(urls))) {
taskDeleter.delete(calendar)
}
@ -239,7 +238,7 @@ class CaldavSynchronizer @Inject constructor(
val httpUrl = resource.href
val remoteCtag = resource.ctag
if (caldavCalendar.ctag?.equals(remoteCtag) == true) {
Timber.d("%s up to date", caldavCalendar.name)
Timber.d("up to date: $caldavCalendar")
return
}
Timber.d("updating $caldavCalendar")

@ -1,29 +1,46 @@
package org.tasks.logging
import android.content.Context
import android.annotation.SuppressLint
import android.app.Application
import android.os.Process
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.logging.LogFormatter.Companion.LINE_SEPARATOR
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.logging.FileHandler
import java.util.logging.Logger
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.inject.Inject
import javax.inject.Singleton
class FileLogger(
context: Context,
@Singleton
class FileLogger @Inject constructor(
private val context: Application,
) : Timber.DebugTree() {
val logDirectory = File(context.cacheDir, "logs").apply { mkdirs() }
private val logDirectory = File(context.cacheDir, "logs").apply { mkdirs() }
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val fileHandler: FileHandler = FileHandler(
"${logDirectory.absolutePath}/log.%g.txt",
20 * 1024 * 1024,
10
)
private val logger = Logger.getLogger(context.packageName)
@SuppressLint("LogNotTimber")
private val logger = Logger.getLogger(context.packageName).apply {
try {
useParentHandlers = false
} catch (e: SecurityException) {
Log.e("FileLogger", "Failed to disable parent handlers", e)
}
}
init {
fileHandler.formatter = LogFormatter()
@ -41,8 +58,43 @@ class FileLogger(
}
}
fun flush() {
fileHandler.flush()
suspend fun getZipFile(): File = withContext(Dispatchers.IO) {
val zipFile = File(context.cacheDir, "logs.zip")
val buffer = ByteArray(1024)
FileOutputStream(zipFile).use { fos ->
ZipOutputStream(fos).use { zos ->
try {
Runtime
.getRuntime()
.exec(arrayOf("logcat", "-d", "-v", "threadtime", "*:*"))
?.inputStream
?.use { logcat ->
zos.putNextEntry(ZipEntry("logcat.txt"))
var len: Int
while (logcat.read(buffer).also { len = it } > 0) {
zos.write(buffer, 0, len)
}
zos.closeEntry()
}
} catch (e: IOException) {
Timber.e(e, "Failed to save logcat")
}
fileHandler.flush()
logDirectory
.listFiles { _, name -> name?.endsWith(".txt") == true }
?.forEach { file ->
FileInputStream(file).use { fis ->
zos.putNextEntry(ZipEntry(file.name))
var len: Int
while (fis.read(buffer).also { len = it } > 0) {
zos.write(buffer, 0, len)
}
zos.closeEntry()
}
}
}
}
zipFile
}
companion object {

@ -3,14 +3,19 @@ package org.tasks.preferences.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.utility.Constants
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.analytics.Firebase
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.logging.FileLogger
import javax.inject.Inject
private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new"
@ -19,6 +24,7 @@ private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new"
class HelpAndFeedback : InjectingPreferenceFragment() {
@Inject lateinit var firebase: Firebase
@Inject lateinit var fileLogger: FileLogger
override fun getPreferenceXml() = R.xml.help_and_feedback
@ -47,6 +53,26 @@ class HelpAndFeedback : InjectingPreferenceFragment() {
false
}
findPreference(R.string.send_application_logs)
.setOnPreferenceClickListener {
lifecycleScope.launch {
val file = FileProvider.getUriForFile(
requireContext(),
Constants.FILE_PROVIDER_AUTHORITY,
fileLogger.getZipFile()
)
val intent = Intent(Intent.ACTION_SEND)
.setType("message/rfc822")
.putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.support_email)))
.putExtra(Intent.EXTRA_SUBJECT, "Tasks logs")
.putExtra(Intent.EXTRA_TEXT, device.debugInfo)
.putExtra(Intent.EXTRA_STREAM, file)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
}
false
}
findPreference(R.string.p_collect_statistics)
.setOnPreferenceClickListener {
showRestartDialog()

@ -320,6 +320,7 @@ File %1$s contained %2$s.\n\n
<string name="source_code">Source code</string>
<string name="translations">Contribute translations</string>
<string name="contact_developer">Contact developer</string>
<string name="send_application_logs">Send application logs</string>
<string name="rate_tasks">Rate Tasks</string>
<string name="quiet_hours_summary">No reminders during quiet hours</string>
<string name="TLA_menu_donate">Donate</string>

@ -30,6 +30,11 @@
android:title="@string/contact_developer"
app:icon="@drawable/ic_outline_email_24px" />
<Preference
android:key="@string/send_application_logs"
android:title="@string/send_application_logs"
app:icon="@drawable/ic_outline_attachment_24px" />
</PreferenceCategory>
<PreferenceCategory

@ -9,10 +9,11 @@ import javax.inject.Inject
class BuildSetup @Inject constructor(
private val context: Application,
private val fileLogger: FileLogger,
) {
fun setup() {
Timber.plant(ErrorReportingTree())
Timber.plant(FileLogger(context))
Timber.plant(fileLogger)
}
private class ErrorReportingTree : Timber.Tree() {

@ -9,6 +9,7 @@ plugins {
alias(libs.plugins.ksp) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.protobuf) apply false
alias(libs.plugins.redacted) apply false
}
buildscript {

@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.redacted)
}
kotlin {
@ -68,6 +69,11 @@ android {
}
}
redacted {
redactedAnnotation = "org/tasks/data/Redacted"
enabled = gradle.startParameter.taskNames.any { it.contains("Release") }
}
dependencies {
ksp(libs.androidx.room.compiler)
}

@ -0,0 +1,5 @@
package org.tasks.data
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class Redacted

@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.Redacted
import org.tasks.data.db.Table
import java.net.HttpURLConnection
@ -20,12 +21,16 @@ data class CaldavAccount(
val id: Long = 0,
@ColumnInfo(name = "cda_uuid")
val uuid: String? = Task.NO_UUID,
@Redacted
@ColumnInfo(name = "cda_name")
var name: String? = "",
@Redacted
@ColumnInfo(name = "cda_url")
var url: String? = "",
@Redacted
@ColumnInfo(name = "cda_username")
var username: String? = "",
@Redacted
@ColumnInfo(name = "cda_password")
@Transient
var password: String? = "",
@ -70,9 +75,6 @@ data class CaldavAccount(
fun isLoggedOut() = error?.startsWith(ERROR_UNAUTHORIZED) == true
fun isPaymentRequired() = error.isPaymentRequired()
override fun toString(): String {
return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, error=$error, accountType=$accountType, isCollapsed=$isCollapsed, serverType=$serverType)"
}
val hasError: Boolean
get() = !error.isNullOrBlank()

@ -8,6 +8,7 @@ import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.NO_ORDER
import org.tasks.data.Redacted
import org.tasks.data.db.Table
@Serializable
@ -19,9 +20,11 @@ data class CaldavCalendar(
@ColumnInfo(name = "cdl_id") var id: Long = 0,
@ColumnInfo(name = "cdl_account") val account: String? = Task.NO_UUID,
@ColumnInfo(name = "cdl_uuid") var uuid: String? = Task.NO_UUID,
@Redacted
@ColumnInfo(name = "cdl_name") var name: String? = "",
@ColumnInfo(name = "cdl_color") var color: Int = 0,
@ColumnInfo(name = "cdl_ctag") var ctag: String? = null,
@Redacted
@ColumnInfo(name = "cdl_url") var url: String? = "",
@ColumnInfo(name = "cdl_icon") val icon: String? = null,
@ColumnInfo(name = "cdl_order") val order: Int = NO_ORDER,

@ -8,6 +8,7 @@ import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.NO_ORDER
import org.tasks.data.Redacted
@Serializable
@CommonParcelize
@ -17,6 +18,7 @@ data class Filter(
@ColumnInfo(name = "_id")
@Transient
val id: Long = 0,
@Redacted
@ColumnInfo(name = "title")
val title: String? = null,
@ColumnInfo(name = "sql")

@ -10,6 +10,7 @@ import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.NO_ORDER
import org.tasks.data.Redacted
import org.tasks.data.UUIDHelper
import org.tasks.data.db.Table
import org.tasks.formatCoordinates
@ -30,16 +31,22 @@ data class Place(
val id: Long = 0,
@ColumnInfo(name = "uid")
val uid: String? = UUIDHelper.newUUID(),
@Redacted
@ColumnInfo(name = "name")
val name: String? = null,
@Redacted
@ColumnInfo(name = "address")
val address: String? = null,
@Redacted
@ColumnInfo(name = "phone")
val phone: String? = null,
@Redacted
@ColumnInfo(name = "url")
val url: String? = null,
@Redacted
@ColumnInfo(name = "latitude")
val latitude: Double = 0.0,
@Redacted
@ColumnInfo(name = "longitude")
val longitude: Double = 0.0,
@ColumnInfo(name = "place_color")

@ -5,6 +5,7 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import org.tasks.data.Redacted
@Entity(
tableName = "principals",
@ -22,8 +23,8 @@ data class Principal(
@PrimaryKey(autoGenerate = true) var id: Long = 0,
val account: Long,
val href: String,
var email: String? = null,
@ColumnInfo(name = "display_name") var displayName: String? = null
@Redacted var email: String? = null,
@Redacted @ColumnInfo(name = "display_name") var displayName: String? = null
) {
val name: String
get() = displayName

@ -6,6 +6,7 @@ import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.tasks.data.Redacted
import org.tasks.data.db.Table
@Serializable
@ -28,6 +29,7 @@ data class Tag(
@ColumnInfo(name = "task", index = true)
@Transient
val task: Long = 0,
@Redacted
@ColumnInfo(name = "name")
val name: String? = null,
@ColumnInfo(name = "tag_uid")

@ -8,6 +8,7 @@ import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.NO_ORDER
import org.tasks.data.Redacted
import org.tasks.data.UUIDHelper
@CommonParcelize
@ -20,6 +21,7 @@ data class TagData(
val id: Long? = null,
@ColumnInfo(name = "remoteId")
val remoteId: String? = UUIDHelper.newUUID(),
@Redacted
@ColumnInfo(name = "name")
val name: String? = "",
@ColumnInfo(name = "color")

@ -15,6 +15,7 @@ import kotlinx.serialization.json.JsonNames
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.CommonRawValue
import org.tasks.data.Redacted
import org.tasks.data.UUIDHelper
import org.tasks.data.db.Table
import org.tasks.data.sql.Field
@ -34,6 +35,7 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor(
@ColumnInfo(name = "_id")
@Transient
var id: Long = NO_ID,
@Redacted
@ColumnInfo(name = "title")
var title: String? = null,
@ColumnInfo(name = "importance")
@ -50,6 +52,7 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor(
var completionDate: Long = 0L,
@ColumnInfo(name = "deleted")
var deletionDate: Long = 0L,
@Redacted
@ColumnInfo(name = "notes")
var notes: String? = null,
@ColumnInfo(name = "estimatedSeconds")

@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.Redacted
import org.tasks.data.UUIDHelper
@Serializable
@ -19,6 +20,7 @@ data class TaskAttachment(
val id: Long? = null,
@ColumnInfo(name = "file_uuid")
val remoteId: String = UUIDHelper.newUUID(),
@Redacted
@ColumnInfo(name = "filename")
val name: String,
@ColumnInfo(name = "uri")

@ -7,6 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.tasks.CommonParcelable
import org.tasks.CommonParcelize
import org.tasks.data.Redacted
import org.tasks.data.db.Table
@Serializable
@ -19,6 +20,7 @@ data class UserActivity(
var id: Long? = null,
@ColumnInfo(name = "remoteId")
var remoteId: String? = Task.NO_UUID,
@Redacted
@ColumnInfo(name = "message")
var message: String? = "",
@ColumnInfo(name = "picture")

@ -214,3 +214,4 @@ ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.26" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
protobuf = { id = "com.google.protobuf", version = "0.9.4" }
redacted = { id = "dev.zacsweers.redacted", version = "1.10.0" }
Loading…
Cancel
Save