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