package processing.mode.android;
import processing.app.Base;
import processing.app.Platform;
import processing.app.Preferences;
import processing.app.exec.ProcessHelper;
import processing.app.exec.ProcessResult;
import processing.core.PApplet;
import javax.swing.*;
import java.awt.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
class AndroidSDK {
private final File folder;
private final File tools;
private final File platforms;
private final File platformTools;
private final File androidTool;
private static final String ANDROID_SDK_PRIMARY =
"Is the Android SDK installed?";
private static final String ANDROID_SDK_SECONDARY =
"The Android SDK does not appear to be installed,
" +
"because the ANDROID_SDK variable is not set.
" +
"If it is installed, click “Locate SDK path” to select the
" +
"location of the SDK, or “Download SDK” to let
" +
"Processing download SDK automatically.
" +
"If you want to download SDK manually, you can visit
"+
"download site at http://developer.android.com/sdk.";
private static final String SELECT_ANDROID_SDK_FOLDER =
"Choose the location of the Android SDK";
private static final String NOT_ANDROID_SDK =
"The selected folder does not appear to contain an Android SDK,\n" +
"or the SDK needs to be updated to the latest version.";
// private static final String ANDROID_SDK_URL =
// "http://developer.android.com/sdk/";
public AndroidSDK(File folder) throws BadSDKException, IOException {
this.folder = folder;
if (!folder.exists()) {
throw new BadSDKException(folder + " does not exist");
}
tools = new File(folder, "tools");
if (!tools.exists()) {
throw new BadSDKException("There is no tools folder in " + folder);
}
platformTools = new File(folder, "platform-tools");
if (!platformTools.exists()) {
throw new BadSDKException("There is no platform-tools folder in " + folder);
}
platforms = new File(folder, "platforms");
if (!platforms.exists()) {
throw new BadSDKException("There is no platforms folder in " + folder);
}
androidTool = findAndroidTool(tools);
final Platform p = Base.getPlatform();
String path = p.getenv("PATH");
p.setenv("ANDROID_SDK", folder.getCanonicalPath());
path = platformTools.getCanonicalPath() + File.pathSeparator +
tools.getCanonicalPath() + File.pathSeparator + path;
String javaHomeProp = System.getProperty("java.home");
File javaHome = new File(javaHomeProp).getCanonicalFile();
p.setenv("JAVA_HOME", javaHome.getCanonicalPath());
path = new File(javaHome, "bin").getCanonicalPath() + File.pathSeparator + path;
p.setenv("PATH", path);
checkDebugCertificate();
}
/**
* If a debug certificate exists, check its expiration date. If it's expired,
* remove it so that it doesn't cause problems during the build.
*/
protected void checkDebugCertificate() {
File dotAndroidFolder = new File(System.getProperty("user.home"), ".android");
File keystoreFile = new File(dotAndroidFolder, "debug.keystore");
if (keystoreFile.exists()) {
// keytool -list -v -storepass android -keystore debug.keystore
ProcessHelper ph = new ProcessHelper(new String[] {
"keytool", "-list", "-v",
"-storepass", "android",
"-keystore", keystoreFile.getAbsolutePath()
});
try {
ProcessResult result = ph.execute();
if (result.succeeded()) {
// Valid from: Mon Nov 02 15:38:52 EST 2009 until: Tue Nov 02 16:38:52 EDT 2010
String[] lines = PApplet.split(result.getStdout(), '\n');
for (String line : lines) {
String[] m = PApplet.match(line, "Valid from: .* until: (.*)");
if (m != null) {
String timestamp = m[1].trim();
// "Sun Jan 22 11:09:08 EST 2012"
// Hilariously, this is the format of Date.toString(), however
// it isn't the default for SimpleDateFormat or others. Yay!
DateFormat df = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
try {
Date date = df.parse(timestamp);
long expireMillis = date.getTime();
if (expireMillis < System.currentTimeMillis()) {
System.out.println("Removing expired debug.keystore file.");
String hidingName = "debug.keystore." + AndroidMode.getDateStamp(expireMillis);
File hidingFile = new File(keystoreFile.getParent(), hidingName);
if (!keystoreFile.renameTo(hidingFile)) {
System.err.println("Could not remove the expired debug.keystore file.");
System.err.println("Please remove the file " + keystoreFile.getAbsolutePath());
}
// } else {
// System.out.println("Nah, that won't expire until " + date); //timestamp);
}
} catch (ParseException pe) {
System.err.println("The date “" + timestamp + "” could not be parsed.");
System.err.println("Please report this as a bug so we can fix it.");
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public File getAndroidTool() {
return androidTool;
}
public String getAndroidToolPath() {
return androidTool.getAbsolutePath();
}
public File getSdkFolder() {
return folder;
}
/*
public File getToolsFolder() {
return tools;
}
*/
public File getPlatformToolsFolder() {
return platformTools;
}
/**
* Checks a path to see if there's a tools/android file inside, a rough check
* for the SDK installation. Also figures out the name of android/android.bat
* so that it can be called explicitly.
*/
private static File findAndroidTool(final File tools) throws BadSDKException {
if (new File(tools, "android.exe").exists()) {
return new File(tools, "android.exe");
}
if (new File(tools, "android.bat").exists()) {
return new File(tools, "android.bat");
}
if (new File(tools, "android").exists()) {
return new File(tools, "android");
}
throw new BadSDKException("Cannot find the android tool in " + tools);
}
/**
* Check for the ANDROID_SDK environment variable. If the variable is set,
* and refers to a legitimate Android SDK, then use that and save the pref.
*
* Check for a previously set android.sdk.path preference. If the pref
* is set, and refers to a legitimate Android SDK, then use that.
*
* Prompt the user to select an Android SDK. If the user selects a
* legitimate Android SDK, then use that, and save the preference.
*
* @return an AndroidSDK
* @throws BadSDKException
* @throws IOException
*/
public static AndroidSDK load() throws BadSDKException, IOException {
final Platform platform = Base.getPlatform();
// The environment variable is king. The preferences.txt entry is a page.
final String sdkEnvPath = platform.getenv("ANDROID_SDK");
if (sdkEnvPath != null) {
try {
final AndroidSDK androidSDK = new AndroidSDK(new File(sdkEnvPath));
// Set this value in preferences.txt, in case ANDROID_SDK
// gets knocked out later. For instance, by that pesky Eclipse,
// which nukes all env variables when launching from the IDE.
Preferences.set("android.sdk.path", sdkEnvPath);
return androidSDK;
} catch (final BadSDKException drop) { }
}
// If android.sdk.path exists as a preference, make sure that the folder
// is not bogus, otherwise the SDK may have been removed or deleted.
final String sdkPrefsPath = Preferences.get("android.sdk.path");
if (sdkPrefsPath != null) {
try {
final AndroidSDK androidSDK = new AndroidSDK(new File(sdkPrefsPath));
// Set this value in preferences.txt, in case ANDROID_SDK
// gets knocked out later. For instance, by that pesky Eclipse,
// which nukes all env variables when launching from the IDE.
Preferences.set("android.sdk.path", sdkPrefsPath);
return androidSDK;
} catch (final BadSDKException wellThatsThat) {
Preferences.unset("android.sdk.path");
}
}
return null;
}
static public AndroidSDK locate(final Frame window, final AndroidMode androidMode)
throws BadSDKException, IOException {
final int result = showLocateDialog(window);
if (result == JOptionPane.CANCEL_OPTION) {
throw new BadSDKException("User canceled attempt to find SDK.");
}
if (result == JOptionPane.YES_OPTION) {
// here we are going to download sdk automatically
//Base.openURL(ANDROID_SDK_URL);
//throw new BadSDKException("No SDK installed.");
return download(androidMode);
}
while (true) {
// TODO this is really a yucky way to do this stuff. fix it.
File folder = selectFolder(SELECT_ANDROID_SDK_FOLDER, null, window);
if (folder == null) {
throw new BadSDKException("User canceled attempt to find SDK.");
}
try {
final AndroidSDK androidSDK = new AndroidSDK(folder);
Preferences.set("android.sdk.path", folder.getAbsolutePath());
return androidSDK;
} catch (final BadSDKException nope) {
JOptionPane.showMessageDialog(window, NOT_ANDROID_SDK);
}
}
}
static public AndroidSDK download(final AndroidMode androidMode) throws BadSDKException {
AndroidMode.sdkDownloadInProgress = true;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
SDKDownloader downloader = new SDKDownloader(androidMode);
downloader.startDownload();
}
});
return null;
}
static public int showLocateDialog(Frame editor) {
// Pane formatting adapted from the Quaqua guide
// http://www.randelshofer.ch/quaqua/guide/joptionpane.html
JOptionPane pane =
new JOptionPane(" " +
"
" + ANDROID_SDK_SECONDARY + "
", JOptionPane.QUESTION_MESSAGE); String[] options = new String[] { "Download SDK automatically", "Locate SDK path manually" }; pane.setOptions(options); // highlight the safest option ala apple hig pane.setInitialValue(options[0]); JDialog dialog = pane.createDialog(editor, null); dialog.setVisible(true); Object result = pane.getValue(); if (result == options[0]) { return JOptionPane.YES_OPTION; } else if (result == options[1]) { return JOptionPane.NO_OPTION; } else { return JOptionPane.CLOSED_OPTION; } } // this was banished from Base because it encourages bad practice. // TODO figure out a better way to handle the above. static public File selectFolder(String prompt, File folder, Frame frame) { if (Base.isMacOS()) { if (frame == null) frame = new Frame(); //.pack(); FileDialog fd = new FileDialog(frame, prompt, FileDialog.LOAD); if (folder != null) { fd.setDirectory(folder.getParent()); //fd.setFile(folder.getName()); } System.setProperty("apple.awt.fileDialogForDirectories", "true"); fd.setVisible(true); System.setProperty("apple.awt.fileDialogForDirectories", "false"); if (fd.getFile() == null) { return null; } return new File(fd.getDirectory(), fd.getFile()); } else { JFileChooser fc = new JFileChooser(); fc.setDialogTitle(prompt); if (folder != null) { fc.setSelectedFile(folder); } fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int returned = fc.showOpenDialog(frame); if (returned == JFileChooser.APPROVE_OPTION) { return fc.getSelectedFile(); } } return null; } private static final String ADB_DAEMON_MSG_1 = "daemon not running"; private static final String ADB_DAEMON_MSG_2 = "daemon started successfully"; public static ProcessResult runADB(final String... cmd) throws InterruptedException, IOException { final String[] adbCmd; if (!cmd[0].equals("adb")) { adbCmd = PApplet.splice(cmd, "adb", 0); } else { adbCmd = cmd; } // printing this here to see if anyone else is killing the adb server if (processing.app.Base.DEBUG) { PApplet.printArray(adbCmd); } // try { ProcessResult adbResult = new ProcessHelper(adbCmd).execute(); // Ignore messages about starting up an adb daemon String out = adbResult.getStdout(); if (out.contains(ADB_DAEMON_MSG_1) && out.contains(ADB_DAEMON_MSG_2)) { StringBuilder sb = new StringBuilder(); for (String line : out.split("\n")) { if (!out.contains(ADB_DAEMON_MSG_1) && !out.contains(ADB_DAEMON_MSG_2)) { sb.append(line).append("\n"); } } return new ProcessResult(adbResult.getCmd(), adbResult.getResult(), sb.toString(), adbResult.getStderr(), adbResult.getTime()); } return adbResult; // } catch (IOException ioe) { // ioe.printStackTrace(); // throw ioe; // } } public static class SDKTarget { public int version = 0; public String name; } public ArrayList