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) {