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<>();