[Android] Implement the log inclusion in the new feedback UI

Nicolas Pomepuy git at videolan.org
Mon Mar 17 06:15:49 UTC 2025


vlc-android | branch: master | Nicolas Pomepuy <nicolas at videolabs.io> | Tue Mar 11 13:55:58 2025 +0100| [ff0f8bbfb7b57a6dda671b6967e88f28c20ab97f] | committer: Nicolas Pomepuy

Implement the log inclusion in the new feedback UI

> https://code.videolan.org/videolan/vlc-android/commit/ff0f8bbfb7b57a6dda671b6967e88f28c20ab97f
---

 .../src/org/videolan/vlc/gui/FeedbackActivity.kt   | 152 +++++++++++++++++----
 1 file changed, 122 insertions(+), 30 deletions(-)

diff --git a/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt b/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt
index c7e7a8f645..dc2c2b86ac 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/FeedbackActivity.kt
@@ -25,14 +25,21 @@
 package org.videolan.vlc.gui
 
 import android.os.Bundle
+import android.util.Log
 import android.view.MenuItem
 import androidx.core.widget.addTextChangedListener
 import androidx.databinding.DataBindingUtil
 import androidx.lifecycle.lifecycleScope
 import com.google.android.material.appbar.MaterialToolbar
+import com.google.android.material.snackbar.Snackbar
 import kotlinx.coroutines.CompletableJob
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.videolan.libvlc.util.AndroidUtil
 import org.videolan.resources.AndroidDevices
+import org.videolan.resources.AppContextProvider
 import org.videolan.resources.CRASH_HAPPENED
 import org.videolan.resources.CRASH_ML_CTX
 import org.videolan.resources.CRASH_ML_MSG
@@ -41,17 +48,22 @@ import org.videolan.tools.isVisible
 import org.videolan.tools.setGone
 import org.videolan.tools.setVisible
 import org.videolan.vlc.BuildConfig
+import org.videolan.vlc.DebugLogService
 import org.videolan.vlc.R
 import org.videolan.vlc.databinding.AboutFeedbackActivityBinding
 import org.videolan.vlc.gui.helpers.FeedbackUtil
 import org.videolan.vlc.gui.helpers.UiTools
+import org.videolan.vlc.util.FileUtils
+import org.videolan.vlc.util.Permissions
 import org.videolan.vlc.util.TextUtils
 import org.videolan.vlc.util.openLinkIfPossible
+import java.io.File
+import java.io.IOException
 
 /**
  * Activity showing the different ways to report some feedback
  */
