diff --git a/pom.xml b/pom.xml index 9ef9fd5..f44fdbe 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,24 @@ 5.6.2 test + + + org.jsoup + jsoup + 1.15.3 + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + net.lingala.zip4j + zip4j + 2.11.2 + diff --git a/src/main/java/fr/bmarsaud/boxedroid/Boxedroid.java b/src/main/java/fr/bmarsaud/boxedroid/Boxedroid.java index 47ae140..0fedf6e 100644 --- a/src/main/java/fr/bmarsaud/boxedroid/Boxedroid.java +++ b/src/main/java/fr/bmarsaud/boxedroid/Boxedroid.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; @@ -24,6 +25,7 @@ import fr.bmarsaud.boxedroid.entity.exception.DeviceNotAvailableException; import fr.bmarsaud.boxedroid.entity.exception.SDKException; import fr.bmarsaud.boxedroid.service.AVDService; +import fr.bmarsaud.boxedroid.service.CmdlineToolsService; import fr.bmarsaud.boxedroid.service.SDKService; public class Boxedroid { @@ -33,6 +35,7 @@ public class Boxedroid { private Logger logger = LoggerFactory.getLogger(Boxedroid.class); private String sdkPath; + private String cmdlineToolsPath; public Boxedroid(String sdkPath) { this.sdkPath = sdkPath; @@ -60,8 +63,8 @@ public void launchEmulator(AndroidVersion version, ABI abi, Variant variant, Str System.exit(1); }; - SDKService sdkService = new SDKService(sdkPath); - AVDService avdService = new AVDService(sdkPath, currentDir); + SDKService sdkService = new SDKService(sdkPath, cmdlineToolsPath); + AVDService avdService = new AVDService(sdkPath, cmdlineToolsPath, currentDir); try { sdkService.install(version.getApiLevel(), abi, variant); @@ -79,6 +82,19 @@ public void launchEmulator(AndroidVersion version, ABI abi, Variant variant, Str } } + /** + * Find where cmdline-tools are installed. Install them if not found. + */ + public void acquireCmdlineTools() { + CmdlineToolsService cmdlineToolsService = new CmdlineToolsService(sdkPath); + try { + cmdlineToolsPath = cmdlineToolsService.acquireCmdlineTools(); + } catch (IOException e) { + logger.error(e.getMessage()); + System.exit(1); + } + } + public static void main(String[] args) { Options options = new Options(); options.addOption( @@ -178,6 +194,7 @@ public static void main(String[] args) { } Boxedroid boxedroid = new Boxedroid(sdkPath); + boxedroid.acquireCmdlineTools(); boxedroid.launchEmulator(androidVersion, abi, variant, device); } } diff --git a/src/main/java/fr/bmarsaud/boxedroid/service/AVDService.java b/src/main/java/fr/bmarsaud/boxedroid/service/AVDService.java index 0429096..b917e2e 100644 --- a/src/main/java/fr/bmarsaud/boxedroid/service/AVDService.java +++ b/src/main/java/fr/bmarsaud/boxedroid/service/AVDService.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -24,7 +25,7 @@ import fr.bmarsaud.boxedroid.util.IOUtils; public class AVDService { - private static final String AVD_MANAGER_PATH = "tools/bin/avdmanager"; + private static final String AVD_MANAGER_PATH = "avdmanager"; private static final String EMULATOR_PATH = "emulator/emulator"; private static final String AVD_DIR = "avd"; private static final String SYS_IMAGE_PREFIX = "Sdk/"; @@ -38,8 +39,8 @@ public class AVDService { private List avds; private String avdsPath; - public AVDService(String sdkPath, String boxedroidPath) { - this.avdManager = new AVDManager(new File(sdkPath, AVD_MANAGER_PATH).getAbsolutePath()); + public AVDService(String sdkPath, String cmdlineToolsPath, String boxedroidPath) { + this.avdManager = new AVDManager(Paths.get(sdkPath, cmdlineToolsPath, AVD_MANAGER_PATH).toAbsolutePath().toString()); this.emulator = new Emulator(new File(sdkPath, EMULATOR_PATH).getAbsolutePath(), sdkPath); this.avdsPath = new File(boxedroidPath, AVD_DIR).getAbsolutePath(); this.avds = new ArrayList<>(); diff --git a/src/main/java/fr/bmarsaud/boxedroid/service/CmdlineToolsService.java b/src/main/java/fr/bmarsaud/boxedroid/service/CmdlineToolsService.java new file mode 100644 index 0000000..7945b63 --- /dev/null +++ b/src/main/java/fr/bmarsaud/boxedroid/service/CmdlineToolsService.java @@ -0,0 +1,170 @@ +package fr.bmarsaud.boxedroid.service; + +import org.apache.commons.lang3.SystemUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CmdlineToolsService { + private static final String ANDROID_STUDIO_DOWNLOAD_PAGE = "https://developer.android.com/studio"; + + private final Logger logger = LoggerFactory.getLogger(CmdlineToolsService.class); + private final String sdkPath; + private Document downloadPageDocument; + + public CmdlineToolsService(String sdkPath) { + this.sdkPath = sdkPath; + } + + /** + * Checks if cmdline-tools are available from the SDK path. + * If not, download them from the Google website after asking the user to accept the agreements + * terms. + * @return The path to the cmdline-tools binaries + */ + public String acquireCmdlineTools() throws IOException { + String cmdlineToolsPath = resolveCmdlineToolsPath(sdkPath); + if(cmdlineToolsPath == null) { + logger.info("cmdline-tools not found in " + sdkPath); + logger.info("Do you want to download it from Google website? (y/n)"); + + String answer = IOUtils.readFromStandardInput(); + if (!"y".equalsIgnoreCase(answer)) { + System.exit(1); + } + + String terms = getCmdlineToolTextTerms(); + if(terms != null) { + logger.info(terms); + logger.info("I have read and agree with the above terms and conditions (y/n)"); + + answer = IOUtils.readFromStandardInput(); + if (!"y".equalsIgnoreCase(answer)) { + System.exit(1); + } + + String downloadUrl = getCmdlineToolsDownloadURL(); + Path downloadedFile = Paths.get(System.getProperty("java.io.tmpdir"), "cmdline-tools.zip"); + IOUtils.downloadFile(downloadUrl, downloadedFile); + IOUtils.unzipFile(downloadedFile.toFile(), Paths.get(sdkPath)); + + cmdlineToolsPath = resolveCmdlineToolsPath(sdkPath); + } + } + return cmdlineToolsPath; + } + + /** + * Extract cmdline-tools download URL for the current platform from the android studio download page + * @return The cmdline-tools download URL + */ + private String getCmdlineToolsDownloadURL() throws IOException { + Document document = getDocument(); + Element element = document.select(getDownloadModalSelector() + " a[data-action=download]").first(); + if(element != null) { + return element.attr("href"); + } + return null; + } + + /** + * Extract the HTML licence terms for the current platform from the android studio download page + * @return The cmdline-tools licence terms + */ + private String getCmdlineToolsHTMLTerms() throws IOException { + Document document = getDocument(); + Element element = document.select(getDownloadModalSelector() + " .sdk-terms").first(); + if(element != null) { + return element.html(); + } + return null; + } + + /** + * Extract the plain-text licence terms for the current platform from the android studio download page + * @return The cmdline-tools licence terms + */ + private String getCmdlineToolTextTerms() throws IOException { + String terms = getCmdlineToolsHTMLTerms(); + if(terms != null) { + terms = terms.replace("
", "\n") + .replace("", "") + .replace("", ""); + + // Add line breaks on titles + Matcher matcher = Pattern.compile("(.*)").matcher(terms); + while(matcher.find()) { + terms = terms.replace(matcher.group(0), "\n" + matcher.group(1) + "\n"); + } + + // Add line breaks on numbered sections + matcher = Pattern.compile(" \\d+\\.\\d+ ").matcher(terms); + while(matcher.find()) { + terms = terms.replace(matcher.group(0), "\n" + matcher.group(0)); + } + + // Transform links to plain text links + matcher = Pattern.compile("(.*)").matcher(terms); + while(matcher.find()) { + terms = terms.replace(matcher.group(0), matcher.group(1)); + } + } + return terms; + } + + /** + * Find the correct path to cmdline-tools binaries. + * In some versions of the Android SDK, cmdline-tools binaries are under the `latest` directory. + * @param sdkPath The path to the Android SDK + * @return The path to cmdline-tools binaries + */ + private String resolveCmdlineToolsPath(String sdkPath) { + List pathsToCheck = Arrays.asList( + "cmdline-tools/bin", + "cmdline-tools/latest/bin" + ); + for(String path : pathsToCheck) { + if(Paths.get(sdkPath, path, "sdkmanager").toFile().exists()) { + return path; + } + } + return null; + } + + /** + * Retrieve the Android Studio download page document + * @return The download page document + */ + private Document getDocument() throws IOException { + if(downloadPageDocument == null) { + downloadPageDocument = Jsoup.connect(ANDROID_STUDIO_DOWNLOAD_PAGE).get(); + } + return downloadPageDocument; + } + + /** + * Get cmdline-tools download modal selector from the current platform + * @return The download modal selector + */ + private String getDownloadModalSelector() { + String platform = ""; + if(SystemUtils.IS_OS_LINUX) { + platform = "linux"; + } else if(SystemUtils.IS_OS_MAC) { + platform = "mac"; + } else if(SystemUtils.IS_OS_WINDOWS) { + platform = "win"; + } + return "#sdk_" + platform + "_download"; + } +} diff --git a/src/main/java/fr/bmarsaud/boxedroid/service/IOUtils.java b/src/main/java/fr/bmarsaud/boxedroid/service/IOUtils.java new file mode 100644 index 0000000..069e090 --- /dev/null +++ b/src/main/java/fr/bmarsaud/boxedroid/service/IOUtils.java @@ -0,0 +1,51 @@ +package fr.bmarsaud.boxedroid.service; + +import net.lingala.zip4j.ZipFile; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; + +public class IOUtils { + /** + * Download a file from a source to a destination. + * @param sourceUrl The source URL to download the file from. + * @param destination The destination to download the file to. + */ + public static void downloadFile(String sourceUrl, Path destination) throws IOException { + try(ReadableByteChannel readableByteChannel = Channels.newChannel(new URL(sourceUrl).openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(destination.toFile())) { + fileOutputStream.getChannel().transferFrom(readableByteChannel, 0, Long.MAX_VALUE); + } + } + + /** + * Unzip all files from a zip file to a destination. + * @param file The zip file to unzip. + * @param destination The destination where to unzip the archive. + */ + public static void unzipFile(File file, Path destination) throws IOException { + try(ZipFile zipFile = new ZipFile(file)) { + zipFile.extractAll(destination.toString()); + } + } + + /** + * Read the first line from the standard input. + * @return The first line of the standard input, null if an {@link IOException} occurred. + */ + public static String readFromStandardInput() { + try { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + return br.readLine(); + } catch (IOException e) { + return null; + } + } +} diff --git a/src/main/java/fr/bmarsaud/boxedroid/service/SDKService.java b/src/main/java/fr/bmarsaud/boxedroid/service/SDKService.java index 6a25122..d2aee8f 100644 --- a/src/main/java/fr/bmarsaud/boxedroid/service/SDKService.java +++ b/src/main/java/fr/bmarsaud/boxedroid/service/SDKService.java @@ -3,8 +3,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -28,12 +28,12 @@ public class SDKService { private List installedPackages; private List availableUpdates; - private static final String SDK_MANAGER_PATH = "tools/bin/sdkmanager"; + private static final String SDK_MANAGER_PATH = "sdkmanager"; private SDKManager sdkManager; - public SDKService(String sdkPath) { - this.sdkManager = new SDKManager(new File(sdkPath, SDK_MANAGER_PATH).getAbsolutePath(), sdkPath); + public SDKService(String sdkPath, String cmdlineToolsPath) { + this.sdkManager = new SDKManager(Paths.get(sdkPath, cmdlineToolsPath, SDK_MANAGER_PATH).toAbsolutePath().toString(), sdkPath); this.availablePackages = new ArrayList<>(); this.installedPackages = new ArrayList<>(); this.availableUpdates = new ArrayList<>();