Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

Commit

Permalink
fix: JDK is not recognized if only JDK 21 is installed (fixes #1616)
Browse files Browse the repository at this point in the history
This also adds a preference in 'Build & run' which allows user to select the currently installed JDK version
  • Loading branch information
itsaky committed Jan 11, 2024
1 parent 23819ee commit 51ef3b1
Show file tree
Hide file tree
Showing 15 changed files with 609 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ 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
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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<JdkDistribution> 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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -68,6 +73,7 @@ private class GradleOptions(
init {
addPreference(GradleCommands())
addPreference(GradleDistrubution())
addPreference(GradleJDKVersionPreference())
addPreference(GradleClearCache())
}
}
Expand Down Expand Up @@ -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)
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<PreferenceChoices.Entry> {
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
?: "<unknown>"

preference.summary = preference.context.getString(R.string.idepref_jdkVersion_summary, javaVersion)
preference.isEnabled = jdkDistProvider.installedDistributions.size > 1
}
}
165 changes: 165 additions & 0 deletions app/src/main/java/com/itsaky/androidide/utils/JdkUtils.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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

// 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/

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

/**
Expand Down
Loading

0 comments on commit 51ef3b1

Please sign in to comment.