diff --git a/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java b/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java
index 3601e8b42..a646f90d1 100755
--- a/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java
+++ b/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java
@@ -28,6 +28,7 @@ import com.itsaky.androidide.databinding.ActivityPreferencesBinding;
import com.itsaky.androidide.fragments.IDEPreferencesFragment;
import com.itsaky.androidide.preferences.AboutPreferences;
import com.itsaky.androidide.preferences.ConfigurationPreferences;
+import com.itsaky.androidide.preferences.DeveloperOptionsPreferences;
import com.itsaky.androidide.preferences.IDEPreferences;
import com.itsaky.androidide.preferences.PrivacyPreferences;
import java.util.ArrayList;
@@ -59,6 +60,7 @@ public class PreferencesActivity extends LimitlessIDEActivity {
prefs.getChildren().clear();
prefs.addPreference(new ConfigurationPreferences());
prefs.addPreference(new PrivacyPreferences());
+ prefs.addPreference(new DeveloperOptionsPreferences());
prefs.addPreference(new AboutPreferences());
final var args = new Bundle();
diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
index 1733a5ea7..9022a5bd6 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
@@ -215,12 +215,6 @@ abstract class BaseEditorActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- if (BuildConfig.DEBUG) {
- StrictMode.setVmPolicy(
- StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()).detectLeakedClosableObjects().build()
- )
- }
-
registerLanguageServers()
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_PROJECT_PATH)) {
diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/IDELogcatReader.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/IDELogcatReader.kt
new file mode 100644
index 000000000..7f97dc121
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/IDELogcatReader.kt
@@ -0,0 +1,62 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+package com.itsaky.androidide.activities.editor
+
+import android.os.Process
+import com.itsaky.androidide.utils.Environment
+import com.itsaky.androidide.utils.ILogger
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+
+/**
+ * Reads the logs from AndroidIDE and saves it to a file in the projects directory.
+ *
+ * @author Akash Yadav
+ */
+class IDELogcatReader : Thread("IDELogcatReader") {
+
+ private val log = ILogger.newInstance("IDELogcatReader")
+
+ override fun run() {
+ val date = Date()
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH:mm:ss.SSS")
+ val outputFile = File(Environment.ANDROIDIDE_HOME,
+ "logs/AndroidIDE-LOG-${dateFormat.format(date)}.txt")
+ log.debug("Creating output file: $outputFile")
+ outputFile.parentFile!!.mkdirs()
+ outputFile.createNewFile()
+ outputFile.outputStream().buffered().use {
+ try {
+ val process = ProcessBuilder("logcat", "--pid=${Process.myPid()}", "-v",
+ "threadtime").let { builder ->
+ builder.redirectErrorStream(true)
+ builder.start()
+ }
+
+ process.inputStream.bufferedReader().forEachLine { line ->
+ it.write("$line\n".toByteArray())
+ it.flush()
+ }
+
+ log.info("Process ended with exit code :", process.waitFor())
+ } catch (err: Throwable) {
+ log.error("Failed to read logs", err)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
index 7a7d1f2da..e48ae254d 100755
--- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
+++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
@@ -20,6 +20,7 @@ package com.itsaky.androidide.app
import android.content.Intent
import android.net.Uri
+import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.Observer
import androidx.work.Constraints
@@ -32,6 +33,7 @@ import com.blankj.utilcode.util.ThrowableUtils.getFullStackTrace
import com.google.android.material.color.DynamicColors
import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.activities.CrashHandlerActivity
+import com.itsaky.androidide.activities.editor.IDELogcatReader
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
@@ -40,6 +42,8 @@ import com.itsaky.androidide.events.EditorEventsIndex
import com.itsaky.androidide.events.LspApiEventsIndex
import com.itsaky.androidide.events.LspJavaEventsIndex
import com.itsaky.androidide.events.ProjectsApiEventsIndex
+import com.itsaky.androidide.preferences.KEY_DEVOPTS_DEBUGGING_DUMPLOGS
+import com.itsaky.androidide.preferences.dumpLogs
import com.itsaky.androidide.preferences.internal.STAT_OPT_IN
import com.itsaky.androidide.preferences.internal.enableMaterialYou
import com.itsaky.androidide.preferences.internal.statOptIn
@@ -63,6 +67,7 @@ import kotlin.system.exitProcess
class IDEApplication : BaseApplication() {
private var uncaughtExceptionHandler: UncaughtExceptionHandler? = null
+ private var ideLogcatReader: IDELogcatReader? = null
private val log = ILogger.newInstance("IDEApplication")
init {
@@ -78,6 +83,16 @@ class IDEApplication : BaseApplication() {
Thread.setDefaultUncaughtExceptionHandler { thread, th -> handleCrash(thread, th) }
super.onCreate()
+ if (BuildConfig.DEBUG) {
+ StrictMode.setVmPolicy(
+ StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()).penaltyLog().detectAll().build()
+ )
+
+ if (dumpLogs) {
+ startLogcatReader()
+ }
+ }
+
EventBus.builder()
.addIndex(AppEventsIndex())
.addIndex(EditorEventsIndex())
@@ -164,15 +179,37 @@ class IDEApplication : BaseApplication() {
})
}
+ private fun startLogcatReader() {
+ if (ideLogcatReader != null) {
+ // already started
+ return
+ }
+
+ log.info("Starting logcat reader...")
+ ideLogcatReader = IDELogcatReader().also { it.start() }
+ }
+
+ private fun stopLogcatReader() {
+ log.info("Stopping logcat reader...")
+ ideLogcatReader?.interrupt()
+ ideLogcatReader = null
+ }
+
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPrefChanged(event: PreferenceChangeEvent) {
+ val enabled = event.value as? Boolean? ?: return
if (event.key == STAT_OPT_IN) {
- val enabled = event.value as Boolean
if (enabled) {
reportStatsIfNecessary()
} else {
cancelStatUploadWorker()
}
+ } else if (event.key == KEY_DEVOPTS_DEBUGGING_DUMPLOGS) {
+ if (enabled) {
+ startLogcatReader()
+ } else {
+ stopLogcatReader()
+ }
}
}
diff --git a/app/src/main/java/com/itsaky/androidide/preferences/developerOptions.kt b/app/src/main/java/com/itsaky/androidide/preferences/developerOptions.kt
new file mode 100644
index 000000000..e509cda2f
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/preferences/developerOptions.kt
@@ -0,0 +1,61 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.preferences
+
+import com.itsaky.androidide.R
+import com.itsaky.androidide.preferences.internal.prefManager
+import kotlinx.parcelize.Parcelize
+
+internal const val KEY_DEVOPTS = "ide.prefs.developerOptions"
+internal const val KEY_DEVOPTS_DEBUGGING = "${KEY_DEVOPTS}.debugging"
+internal const val KEY_DEVOPTS_DEBUGGING_DUMPLOGS = "${KEY_DEVOPTS_DEBUGGING}.dumpLogs"
+
+@Parcelize
+internal class DeveloperOptionsScreen(override val key: String = KEY_DEVOPTS,
+ override val title: Int = R.string.title_developer_options,
+ override val summary: Int? = R.string.idepref_devOptions_summary,
+ override val children: List = mutableListOf()) : IPreferenceScreen() {
+
+ init {
+ addPreference(DebuggingPreferences())
+ }
+}
+
+@Parcelize
+internal class DebuggingPreferences(
+ override val key: String = KEY_DEVOPTS_DEBUGGING,
+ override val title: Int = R.string.idepref_group_debugging,
+ override val children: List = mutableListOf()) : IPreferenceGroup() {
+
+ init {
+ addPreference(DumpLogsPreference())
+ }
+}
+
+internal var dumpLogs: Boolean
+ get() = prefManager.getBoolean(KEY_DEVOPTS_DEBUGGING_DUMPLOGS, false)
+ set(value) {
+ prefManager.putBoolean(KEY_DEVOPTS_DEBUGGING_DUMPLOGS, value)
+ }
+
+@Parcelize
+internal class DumpLogsPreference(
+ override val key: String = KEY_DEVOPTS_DEBUGGING_DUMPLOGS,
+ override val title: Int = R.string.idepref_devOptions_dumpLogs_title,
+ override val summary: Int? = R.string.idepref_devOptions_dumpLogs_summary) :
+ SwitchPreference(setValue = ::dumpLogs::set, getValue = ::dumpLogs::get)
\ No newline at end of file
diff --git a/app/src/main/java/com/itsaky/androidide/preferences/root.kt b/app/src/main/java/com/itsaky/androidide/preferences/root.kt
index dd19a4fee..0e1010906 100644
--- a/app/src/main/java/com/itsaky/androidide/preferences/root.kt
+++ b/app/src/main/java/com/itsaky/androidide/preferences/root.kt
@@ -19,9 +19,9 @@ package com.itsaky.androidide.preferences
import android.content.Intent
import com.itsaky.androidide.activities.AboutActivity
-import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.app.IDEApplication
+import com.itsaky.androidide.resources.R.string
import kotlinx.parcelize.Parcelize
private const val KEY_GITHUB = "idepref_gh"
@@ -82,6 +82,7 @@ class ConfigurationPreferences(
override val title: Int = string.configure,
override val children: List = mutableListOf()
) : IPreferenceGroup() {
+
init {
addPreference(GeneralPreferences())
addPreference(EditorPreferences())
@@ -95,17 +96,31 @@ class PrivacyPreferences(
override val title: Int = string.title_privacy,
override val children: List = mutableListOf()
) : IPreferenceGroup() {
+
init {
addPreference(StatPreferences())
}
}
+@Parcelize
+class DeveloperOptionsPreferences(
+ override val key: String = "idepref_devOpts",
+ override val title: Int = string.title_developer_options,
+ override val children: List = mutableListOf()
+) : IPreferenceGroup() {
+
+ init {
+ addPreference(DeveloperOptionsScreen())
+ }
+}
+
@Parcelize
class AboutPreferences(
override val key: String = "idepref_category_about",
override val title: Int = string.about,
override val children: List = mutableListOf()
) : IPreferenceGroup() {
+
init {
addPreference(github)
addPreference(channel)
diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml
index f0a2a3bd3..330454ce6 100644
--- a/resources/src/main/res/values/strings.xml
+++ b/resources/src/main/res/values/strings.xml
@@ -581,4 +581,9 @@
Android veresion
Opt-in
No, thanks
+ Developer options
+ Experimental/debugging options for AndroidIDE
+ Debugging
+ Dump logs
+ Dump AndroidIDE logs to $HOME/.androidide/logs