Skip to content

Commit d700e05

Browse files
committed
Merge pull request #69 from imilka/exportSignedPackage
Merging export signed package feature from Imil
2 parents d829658 + 219177c commit d700e05

File tree

6 files changed

+571
-43
lines changed

6 files changed

+571
-43
lines changed

build.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<property name="core.jar.path" value="android-core.zip" />
99
<property name="mode.jar.path" value="mode/AndroidMode.jar" />
1010
<property name="mode.dist.path" value="release/AndroidMode.zip" />
11-
11+
<property name="tools.jar.path" value="mode/tools.jar" />
1212

1313
<target name="clean" description="Clean the build directories">
1414
<delete dir="bin" />

src/processing/mode/android/AndroidBuild.java

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@
2121

2222
package processing.mode.android;
2323

24-
import java.io.*;
25-
26-
import org.apache.tools.ant.*;
27-
28-
import processing.app.*;
29-
import processing.app.exec.*;
24+
import org.apache.tools.ant.BuildException;
25+
import org.apache.tools.ant.DefaultLogger;
26+
import org.apache.tools.ant.Project;
27+
import org.apache.tools.ant.ProjectHelper;
28+
import processing.app.Base;
29+
import processing.app.Library;
30+
import processing.app.Sketch;
31+
import processing.app.SketchException;
32+
import processing.app.exec.ProcessHelper;
33+
import processing.app.exec.ProcessResult;
3034
import processing.core.PApplet;
3135
import processing.mode.java.JavaBuild;
3236

37+
import java.io.*;
38+
import java.security.Permission;
39+
3340

