diff --git a/app/proguard.pro b/app/proguard.pro
index d37b4fb38..7fed68313 100644
--- a/app/proguard.pro
+++ b/app/proguard.pro
@@ -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
diff --git a/app/src/debug/java/org/tasks/BuildSetup.kt b/app/src/debug/java/org/tasks/BuildSetup.kt
index f413c3b6c..dd72c69c1 100644
--- a/app/src/debug/java/org/tasks/BuildSetup.kt
+++ b/app/src/debug/java/org/tasks/BuildSetup.kt
@@ -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)
diff --git a/app/src/main/java/org/tasks/Tasks.kt b/app/src/main/java/org/tasks/Tasks.kt
index 06923049b..80c0178fd 100644
--- a/app/src/main/java/org/tasks/Tasks.kt
+++ b/app/src/main/java/org/tasks/Tasks.kt
@@ -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) {
diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
index 9bcc995bd..4c854cb41 100644
--- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
+++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
@@ -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")
diff --git a/app/src/main/java/org/tasks/logging/FileLogger.kt b/app/src/main/java/org/tasks/logging/FileLogger.kt
index a7bbacda8..e77b556a7 100644
--- a/app/src/main/java/org/tasks/logging/FileLogger.kt
+++ b/app/src/main/java/org/tasks/logging/FileLogger.kt
@@ -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 {
diff --git a/app/src/main/java/org/tasks/preferences/fragments/HelpAndFeedback.kt b/app/src/main/java/org/tasks/preferences/fragments/HelpAndFeedback.kt
index a67a9a6db..cfd2a7e06 100644
--- a/app/src/main/java/org/tasks/preferences/fragments/HelpAndFeedback.kt
+++ b/app/src/main/java/org/tasks/preferences/fragments/HelpAndFeedback.kt
@@ -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()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f25648272..2f67ad185 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -320,6 +320,7 @@ File %1$s contained %2$s.\n\n
Source code
Contribute translations
Contact developer
+ Send application logs
Rate Tasks
No reminders during quiet hours
Donate
diff --git a/app/src/main/res/xml/help_and_feedback.xml b/app/src/main/res/xml/help_and_feedback.xml
index 2f97efc03..02db4361f 100644
--- a/app/src/main/res/xml/help_and_feedback.xml
+++ b/app/src/main/res/xml/help_and_feedback.xml
@@ -30,6 +30,11 @@
android:title="@string/contact_developer"
app:icon="@drawable/ic_outline_email_24px" />
+
+