Skip to content
18 changes: 18 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@
<version>5.6.2</version>
<scope>test</scope>
</dependency>
<!-- HTML parsing -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>
<!-- Utils -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- Zip -->
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.2</version>
</dependency>
</dependencies>

<build>
Expand Down
21 changes: 19 additions & 2 deletions src/main/java/fr/bmarsaud/boxedroid/Boxedroid.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -178,6 +194,7 @@ public static void main(String[] args) {
}

Boxedroid boxedroid = new Boxedroid(sdkPath);
boxedroid.acquireCmdlineTools();
boxedroid.launchEmulator(androidVersion, abi, variant, device);
}
}
7 changes: 4 additions & 3 deletions src/main/java/fr/bmarsaud/boxedroid/service/AVDService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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/";
Expand All @@ -38,8 +39,8 @@ public class AVDService {
private List<AVD> 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<>();
Expand Down
170 changes: 170 additions & 0 deletions src/main/java/fr/bmarsaud/boxedroid/service/CmdlineToolsService.java
Original file line number Diff line number Diff line change
@@ -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("<br \\>", "\n")
.replace("<em>", "")
.replace("</em>", "");

// Add line breaks on titles
Matcher matcher = Pattern.compile("<h[23].*>(.*)</h[23]>").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 <a> links to plain text links
matcher = Pattern.compile("<a.*>(.*)</a>").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<String> 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";
}
}
51 changes: 51 additions & 0 deletions src/main/java/fr/bmarsaud/boxedroid/service/IOUtils.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
8 changes: 4 additions & 4 deletions src/main/java/fr/bmarsaud/boxedroid/service/SDKService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,12 +28,12 @@ public class SDKService {
private List<InstalledPackage> installedPackages;
private List<AvailableUpdates> 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<>();
Expand Down