3441
class AndroidBuild extends JavaBuild {
3542
// static final String basePackage = "changethispackage.beforesubmitting.tothemarket";
@@ -267,20 +274,95 @@ public File exportProject() throws IOException, SketchException {
267274
return null;
268275
}
269276

270-
271-
public boolean exportPackage() throws IOException, SketchException {
277+
public File exportPackage(String keyStorePassword) throws Exception {
272278
File projectFolder = build("release");
273-
if (projectFolder == null) {
274-
return false;
275-
}
279+
if(projectFolder == null) return null;
276280

277-
// TODO all the signing magic needs to happen here
281+
File signedPackage = signPackage(projectFolder, keyStorePassword);
282+
if(signedPackage == null) return null;
278283

279284
File exportFolder = createExportFolder();
280285
Base.copyDir(projectFolder, exportFolder);
281-
return true;
286+
return new File(exportFolder, "/bin/");
287+
}
288+
289+
private File signPackage(File projectFolder, String keyStorePassword) throws Exception {
290+
File keyStore = AndroidKeyStore.getKeyStore();
291+
if(keyStore == null) return null;
292+
293+
File unsignedPackage = new File(projectFolder, "bin/" + sketch.getName() + "-release-unsigned.apk");
294+
if(!unsignedPackage.exists()) return null;
295+
296+
JarSigner.signJar(unsignedPackage, AndroidKeyStore.ALIAS_STRING, keyStorePassword, keyStore.getAbsolutePath(), keyStorePassword);
297+
298+
//if(verifySignedPackage(unsignedPackage)) {
299+
File signedPackage = new File(projectFolder, "bin/" + sketch.getName() + "-release-signed.apk");
300+
if(signedPackage.exists()) {
301+
boolean deleteResult = signedPackage.delete();
302+
if(!deleteResult) {
303+
Base.showWarning("Error during package signing",
304+
"Unable to delete old signed package");
305+
return null;
306+
}
307+
}
308+
309+
boolean renameResult = unsignedPackage.renameTo(signedPackage);
310+
if(!renameResult) {
311+
Base.showWarning("Error during package signing",
312+
"Unable to rename package file");
313+
return null;
314+
}
315+
316+
File alignedPackage = zipalignPackage(signedPackage, projectFolder);
317+
return alignedPackage;
318+
/*} else {
319+
Base.showWarning("Error during package signing",
320+
"Verification of the signed package has failed");
321+
return null;
322+
}*/
282323
}
283324

325+
/*private boolean verifySignedPackage(File signedPackage) throws Exception {
326+
String[] args = {
327+
"-verify", signedPackage.getCanonicalPath()
328+
};
329+
330+
PrintStream defaultPrintStream = System.out;
331+
332+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
333+
PrintStream printStream = new PrintStream(baos);
334+
System.setOut(printStream);
335+
336+
SystemExitControl.forbidSystemExitCall();
337+
try {
338+
JarSigner.main(args);
339+
} catch (SystemExitControl.ExitTrappedException ignored) { }
340+
SystemExitControl.enableSystemExitCall();
341+
342+
System.setOut(defaultPrintStream);
343+
String result = baos.toString();
344+
345+
baos.close();
346+
printStream.close();
347+
348+
return result.contains("verified");
349+
} */
350+
351+
private File zipalignPackage(File signedPackage, File projectFolder) throws IOException, InterruptedException {
352+
String zipalignPath = sdk.getSdkFolder().getAbsolutePath() + "/tools/zipalign";
353+
File alignedPackage = new File(projectFolder, "bin/" + sketch.getName() + "-release-signed-aligned.apk");
354+
355+
String[] args = {
356+
zipalignPath, "-v", "-f", "4",
357+
signedPackage.getAbsolutePath(), alignedPackage.getAbsolutePath()
358+
};
359+
360+
Process alignProcess = Runtime.getRuntime().exec(args);
361+
alignProcess.waitFor();
362+
363+
if(alignedPackage.exists()) return alignedPackage;
364+
return null;
365+
}
284366

285367
/*
286368
// SDK tools 17 have a problem where 'dex' won't pick up the libs folder
@@ -815,3 +897,26 @@ public void cleanup() {
815897
tmpFolder.deleteOnExit();
816898
}
817899
}
900+
901+
// http://www.avanderw.co.za/preventing-calls-to-system-exit-in-java/
902+
class SystemExitControl {
903+
904+
public static class ExitTrappedException extends SecurityException {
905+
}
906+
907+
public static void forbidSystemExitCall() {
908+
final SecurityManager securityManager = new SecurityManager() {
909+
@Override
910+
public void checkPermission(Permission permission) {
911+
if (permission.getName().contains("exitVM")) {
912+
throw new ExitTrappedException();
913+
}
914+
}
915+
};
916+
System.setSecurityManager(securityManager);
917+
}
918+
919+
public static void enableSystemExitCall() {
920+
System.setSecurityManager(null);
921+
}
922+
}

src/processing/mode/android/AndroidEditor.java

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,16 @@
2121

2222
package processing.mode.android;
2323

24+
import processing.app.*;
25+
import processing.core.PApplet;
26+
import processing.mode.java.JavaEditor;
27+
28+
import javax.swing.*;
2429
import java.awt.event.ActionEvent;
2530
import java.awt.event.ActionListener;
2631
import java.io.File;
2732
import java.io.IOException;
2833

29-
import javax.swing.JMenu;
30-
import javax.swing.JMenuItem;
31-
32-
import processing.app.*;
33-
import processing.mode.java.JavaEditor;
34-
35-
import processing.core.PApplet;
36-
3734

3835
public class AndroidEditor extends JavaEditor {
3936
private AndroidMode androidMode;
@@ -376,27 +373,37 @@ public void run() {
376373
public void handleExportPackage() {
377374
// Need to implement an entire signing setup first
378375
// http://dev.processing.org/bugs/show_bug.cgi?id=1430
379-
statusError("Exporting signed packages is not yet implemented.");
380-
deactivateExport();
381-
382-
// make a release build
383-
// try {
384-
// buildReleaseForExport("release");
385-
// } catch (final MonitorCanceled ok) {
386-
// statusNotice("Canceled.");
387-
// } finally {
388-
// deactivateExport();
389-
// }
390-
391-
// TODO now sign it... lots of fun signing code mess to go here. yay!
376+
if(handleExportCheckModified()) {
377+
deactivateExport();
378+
new KeyStoreManager(this);
379+
}
380+
}
392381

393-
// maybe even send it to the device? mmm?
394-
// try {
395-
// runSketchOnDevice(AndroidEnvironment.getInstance().getHardware(), "release");
396-
// } catch (final MonitorCanceled ok) {
397-
// editor.statusNotice("Canceled.");
398-
// } finally {
399-
// editor.deactivateExport();
400-
// }
382+
public void startExportPackage(final String keyStorePassword) {
383+
new Thread() {
384+
public void run() {
385+
startIndeterminate();
386+
statusNotice("Exporting signed package...");
387+
AndroidBuild build = new AndroidBuild(sketch, androidMode);
388+
try {
389+
File projectFolder = build.exportPackage(keyStorePassword);
390+
if(projectFolder != null) {
391+
statusNotice("Done with export.");
392+
Base.openFolder(projectFolder);
393+
} else {
394+
statusError("Error with export");
395+
}
396+
} catch (IOException e) {
397+
statusError(e);
398+
} catch (SketchException e) {
399+
statusError(e);
400+
} catch (InterruptedException e) {
401+
e.printStackTrace();
402+
} catch (Exception e) {
403+
e.printStackTrace();
404+
}
405+
stopIndeterminate();
406+
}
407+
}.start();
401408
}
402409
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package processing.mode.android;
2+
3+
import processing.app.Base;
4+
5+
import java.io.File;
6+
7+
/**
8+
* Created with IntelliJ IDEA.
9+
* User: imilka
10+
* Date: 27.05.14
11+
* Time: 14:38
12+
*/
13+
public class AndroidKeyStore {
14+
15+
public static final String ALIAS_STRING = "processing-keystore";
16+
public static final String KEYSTORE_FILE_NAME = "android-release-key.keystore";
17+
18+
public static File getKeyStore() {
19+
File keyStore = getKeyStoreLocation();
20+
if(!keyStore.exists()) return null;
21+
return keyStore;
22+
}
23+
24+
public static File getKeyStoreLocation() {
25+
File sketchbookFolder = processing.app.Base.getSketchbookFolder();
26+
File keyStoreFolder = new File(sketchbookFolder, "keystore");
27+
if(!keyStoreFolder.exists()) {
28+
boolean result = keyStoreFolder.mkdirs();
29+
30+
if(!result) {
31+
Base.showWarning("Folders, folders, folders",
32+
"Could not create the necessary folders to build.\n" +
33+
"Perhaps you have some file permissions to sort out?", null);
34+
return null;
35+
}
36+
}
37+
38+
File keyStore = new File(keyStoreFolder, KEYSTORE_FILE_NAME);
39+
return keyStore;
40+
}
41+
42+
public static void generateKeyStore(String password,
43+
String commonName, String organizationalUnit,
44+
String organizationName, String locality,
45+
String state, String country) throws Exception {
46+
String dnamePlaceholder = "CN=%s, OU=%s, O=%s, L=%s, S=%s, C=%s";
47+
String dname = String.format(dnamePlaceholder,
48+
parseDnameField(commonName), parseDnameField(organizationalUnit), parseDnameField(organizationName),
49+
parseDnameField(locality), parseDnameField(state), parseDnameField(country));
50+
51+
String[] args = {
52+
System.getProperty("java.home")
53+
+ System.getProperty("file.separator") + "bin"
54+
+ System.getProperty("file.separator") + "keytool", "-genkey",
55+
"-keystore", getKeyStoreLocation().getAbsolutePath(),
56+
"-alias", ALIAS_STRING,
57+
"-keyalg", "RSA",
58+
"-keysize", "2048",
59+
"-validity", "10000",
60+
"-keypass", password,
61+
"-storepass", password,
62+
"-dname", dname
63+
};
64+
65+
Process generation = Runtime.getRuntime().exec(args);
66+
generation.waitFor();
67+
68+
if(getKeyStore() == null) throw new Exception();
69+
}
70+
71+
public static boolean resetKeyStore() {
72+
File keyStore = getKeyStore();
73+
if(keyStore == null) return true;
74+
75+
File keyStoreBackup = new File(processing.app.Base.getSketchbookFolder(), "keystore/" + KEYSTORE_FILE_NAME + "-" + AndroidMode.getDateStamp());
76+
if(!keyStore.renameTo(keyStoreBackup)) return false;
77+
return true;
78+
}
79+
80+
private static String parseDnameField(String content) {
81+
if(content == null || content.length() == 0) return "Unknown";
82+
else return content;
83+
}
84+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package processing.mode.android;
2+
3+
import org.apache.tools.ant.Project;
4+
import org.apache.tools.ant.Target;
5+
import org.apache.tools.ant.taskdefs.SignJar;
6+
7+
import java.io.File;
8+
9+
public class JarSigner extends SignJar {
10+
11+
public JarSigner() {
12+
setProject(new Project());
13+
getProject().init();
14+
setTaskName("signJar");
15+
setTaskType("signJar");
16+
setOwningTarget(new Target());
17+
}
18+
19+
public static void signJar(File jarToSign, String alias, String keypass, String keystore, String storepass) {
20+
JarSigner signer = new JarSigner();
21+
signer.setJar(jarToSign);
22+
signer.setAlias(alias);
23+
signer.setKeypass(keypass);
24+
signer.setKeystore(keystore);
25+
signer.setStorepass(storepass);
26+
signer.setSignedjar(jarToSign);
27+
signer.execute();
28+
}
29+
}

0 commit comments

Comments
 (0)