diff --git a/app/src/main/java/com/itsaky/androidide/activities/OnboardingActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/OnboardingActivity.kt
index dd0dc56424..f7c4e37f6e 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/OnboardingActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/OnboardingActivity.kt
@@ -19,7 +19,6 @@ package com.itsaky.androidide.activities
import android.content.Intent
import android.os.Bundle
-import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.github.appintro.AppIntro2
@@ -27,6 +26,7 @@ import com.github.appintro.AppIntroPageTransformerType
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider
+import com.itsaky.androidide.app.configuration.IJdkDistributionProvider
import com.itsaky.androidide.fragments.onboarding.GreetingFragment
import com.itsaky.androidide.fragments.onboarding.IdeSetupConfigurationFragment
import com.itsaky.androidide.fragments.onboarding.OnboardingInfoFragment
@@ -147,14 +147,15 @@ class OnboardingActivity : AppIntro2() {
}
private fun checkToolsIsInstalled(): Boolean {
- return Environment.JAVA.exists() && Environment.ANDROID_HOME.exists()
+ return IJdkDistributionProvider.getInstance().installedDistributions.isNotEmpty()
+ && Environment.ANDROID_HOME.exists()
}
private fun isSetupDone() =
(checkToolsIsInstalled() && statConsentDialogShown && PermissionsFragment.areAllPermissionsGranted(
this))
- private fun isInstalledOnSdCard() : Boolean {
+ private fun isInstalledOnSdCard(): Boolean {
// noinspection SdCardPath
return PackageUtils.isAppInstalledOnExternalStorage(this) &&
TermuxConstants.TERMUX_FILES_DIR_PATH != filesDir.absolutePath
diff --git a/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt
index dc379bd5c0..3d54728fda 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/SplashActivity.kt
@@ -20,7 +20,6 @@ package com.itsaky.androidide.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
-import android.os.PersistableBundle
/**
* @author Akash Yadav
diff --git a/app/src/main/java/com/itsaky/androidide/app/configuration/JdkDistributionProviderImpl.kt b/app/src/main/java/com/itsaky/androidide/app/configuration/JdkDistributionProviderImpl.kt
new file mode 100644
index 0000000000..4e67175d70
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/app/configuration/JdkDistributionProviderImpl.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.app.configuration
+
+import com.google.auto.service.AutoService
+import com.itsaky.androidide.models.JdkDistribution
+import com.itsaky.androidide.preferences.internal.javaHome
+import com.itsaky.androidide.utils.Environment
+import com.itsaky.androidide.utils.ILogger
+import com.itsaky.androidide.utils.JdkUtils
+import java.io.File
+
+/**
+ * @author Akash Yadav
+ */
+@AutoService(IJdkDistributionProvider::class)
+class JdkDistributionProviderImpl : IJdkDistributionProvider {
+
+ companion object {
+
+ private val log = ILogger.newInstance("JdkDistributionProviderImpl")
+ }
+
+ override val installedDistributions: List by lazy {
+ JdkUtils.findJavaInstallations().also { distributions ->
+
+ // set the default value for the 'javaHome' preference
+ if (javaHome.isBlank() && distributions.isNotEmpty()) {
+ var defaultDist = distributions.find {
+ it.javaVersion.startsWith(IJdkDistributionProvider.DEFAULT_JAVA_VERSION)
+ }
+
+ if (defaultDist == null) {
+ // if JDK 17 is not installed, use the first available installation
+ defaultDist = distributions[0]
+ }
+
+ javaHome = defaultDist.javaHome
+ }
+
+ val home = File(javaHome)
+ val java = File(home, "bin/java")
+
+ // the previously selected JDK distribution does not exist
+ // check if we have other distributions installed
+ if (!home.exists() || !java.exists() || !java.isFile) {
+ if (distributions.isNotEmpty()) {
+ log.warn(
+ "Previously selected java.home does not exists! Falling back to ${distributions[0]}...")
+ javaHome = distributions[0].javaHome
+ }
+ }
+
+ if (!java.canExecute()) {
+ java.setExecutable(true)
+ }
+
+ log.debug("Setting Environment.JAVA_HOME to $javaHome")
+
+ Environment.JAVA_HOME = File(javaHome)
+ Environment.JAVA = Environment.JAVA_HOME.resolve("bin/java")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt
index d68d4baa04..94646aaa94 100644
--- a/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt
+++ b/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt
@@ -17,14 +17,18 @@
package com.itsaky.androidide.preferences
+import android.content.Context
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.itsaky.androidide.R
+import com.itsaky.androidide.app.configuration.IJdkDistributionProvider
+import com.itsaky.androidide.models.JdkDistribution
import com.itsaky.androidide.preferences.internal.CUSTOM_GRADLE_INSTALLATION
import com.itsaky.androidide.preferences.internal.GRADLE_CLEAR_CACHE
import com.itsaky.androidide.preferences.internal.GRADLE_COMMANDS
import com.itsaky.androidide.preferences.internal.LAUNCH_APP_AFTER_INSTALL
+import com.itsaky.androidide.preferences.internal.PREF_JAVA_HOME
import com.itsaky.androidide.preferences.internal.gradleInstallationDir
import com.itsaky.androidide.preferences.internal.isBuildCacheEnabled
import com.itsaky.androidide.preferences.internal.isDebugEnabled
@@ -33,6 +37,7 @@ import com.itsaky.androidide.preferences.internal.isOfflineEnabled
import com.itsaky.androidide.preferences.internal.isScanEnabled
import com.itsaky.androidide.preferences.internal.isStacktraceEnabled
import com.itsaky.androidide.preferences.internal.isWarningModeAllEnabled
+import com.itsaky.androidide.preferences.internal.javaHome
import com.itsaky.androidide.preferences.internal.launchAppAfterInstall
import com.itsaky.androidide.resources.R.drawable
import com.itsaky.androidide.resources.R.string
@@ -68,6 +73,7 @@ private class GradleOptions(
init {
addPreference(GradleCommands())
addPreference(GradleDistrubution())
+ addPreference(GradleJDKVersionPreference())
addPreference(GradleClearCache())
}
}
@@ -167,4 +173,48 @@ private class LaunchAppAfterInstall(
override val summary: Int? = R.string.idepref_launchAppAfterInstall_summary,
override val icon: Int? = drawable.ic_open_external
) :
- SwitchPreference(setValue = ::launchAppAfterInstall::set, getValue = ::launchAppAfterInstall::get)
\ No newline at end of file
+ SwitchPreference(setValue = ::launchAppAfterInstall::set, getValue = ::launchAppAfterInstall::get)
+
+@Parcelize
+class GradleJDKVersionPreference(
+ override val key: String = PREF_JAVA_HOME,
+ override val title: Int = R.string.idepref_jdkVersion_title,
+ override val icon: Int? = R.drawable.ic_language_java,
+) : SingleChoicePreference() {
+
+ override fun getEntries(preference: Preference): Array {
+ val distributions = IJdkDistributionProvider.getInstance().installedDistributions
+ check(distributions.isNotEmpty()) {
+ "No JDK installations are available."
+ }
+
+ return distributions.map { dist ->
+ PreferenceChoices.Entry(dist.javaVersion, javaHome == dist.javaHome, dist)
+ }.toTypedArray()
+ }
+
+ override fun onChoiceConfirmed(
+ preference: Preference,
+ entry: PreferenceChoices.Entry,
+ position: Int
+ ) {
+ super.onChoiceConfirmed(preference, entry, position)
+ javaHome = (entry.data as JdkDistribution).javaHome
+ updatePreference(preference)
+ }
+
+ override fun onCreatePreference(context: Context): Preference {
+ return super.onCreatePreference(context).also { preference ->
+ updatePreference(preference)
+ }
+ }
+
+ private fun updatePreference(preference: Preference) {
+ val jdkDistProvider = IJdkDistributionProvider.getInstance()
+ val javaVersion = jdkDistProvider.forJavaHome(javaHome)?.javaVersion
+ ?: ""
+
+ preference.summary = preference.context.getString(R.string.idepref_jdkVersion_summary, javaVersion)
+ preference.isEnabled = jdkDistProvider.installedDistributions.size > 1
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/itsaky/androidide/utils/JdkUtils.kt b/app/src/main/java/com/itsaky/androidide/utils/JdkUtils.kt
new file mode 100644
index 0000000000..e8e6b90899
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/utils/JdkUtils.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.utils
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.itsaky.androidide.app.IDEApplication
+import com.itsaky.androidide.models.JdkDistribution
+import com.itsaky.androidide.shell.executeProcessAsync
+import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment
+import java.io.File
+import java.nio.file.Files
+
+/**
+ * Utilities related to JDK installations.
+ *
+ * @author Akash Yadav
+ */
+object JdkUtils {
+
+ private val log = ILogger.newInstance("JdkUtils")
+
+ /**
+ * Finds the available JDK installations and returns the JAVA_HOME for each installation.
+ */
+ @JvmStatic
+ @WorkerThread
+ fun findJavaInstallations(): List {
+
+ // a valid JDK can be installed anywhere in the file system
+ // however, we currently only check for installations that are located in $PREFIX/opt dir
+ // TODO: Find a way to efficiently list all JDK installations, including those which are located
+ // outside of $PREFIX/opt
+ return try {
+ val optDir = File(Environment.PREFIX, "opt")
+ if (!optDir.exists() || !optDir.isDirectory) {
+ emptyList()
+ } else {
+ optDir.listFiles()?.mapNotNull { dir ->
+ if (Files.isSymbolicLink(dir.toPath())) {
+ // ignore symbolic links
+ return@mapNotNull null
+ }
+
+ val java = File(dir, "bin/java")
+ if (!canExecute(java)) {
+ // java binary does not exist or is not executable
+ return@mapNotNull null
+ }
+
+ return@mapNotNull getDistFromJavaBin(java)
+ } ?: run {
+ log.error("Failed to list files in $optDir")
+ emptyList()
+ }
+ }
+ } catch (e: Exception) {
+ log.error("Failed to list java alternatives", e)
+ emptyList()
+ }
+ }
+
+ private fun canExecute(file: File): Boolean {
+ return file.exists() && file.isFile && file.canExecute()
+ }
+
+ /**
+ * Returns a [JdkDistribution] instances representing the JDK installation of the given
+ * `java` binary executable. This binary file is executed to extract the actual `java.home`
+ * value.
+ *
+ * @param java The path to the `java` binary executable.
+ * @return The [JdkDistribution] instance, or `null` if there was an error while getting required
+ * information from the installation.
+ */
+ @JvmStatic
+ fun getDistFromJavaBin(java: File): JdkDistribution? {
+ if (!java.exists() || !java.isFile || !java.canExecute()) {
+ log.error(
+ "Failed to lookup JDK installation. File '$java' does not exist or cannot be executed.")
+ return null
+ }
+
+ val properties = readProperties(java) ?: run {
+ log.error("Failed to retrieve Java properties from java binary: '$java'")
+ return null
+ }
+
+ return readDistFromProps(properties)
+ }
+
+ @VisibleForTesting
+ internal fun readDistFromProps(properties: String): JdkDistribution? {
+ val javaHome = Regex("java\\.home\\s*=\\s*(.*)").find(properties)?.groupValues?.get(1) ?: run {
+ log.error("Failed to determine property 'java.home'. Properties:", properties)
+ return null
+ }
+
+ log.debug("Found java.home=${javaHome}")
+
+ val javaVersion = Regex("java\\.version\\s*=\\s*(.*)").find(properties)?.groupValues?.get(1)
+ ?: run {
+ log.error("Failed to determine property 'java.version'. Properties:", properties)
+ return null
+ }
+
+ log.debug("Found java.version=${javaVersion}")
+
+ return JdkDistribution(javaVersion, javaHome)
+ }
+
+ /**
+ * Returns a [JdkDistribution] instance representing the JDK installation at the given
+ * location.
+ *
+ * @param javaHome The path to the installed JDK.
+ * @return The [JdkDistribution] instance, or `null` if there was an error while getting required
+ * information from the installation.
+ */
+ @JvmStatic
+ fun getDistFromJavaHome(javaHome: File): JdkDistribution? {
+ return getDistFromJavaBin(javaHome.resolve("bin/java"))
+ }
+
+ private fun readProperties(file: File): String? {
+ val propsCmd = "${file.absolutePath} -XshowSettings:properties -version"
+ val process = executeWithBash(propsCmd) ?: return null
+ return process.inputStream.bufferedReader().readText()
+ }
+
+ @WorkerThread
+ private fun executeWithBash(cmd: String): Process? {
+ val shell = Environment.BASH_SHELL
+
+ if (!canExecute(shell)) {
+ log.warn(
+ "Unable to determine JDK installations. Command ${shell.absolutePath} not found or is not executable.")
+ return null
+ }
+
+ val env = HashMap(TermuxShellEnvironment().getEnvironment(IDEApplication.instance, false))
+
+ return executeProcessAsync {
+ command = listOf(shell.absolutePath, "-c", cmd)
+ environment = env
+ redirectErrorStream = true
+ workingDirectory = Environment.HOME
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/itsaky/androidide/app/IDEBuildConfigProviderImplTest.kt b/app/src/test/java/com/itsaky/androidide/app/configuration/IDEBuildConfigProviderImplTest.kt
similarity index 97%
rename from app/src/test/java/com/itsaky/androidide/app/IDEBuildConfigProviderImplTest.kt
rename to app/src/test/java/com/itsaky/androidide/app/configuration/IDEBuildConfigProviderImplTest.kt
index 72a9abdda0..045324ec1e 100644
--- a/app/src/test/java/com/itsaky/androidide/app/IDEBuildConfigProviderImplTest.kt
+++ b/app/src/test/java/com/itsaky/androidide/app/configuration/IDEBuildConfigProviderImplTest.kt
@@ -15,14 +15,12 @@
* along with AndroidIDE. If not, see .
*/
-package com.itsaky.androidide.app
+package com.itsaky.androidide.app.configuration
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.BuildConfig.ABI_ARM64_V8A
import com.itsaky.androidide.BuildConfig.ABI_ARMEABI_V7A
import com.itsaky.androidide.BuildConfig.ABI_X86_64
-import com.itsaky.androidide.app.configuration.CpuArch
-import com.itsaky.androidide.app.configuration.IDEBuildConfigProviderImpl
import org.junit.Test
/**
diff --git a/app/src/test/java/com/itsaky/androidide/utils/JdkUtilsTest.kt b/app/src/test/java/com/itsaky/androidide/utils/JdkUtilsTest.kt
new file mode 100644
index 0000000000..20ad297599
--- /dev/null
+++ b/app/src/test/java/com/itsaky/androidide/utils/JdkUtilsTest.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.utils
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+/**
+ * @author Akash Yadav
+ */
+@RunWith(RobolectricTestRunner::class)
+class JdkUtilsTest {
+
+ companion object {
+
+ const val JDK_17_PROPS =
+ """
+ Property settings:
+ file.encoding = UTF-8
+ file.separator = /
+ java.class.path =
+ java.class.version = 61.0
+ java.home = /data/data/com.itsaky.androidide/files/usr/opt/openjdk-17.0
+ java.io.tmpdir = /data/data/com.itsaky.androidide/files/usr/tmp/
+ java.library.path = /data/data/com.itsaky.androidide/files/usr/java/packages/lib
+ /data/data/com.itsaky.androidide/files/usr/lib
+ java.runtime.name = OpenJDK Runtime Environment
+ java.runtime.version = 17-internal+0-adhoc.root.src
+ java.specification.name = Java Platform API Specification
+ java.specification.vendor = Oracle Corporation
+ java.specification.version = 17
+ java.vendor = N/A
+ java.vendor.url = https://openjdk.java.net/
+ java.vendor.url.bug = https://bugreport.java.com/bugreport/
+ java.version = 17-internal
+ java.version.date = 2021-09-14
+ java.vm.compressedOopsMode = 32-bit
+ java.vm.info = mixed mode
+ java.vm.name = OpenJDK 64-Bit Server VM
+ java.vm.specification.name = Java Virtual Machine Specification
+ java.vm.specification.vendor = Oracle Corporation
+ java.vm.specification.version = 17
+ java.vm.vendor = Oracle Corporation
+ java.vm.version = 17-internal+0-adhoc.root.src
+ jdk.debug = release
+ line.separator = \n
+ native.encoding = UTF-8
+ os.arch = aarch64
+ os.name = Linux
+ os.version = 4.9.305-ððð
+ path.separator = :
+ sun.arch.data.model = 64
+ sun.boot.library.path = /data/data/com.itsaky.androidide/files/usr/opt/openjdk-17.0/lib
+ sun.cpu.endian = little
+ sun.io.unicode.encoding = UnicodeLittle
+ sun.java.launcher = SUN_STANDARD
+ sun.jnu.encoding = UTF-8
+ sun.management.compiler = HotSpot 64-Bit Tiered Compilers
+ sun.stderr.encoding = UTF-8
+ sun.stdout.encoding = UTF-8
+ user.dir = /data/data/com.itsaky.androidide/files/home
+ user.home = /data/data/com.itsaky.androidide/files/home
+ user.language = en
+ user.name = u0_a248
+
+openjdk version "17-internal" 2021-09-14
+OpenJDK Runtime Environment (build 17-internal+0-adhoc.root.src)
+OpenJDK 64-Bit Server VM (build 17-internal+0-adhoc.root.src, mixed mode)
+ """
+
+ const val JDK_21_PROPS =
+ """
+ Property settings:
+ file.encoding = UTF-8
+ file.separator = /
+ java.class.path =
+ java.class.version = 65.0
+ java.home = /data/data/com.itsaky.androidide/files/usr/opt/openjdk-21.0.1
+ java.io.tmpdir = /data/data/com.itsaky.androidide/files/usr/tmp/
+ java.library.path = /data/data/com.itsaky.androidide/files/usr/java/packages/lib
+ /data/data/com.itsaky.androidide/files/usr/lib
+ java.runtime.name = OpenJDK Runtime Environment
+ java.runtime.version = 21.0.1-internal-adhoc.root.src
+ java.specification.name = Java Platform API Specification
+ java.specification.vendor = Oracle Corporation
+ java.specification.version = 21
+ java.vendor = N/A
+ java.vendor.url = https://openjdk.org/
+ java.vendor.url.bug = https://bugreport.java.com/bugreport/
+ java.version = 21.0.1-internal
+ java.version.date = 2023-10-17
+ java.vm.compressedOopsMode = 32-bit
+ java.vm.info = mixed mode
+ java.vm.name = OpenJDK 64-Bit Server VM
+ java.vm.specification.name = Java Virtual Machine Specification
+ java.vm.specification.vendor = Oracle Corporation
+ java.vm.specification.version = 21
+ java.vm.vendor = Oracle Corporation
+ java.vm.version = 21.0.1-internal-adhoc.root.src
+ jdk.debug = release
+ line.separator = \n
+ native.encoding = UTF-8
+ os.arch = aarch64
+ os.name = Linux
+ os.version = 4.9.305-ððð
+ path.separator = :
+ stderr.encoding = UTF-8
+ stdout.encoding = UTF-8
+ sun.arch.data.model = 64
+ sun.boot.library.path = /data/data/com.itsaky.androidide/files/usr/opt/openjdk-21.0.1/lib
+ sun.cpu.endian = little
+ sun.io.unicode.encoding = UnicodeLittle
+ sun.java.launcher = SUN_STANDARD
+ sun.jnu.encoding = UTF-8
+ sun.management.compiler = HotSpot 64-Bit Tiered Compilers
+ user.dir = /data/data/com.itsaky.androidide/files/home
+ user.home = /data/data/com.itsaky.androidide/files/home
+ user.language = en
+ user.name = u0_a248
+
+openjdk version "21.0.1-internal" 2023-10-17
+OpenJDK Runtime Environment (build 21.0.1-internal-adhoc.root.src)
+OpenJDK 64-Bit Server VM (build 21.0.1-internal-adhoc.root.src, mixed mode)
+ """
+
+ const val JDK_PROPS_EMPTY = ""
+ const val JDK_PROPS_INVALID = """
+ abcdef = ghijk
+ java.ver = 17-internal
+ java.hme = /somewhere/
+ """
+ }
+
+ @Test
+ fun `test parsing java home for JDK 17`() {
+ val dist = JdkUtils.readDistFromProps(JDK_17_PROPS)
+ assertThat(dist).isNotNull()
+ assertThat(dist!!.javaVersion).isEqualTo("17-internal")
+ assertThat(dist.javaHome).isEqualTo(
+ "/data/data/com.itsaky.androidide/files/usr/opt/openjdk-17.0")
+ }
+
+ @Test
+ fun `test parsing java home for JDK 21`() {
+ val dist = JdkUtils.readDistFromProps(JDK_21_PROPS)
+ assertThat(dist).isNotNull()
+ assertThat(dist!!.javaVersion).isEqualTo("21.0.1-internal")
+ assertThat(dist.javaHome).isEqualTo(
+ "/data/data/com.itsaky.androidide/files/usr/opt/openjdk-21.0.1")
+ }
+
+ @Test
+ fun `test parsing invalid props`() {
+ val dist = JdkUtils.readDistFromProps(JDK_PROPS_INVALID)
+ assertThat(dist).isNull()
+ }
+
+ @Test
+ fun `test parsing empty props`() {
+ val dist = JdkUtils.readDistFromProps(JDK_PROPS_EMPTY)
+ assertThat(dist).isNull()
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt b/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt
new file mode 100644
index 0000000000..07a285765b
--- /dev/null
+++ b/common/src/main/java/com/itsaky/androidide/app/configuration/IJdkDistributionProvider.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.app.configuration
+
+import com.itsaky.androidide.models.JdkDistribution
+import com.itsaky.androidide.utils.ServiceLoader
+
+/**
+ * Provides information about various JDK distributions installed on the device.
+ *
+ * @author Akash Yadav
+ */
+interface IJdkDistributionProvider {
+
+ /**
+ * The list of JDK distributions installed on the device.
+ */
+ val installedDistributions: List
+
+ /**
+ * Get the [JdkDistribution] instance for the given java version.
+ *
+ * @return The [JdkDistribution] instance for the given java version, or `null` if no such
+ * distribution is found.
+ */
+ fun forVersion(javaVersion: String) : JdkDistribution? =
+ installedDistributions.firstOrNull { it.javaVersion == javaVersion }
+
+
+ /**
+ * Get the [JdkDistribution] instance for the given java home.
+ *
+ * @return The [JdkDistribution] instance for the given java home, or `null` if no such
+ * distribution is found.
+ */
+ fun forJavaHome(javaHome: String) : JdkDistribution? =
+ installedDistributions.firstOrNull { it.javaHome == javaHome }
+
+ companion object {
+
+ /**
+ * The default java version.
+ */
+ const val DEFAULT_JAVA_VERSION = "17"
+
+ private val _instance by lazy {
+ ServiceLoader.load(
+ IJdkDistributionProvider::class.java,
+ IJdkDistributionProvider::class.java.classLoader
+ ).findFirstOrThrow()
+ }
+
+ /**
+ * Get instance of [IJdkDistributionProvider].
+ */
+ @JvmStatic
+ fun getInstance(): IJdkDistributionProvider = _instance
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java b/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java
index fb0180b048..81a0b0f737 100755
--- a/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java
+++ b/common/src/main/java/com/itsaky/androidide/managers/ToolsManager.java
@@ -23,6 +23,7 @@
import com.blankj.utilcode.util.ResourceUtils;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider;
+import com.itsaky.androidide.app.configuration.IJdkDistributionProvider;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
import java.io.File;
@@ -51,6 +52,9 @@ public static void init(@NonNull BaseApplication app, Runnable onFinish) {
}
CompletableFuture.runAsync(() -> {
+ // Load installed JDK distributions
+ IJdkDistributionProvider.getInstance().getInstalledDistributions();
+
writeNoMediaFile();
extractAapt2();
extractToolingApi();
diff --git a/common/src/main/java/com/itsaky/androidide/models/JdkDistribution.kt b/common/src/main/java/com/itsaky/androidide/models/JdkDistribution.kt
new file mode 100644
index 0000000000..5f9d8ee35c
--- /dev/null
+++ b/common/src/main/java/com/itsaky/androidide/models/JdkDistribution.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.models
+
+/**
+ * An installed JDK distribution.
+ *
+ * @param javaVersion The Java version.
+ * @param javaHome The path to the installed JDK.
+ * @author Akash Yadav
+ */
+data class JdkDistribution(
+ val javaVersion: String,
+ val javaHome: String
+)
diff --git a/common/src/main/java/com/itsaky/androidide/utils/Environment.java b/common/src/main/java/com/itsaky/androidide/utils/Environment.java
index 21136ae35c..f1204a46c7 100755
--- a/common/src/main/java/com/itsaky/androidide/utils/Environment.java
+++ b/common/src/main/java/com/itsaky/androidide/utils/Environment.java
@@ -19,6 +19,7 @@
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.FileUtils;
+import com.itsaky.androidide.syntax.highlighters.JavaHighlighter;
import java.io.File;
import java.util.Map;
import java.util.UUID;
@@ -31,7 +32,7 @@ public final class Environment {
public static final String DEFAULT_HOME = DEFAULT_ROOT + "/home";
private static final String DEFAULT_ANDROID_HOME = DEFAULT_HOME + "/android-sdk";
public static final String DEFAULT_PREFIX = DEFAULT_ROOT + "/usr";
- private static final String DEFAULT_JAVA_HOME = DEFAULT_PREFIX + "/opt/openjdk";
+ public static final String DEFAULT_JAVA_HOME = DEFAULT_PREFIX + "/opt/openjdk";
private static final String ANDROIDIDE_PROJECT_CACHE_DIR = ".androidide";
private static final ILogger LOG = ILogger.newInstance("Environment");
public static File ROOT;
@@ -57,7 +58,7 @@ public final class Environment {
public static File GRADLE_USER_HOME;
public static File AAPT2;
public static File JAVA;
- public static File SHELL;
+ public static File BASH_SHELL;
public static File LOGIN_SHELL;
public static void init() {
@@ -82,11 +83,11 @@ public static void init() {
JAVA_HOME = new File(DEFAULT_JAVA_HOME);
JAVA = new File(JAVA_HOME, "bin/java");
- SHELL = new File(BIN_DIR, "bash");
+ BASH_SHELL = new File(BIN_DIR, "bash");
LOGIN_SHELL = new File(BIN_DIR, "login");
setExecutable(JAVA);
- setExecutable(SHELL);
+ setExecutable(BASH_SHELL);
System.setProperty("user.home", HOME.getAbsolutePath());
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java
index 10db357745..9723a405a4 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java
+++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java
@@ -26,7 +26,6 @@
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
-
import com.itsaky.androidide.builder.model.IJavaCompilerSettings;
import com.itsaky.androidide.javac.services.compiler.ReusableBorrow;
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
@@ -35,12 +34,10 @@
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.util.StringSearch;
-import com.itsaky.androidide.tooling.api.IProject;
import com.itsaky.androidide.tooling.api.ProjectType;
import com.itsaky.androidide.utils.ClassTrie;
import com.itsaky.androidide.utils.SourceClassTrie;
import com.itsaky.androidide.utils.StopWatch;
-
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -54,7 +51,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
-
import jdkx.lang.model.SourceVersion;
import jdkx.tools.Diagnostic;
import jdkx.tools.JavaFileObject;
diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/internal/buildPreferences.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/internal/buildPreferences.kt
index d898cfd475..945ed054aa 100644
--- a/preferences/src/main/java/com/itsaky/androidide/preferences/internal/buildPreferences.kt
+++ b/preferences/src/main/java/com/itsaky/androidide/preferences/internal/buildPreferences.kt
@@ -17,6 +17,8 @@
package com.itsaky.androidide.preferences.internal
+import com.itsaky.androidide.utils.Environment
+
// Defined in PreferenceManager so that it could be accessible from Environment class
const val STACKTRACE = "idepref_gradleCmd_stacktrace"
@@ -31,6 +33,7 @@ const val GRADLE_COMMANDS = "idepref_build_gradleCommands"
const val GRADLE_CLEAR_CACHE = "idepref_build_gradleClearCache"
const val CUSTOM_GRADLE_INSTALLATION = "idepref_build_customGradleInstallation"
const val LAUNCH_APP_AFTER_INSTALL = "ide.build.run.launchAppAfterInstall"
+const val PREF_JAVA_HOME = "ide.build.javaHome"
/** Switch for Gradle `--debug` option. */
var isDebugEnabled: Boolean
@@ -95,4 +98,13 @@ var launchAppAfterInstall: Boolean
get() = prefManager.getBoolean(LAUNCH_APP_AFTER_INSTALL, false)
set(value) {
prefManager.putBoolean(LAUNCH_APP_AFTER_INSTALL, value)
+ }
+
+/**
+ * The selected Java installation.
+ */
+var javaHome : String
+ get() = prefManager.getString(PREF_JAVA_HOME, "")
+ set(value) {
+ prefManager.putString(PREF_JAVA_HOME, value)
}
\ No newline at end of file
diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml
index b31cf82427..02b0571561 100644
--- a/resources/src/main/res/values/strings.xml
+++ b/resources/src/main/res/values/strings.xml
@@ -557,4 +557,6 @@
No application available to handle intent.
Open source licenses
View open source licenses.
+ JDK version
+ Currently selected: %1$s (requires restart if changed)
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/Main.java b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/Main.java
index 8624d356a3..bf0bec4991 100644
--- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/Main.java
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/Main.java
@@ -59,6 +59,8 @@ public static void main(String[] args) {
server.connect(client);
LOG.debug("Server started. Will run until shutdown message is received...");
+ LOG.debug("Running on Java version:", System.getProperty("java.version", ""));
+
try {
Main.future.get();
} catch (CancellationException cancellationException) {