-class FeedbackActivity : BaseActivity() {
+class FeedbackActivity : BaseActivity(), DebugLogService.Client.Callback {
 
     internal lateinit var binding: AboutFeedbackActivityBinding
     override fun getSnackAnchorView(overAudioPlayer: Boolean) = binding.root
@@ -60,9 +72,80 @@ class FeedbackActivity : BaseActivity() {
     private var mlErrorMessage: String? = null
     private var mlErrorContext: String? = null
 
+    //logs
+    private var logMessage = ""
+    private lateinit var client: DebugLogService.Client
+    private lateinit var logcatZipPath: String
+
+    override fun onStarted(logList: List<String>) {
+        logMessage = "Starting collecting logs at ${System.currentTimeMillis()}"
+        //initiate a log to wait for
+        Log.d("FeedbackActivity", logMessage)
+    }
+
+    override fun onStopped() {
+    }
+
+    override fun onLog(msg: String) {
+        //Wait for the log to initiate a save to avoid ANR
+        if (msg.contains(logMessage)) {
+            if (AndroidUtil.isOOrLater && !Permissions.canWriteStorage())
+                Permissions.askWriteStoragePermission(this, false) { client.save() }
+            else
+                client.save()
+        }
+    }
+
+    override fun onSaved(success: Boolean, path: String) {
+        if (!success) {
+            Snackbar.make(window.decorView, R.string.dump_logcat_failure, Snackbar.LENGTH_LONG).show()
+            client.stop()
+            return
+        }
+        lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
+            withContext(Dispatchers.IO) {
+                client.stop()
+                if (!::logcatZipPath.isInitialized) {
+                    val externalPath = AppContextProvider.appContext.getExternalFilesDir(null)?.absolutePath
+                        ?: return at withContext
+                    logcatZipPath = "$externalPath/logcat.zip"
+                }
+                val filesToAdd = mutableListOf(path)
+                //add previous crash logs
+                try {
+                    AppContextProvider.appContext.getExternalFilesDir(null)?.absolutePath?.let { folder ->
+                        File(folder).listFiles()?.forEach {
+                            if (it.isFile && (it.name.contains("crash_") || it.name.contains("logcat_"))) filesToAdd.add(it.path)
+                        }
+                    }
+                } catch (exception: IOException) {
+                    Snackbar.make(window.decorView, R.string.dump_logcat_failure, Snackbar.LENGTH_LONG).show()
+                    client.stop()
+                    return at withContext
+                }
+
+                if (!FileUtils.zip(filesToAdd.toTypedArray(), logcatZipPath)) {
+                    Snackbar.make(window.decorView, R.string.dump_logcat_failure, Snackbar.LENGTH_LONG).show()
+                    client.stop()
+                    return at withContext
+                }
+                try {
+                    filesToAdd.forEach { FileUtils.deleteFile(it) }
+                } catch (exception: IOException) {
+                    Snackbar.make(window.decorView, R.string.dump_logcat_failure, Snackbar.LENGTH_LONG).show()
+                    client.stop()
+                    return at withContext
+                }
+
+                sendEmail(true)
+            }
+        }
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
+        client = DebugLogService.Client(this, this)
         binding = DataBindingUtil.setContentView(this, R.layout.about_feedback_activity)
         val toolbar = findViewById<MaterialToolbar>(R.id.main_toolbar)
         setSupportActionBar(toolbar)
@@ -104,35 +187,10 @@ class FeedbackActivity : BaseActivity() {
             openLinkIfPossible("https://docs.videolan.me/vlc-user/android/")
         }
         binding.emailSupportSend.setOnClickListener {
-            val feedbackTypePosition = feedbackTypeEntries.indexOf(binding.feedbackTypeEntry.text.toString())
-            val isCrashFromML = !mlErrorContext.isNullOrEmpty() || !mlErrorMessage.isNullOrEmpty()
-            val subjectPrepend = when {
-                isCrashFromML -> "[ML Crash]"
-                feedbackTypePosition == 0 -> "[Help] "
-                feedbackTypePosition == 1 -> "[Feedback/Request] "
-                feedbackTypePosition == 2 -> "[Bug] "
-                else -> "[Crash] "
-            }
-            val mail = if (BuildConfig.BETA && feedbackTypePosition > 2) FeedbackUtil.SupportType.CRASH_REPORT_EMAIL else FeedbackUtil.SupportType.SUPPORT_EMAIL
-            lifecycleScope.launch {
-                val message = if (isCrashFromML)
-                    buildString {
-                        append(binding.messageTextInputLayout.editText?.text.toString())
-                        append("<br /><br />")
-                        append("____________________________<br />")
-                        append("ML Crash!<br />")
-                        append("____________________________<br />")
-                        append("ML Context: $mlErrorContext<br />ML error message: $mlErrorMessage")
-                    }
-                else binding.messageTextInputLayout.editText?.text.toString()
-                FeedbackUtil.sendEmail(
-                    this at FeedbackActivity,
-                    mail,
-                    binding.showIncludes && binding.includeMedialibrary.isChecked,
-                    message,
-                    subjectPrepend + binding.subjectTextInputLayout.editText?.text.toString()
-                )
-            }
+            if (binding.includeLogs.isChecked) {
+                client.start()
+            } else
+                sendEmail()
         }
         val installSource = FeedbackUtil.getInstallSource(this)
         if (installSource == null)
@@ -163,6 +221,39 @@ class FeedbackActivity : BaseActivity() {
         }
     }
 
+    private fun sendEmail(includeLogs: Boolean = false) {
+        val feedbackTypePosition = feedbackTypeEntries.indexOf(binding.feedbackTypeEntry.text.toString())
+        val isCrashFromML = !mlErrorContext.isNullOrEmpty() || !mlErrorMessage.isNullOrEmpty()
+        val subjectPrepend = when {
+            isCrashFromML -> "[ML Crash]"
+            feedbackTypePosition == 0 -> "[Help] "
+            feedbackTypePosition == 1 -> "[Feedback/Request] "
+            feedbackTypePosition == 2 -> "[Bug] "
+            else -> "[Crash] "
+        }
+        val mail = if (BuildConfig.BETA && feedbackTypePosition > 2) FeedbackUtil.SupportType.CRASH_REPORT_EMAIL else FeedbackUtil.SupportType.SUPPORT_EMAIL
+        lifecycleScope.launch {
+            val message = if (isCrashFromML)
+                buildString {
+                    append(binding.messageTextInputLayout.editText?.text.toString())
+                    append("<br /><br />")
+                    append("____________________________<br />")
+                    append("ML Crash!<br />")
+                    append("____________________________<br />")
+                    append("ML Context: $mlErrorContext<br />ML error message: $mlErrorMessage")
+                }
+            else binding.messageTextInputLayout.editText?.text.toString()
+            FeedbackUtil.sendEmail(
+                this at FeedbackActivity,
+                mail,
+                binding.showIncludes && binding.includeMedialibrary.isChecked,
+                message,
+                subjectPrepend + binding.subjectTextInputLayout.editText?.text.toString(),
+                if (includeLogs) logcatZipPath else null
+            )
+        }
+    }
+
     /**
      * Update the visibility for the form includes section.
      *
@@ -200,6 +291,7 @@ class FeedbackActivity : BaseActivity() {
     override fun onDestroy() {
         job?.complete()
         job = null
+        if (::client.isInitialized) client.release()
         super.onDestroy()
     }
 



More information about the Android mailing list