diff --git a/mode/languages/mode.properties b/mode/languages/mode.properties
index fc025df5..ef7805e2 100644
--- a/mode/languages/mode.properties
+++ b/mode/languages/mode.properties
@@ -13,6 +13,7 @@
menu.file.export_signed_package = Export Signed Package
menu.file.export_android_project = Export Android Project
+menu.file.export_signed_bundle = Export Signed Bundle
# | File | Edit | Sketch | Android | Tools | Help |
# | Android |
@@ -61,8 +62,11 @@ android_editor.status.exporting_project = Exporting an Android project of the sk
android_editor.status.project_export_completed = Done with project export.
android_editor.status.project_export_failed = Error with project export.
android_editor.status.exporting_package = Exporting signed package...
+android_editor.status.exporting_bundle = Exporting signed bundle...
android_editor.status.package_export_completed = Done with package export.
+android_editor.status.bundle_export_completed = Done with bundle export.
android_editor.status.package_export_failed = Error with package export.
+android_editor.status.bundle_export_failed = Error with bundle export.
android_editor.error.cannot_create_sketch_properties = Error While creating sketch properties file "%s": %s
# ---------------------------------------
@@ -84,8 +88,10 @@ android_mode.dialog.wallpaper_installed_body = Processing just built and install
android_mode.dialog.watchface_installed_title = Watch face installed!
android_mode.dialog.watchface_installed_body = Processing just built and installed your sketch as a watch face on the selected device.
You need to add it as a favourite watch face on the device and then select it from the watch face picker in order to run it.
android_mode.dialog.cannot_export_package_title = Cannot export package...
+android_mode.dialog.cannot_export_bundle_title = Cannot export bundle...
android_mode.dialog.cannot_export_package_body = The sketch still has the default package name. Not good, since this name will uniquely identify your app on the Play store... for ever! Come up with a different package name and write in the AndroidManifest.xml file in the sketch folder, after the "package=" attribute inside the manifest tag, which also contains version code and name. Once you have done that, try exporting the sketch again.
For more info on distributing apps from Processing,
check this online tutorial.
android_mode.dialog.cannot_use_default_icons_title = Cannot export package...
+android_mode.dialog.cannot_use_default_icons_title_bundle = Cannot export bundle...
android_mode.dialog.cannot_use_default_icons_body = The sketch does not include all required app icons. Processing could use its default set of Android icons, which are okay to test the app on your device, but a bad idea to distribute it on the Play store. Create a full set of unique icons for your app, and copy them into the sketch folder. Once you have done that, try exporting the sketch again.
For more info on distributing apps from Processing,
check this online tutorial.
android_mode.warn.cannot_load_sdk_title = Bad news...
android_mode.warn.cannot_load_sdk_body = The Android SDK could not be loaded.\nThe Android Mode will be disabled.
diff --git a/mode/languages/mode_ko.properties b/mode/languages/mode_ko.properties
index bac8efac..24859b16 100644
--- a/mode/languages/mode_ko.properties
+++ b/mode/languages/mode_ko.properties
@@ -13,6 +13,7 @@
menu.file.export_signed_package = 서명 된 패키지 내보내기
menu.file.export_android_project = 안드로이드 프로젝트 내보내기
+menu.file.export_signed_bundle = 안드로이드 번들 내보내기
# | File | Edit | Sketch | Android | Tools | Help |
# | Android |
@@ -27,4 +28,4 @@ menu.android.ar = AR
menu.android.devices = 장치들
menu.android.devices.no_connected_devices = 연결된 기기 없음
menu.android.sdk_updater = SDK 업데이터
-menu.android.reset_adb = ADB 재설정
\ No newline at end of file
+menu.android.reset_adb = ADB 재설정
diff --git a/mode/src/processing/mode/android/AndroidBuild.java b/mode/src/processing/mode/android/AndroidBuild.java
index 379dd321..72a2d1a2 100644
--- a/mode/src/processing/mode/android/AndroidBuild.java
+++ b/mode/src/processing/mode/android/AndroidBuild.java
@@ -188,7 +188,21 @@ public String getPathForAPK() {
return null;
}
return apkFile.getAbsolutePath();
- }
+ }
+
+ /**
+ * Build into temporary folders for building bundles (needed for the Windows 8.3 bugs in the Android SDK)
+ * @param target "debug" or "release"
+ * @throws SketchException
+ * @throws IOException
+ */
+ public File buildBundle(String target) throws IOException, SketchException {
+ this.target = target;
+ File folder = createProject(true);
+ if (folder == null) return null;
+ if (!gradleBuildBundle()) return null;
+ return folder;
+ }
/**
@@ -242,6 +256,44 @@ protected File createProject(boolean external)
return tmpFolder;
}
+
+ protected boolean gradleBuildBundle() throws SketchException {
+ ProjectConnection connection = GradleConnector.newConnector()
+ .forProjectDirectory(tmpFolder)
+ .connect();
+
+ boolean success = false;
+ BuildLauncher build = connection.newBuild();
+ build.setStandardOutput(System.out);
+ build.setStandardError(System.err);
+
+ try {
+ if (target.equals("debug")) build.forTasks("bundleDebug");
+ else build.forTasks("bundleRelease");
+ build.run();
+ renameAAB();
+ success = true;
+ } catch (org.gradle.tooling.UnsupportedVersionException e) {
+ e.printStackTrace();
+ success = false;
+ } catch (org.gradle.tooling.BuildException e) {
+ e.printStackTrace();
+ success = false;
+ } catch (org.gradle.tooling.BuildCancelledException e) {
+ e.printStackTrace();
+ success = false;
+ } catch (org.gradle.tooling.GradleConnectionException e) {
+ e.printStackTrace();
+ success = false;
+ } catch (Exception e) {
+ e.printStackTrace();
+ success = false;
+ } finally {
+ connection.close();
+ }
+
+ return success;
+ }
protected boolean gradleBuild() throws SketchException {
@@ -676,7 +728,25 @@ public File exportProject() throws IOException, SketchException {
Util.copyDir(projectFolder, exportFolder);
installGradlew(exportFolder);
return exportFolder;
- }
+ }
+
+
+ // ---------------------------------------------------------------------------
+ // Export bundle
+
+
+ public File exportBundle(String keyStorePassword) throws Exception {
+ File projectFolder = buildBundle("release");
+ if (projectFolder == null) return null;
+
+ File signedPackage = signPackage(projectFolder, keyStorePassword, "aab");
+ if (signedPackage == null) return null;
+
+ // Final export folder
+ File exportFolder = createExportFolder("buildBundle");
+ Util.copyDir(new File(projectFolder, getPathToAAB()), exportFolder);
+ return exportFolder;
+ }
// ---------------------------------------------------------------------------
@@ -687,7 +757,7 @@ public File exportPackage(String keyStorePassword) throws Exception {
File projectFolder = build("release");
if (projectFolder == null) return null;
- File signedPackage = signPackage(projectFolder, keyStorePassword);
+ File signedPackage = signPackage(projectFolder, keyStorePassword, "apk");
if (signedPackage == null) return null;
// Final export folder
@@ -697,26 +767,34 @@ public File exportPackage(String keyStorePassword) throws Exception {
}
- private File signPackage(File projectFolder, String keyStorePassword) throws Exception {
+ private File signPackage(File projectFolder, String keyStorePassword, String fileExt) throws Exception {
File keyStore = AndroidKeyStore.getKeyStore();
if (keyStore == null) return null;
-
- File unsignedPackage = new File(projectFolder,
- getPathToAPK() + sketch.getName().toLowerCase() + "_release_unsigned.apk");
- if (!unsignedPackage.exists()) return null;
- File signedPackage = new File(projectFolder,
- getPathToAPK() + sketch.getName().toLowerCase() + "_release_signed.apk");
+
+ String path=getPathToAPK();
+ if(fileExt.equals("aab")){
+ path = getPathToAAB();
+ }
+ File unsignedPackage = new File(projectFolder,
+ path + sketch.getName().toLowerCase() + "_release_unsigned." + fileExt);
+ if (!unsignedPackage.exists()) return null;
+ File signedPackage = new File(projectFolder,
+ path + sketch.getName().toLowerCase() + "_release_signed." + fileExt);
+
JarSigner.signJar(unsignedPackage, signedPackage,
AndroidKeyStore.ALIAS_STRING, keyStorePassword,
keyStore.getAbsolutePath(), keyStorePassword);
- File alignedPackage = zipalignPackage(signedPackage, projectFolder);
+ if(fileExt.equals("aab")){
+ return signedPackage;
+ }
+ File alignedPackage = zipalignPackage(signedPackage, projectFolder, fileExt);
return alignedPackage;
}
- private File zipalignPackage(File signedPackage, File projectFolder)
+ private File zipalignPackage(File signedPackage, File projectFolder, String fileExt)
throws IOException, InterruptedException {
File zipAlign = sdk.getZipAlignTool();
if (zipAlign == null || !zipAlign.exists()) {
@@ -724,9 +802,10 @@ private File zipalignPackage(File signedPackage, File projectFolder)
AndroidMode.getTextString("android_build.warn.cannot_find_zipalign.body"));
return null;
}
+
File alignedPackage = new File(projectFolder,
- getPathToAPK() + sketch.getName().toLowerCase() + "_release_signed_aligned.apk");
+ getPathToAPK() + sketch.getName().toLowerCase() + "_release_signed_aligned."+fileExt);
String[] args = {
zipAlign.getAbsolutePath(), "-v", "-f", "4",
@@ -841,7 +920,24 @@ private void copyCodeFolder(final File libsFolder) throws IOException {
}
}
}
- }
+ }
+
+ private void renameAAB() {
+ String suffix = target.equals("release") ? "release" : "debug";
+ String aabName = getPathToAAB() + module + "-" + suffix + ".aab";
+ final File aabFile = new File(tmpFolder, aabName);
+ if (aabFile.exists()) {
+ String suffixNew = target.equals("release") ? "release_unsigned" : "debug";
+ String aabNameNew = getPathToAAB() +
+ sketch.getName().toLowerCase() + "_" + suffixNew + ".aab";
+ final File aabFileNew = new File(tmpFolder, aabNameNew);
+ aabFile.renameTo(aabFileNew);
+ }
+ }
+
+ private String getPathToAAB() {
+ return module + "/build/outputs/bundle/" + target + "/";
+ }
private void renameAPK() {
@@ -1014,4 +1110,4 @@ static private boolean versionCheck(String currentVersion, String minVersion) {
return false;
}
-}
\ No newline at end of file
+}
diff --git a/mode/src/processing/mode/android/AndroidEditor.java b/mode/src/processing/mode/android/AndroidEditor.java
index e9c36639..74a28720 100644
--- a/mode/src/processing/mode/android/AndroidEditor.java
+++ b/mode/src/processing/mode/android/AndroidEditor.java
@@ -136,7 +136,15 @@ public void actionPerformed(ActionEvent e) {
}
});
- return buildFileMenu(new JMenuItem[] { exportPackage, exportProject});
+ String exportBundleTitle = AndroidToolbar.getTitle(AndroidToolbar.EXPORT_BUNDLE, false);
+ JMenuItem exportBundle = Toolkit.newJMenuItem(exportBundleTitle, 'B');
+ exportBundle.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ handleExportBundle();
+ }
+ });
+
+ return buildFileMenu(new JMenuItem[] { exportPackage, exportProject, exportBundle});
}
@@ -496,15 +504,53 @@ public void run() {
}
}
+ /**
+ * Create a release bundle of the sketch
+ */
+ public void handleExportBundle() {
+ if (androidMode.checkPackageName(sketch, appComponent, true) &&
+ androidMode.checkAppIcons(sketch, appComponent, true) && handleExportCheckModified()) {
+ new KeyStoreManager(this, true);
+ }
+ }
+
+ public void startExportBundle(final String keyStorePassword) {
+ new Thread() {
+ public void run() {
+ startIndeterminate();
+ statusNotice(AndroidMode.getTextString("android_editor.status.exporting_bundle"));
+ AndroidBuild build = new AndroidBuild(sketch, androidMode, appComponent);
+ try {
+ File projectFolder = build.exportBundle(keyStorePassword);
+ if (projectFolder != null) {
+
+ statusNotice(AndroidMode.getTextString("android_editor.status.bundle_export_completed"));
+ Platform.openFolder(projectFolder);
+ } else {
+ statusError(AndroidMode.getTextString("android_editor.status.bundle_export_failed"));
+ }
+ } catch (IOException e) {
+ statusError(e);
+ } catch (SketchException e) {
+ statusError(e);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ stopIndeterminate();
+ }
+ }.start();
+ }
/**
* Create a release build of the sketch and install its apk files on the
* attached device.
*/
public void handleExportPackage() {
- if (androidMode.checkPackageName(sketch, appComponent) &&
- androidMode.checkAppIcons(sketch, appComponent) && handleExportCheckModified()) {
- new KeyStoreManager(this);
+ if (androidMode.checkPackageName(sketch, appComponent, false) &&
+ androidMode.checkAppIcons(sketch, appComponent, false) && handleExportCheckModified()) {
+ new KeyStoreManager(this, false);
}
}
@@ -753,4 +799,4 @@ public void actionPerformed(ActionEvent e) {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/mode/src/processing/mode/android/AndroidMode.java b/mode/src/processing/mode/android/AndroidMode.java
index 574bf1c7..b7d12c26 100644
--- a/mode/src/processing/mode/android/AndroidMode.java
+++ b/mode/src/processing/mode/android/AndroidMode.java
@@ -326,21 +326,26 @@ public void handleStop(RunnerListener listener) {
}
- public boolean checkPackageName(Sketch sketch, int comp) {
+ public boolean checkPackageName(Sketch sketch, int comp, boolean forBundle) {
Manifest manifest = new Manifest(sketch, comp, getFolder(), false);
String defName = Manifest.BASE_PACKAGE + "." + sketch.getName().toLowerCase();
String name = manifest.getPackageName();
if (name.toLowerCase().equals(defName.toLowerCase())) {
// The user did not set the package name, show error and stop
- AndroidUtil.showMessage(AndroidMode.getTextString("android_mode.dialog.cannot_export_package_title"),
- AndroidMode.getTextString("android_mode.dialog.cannot_export_package_body", DISTRIBUTING_APPS_TUT_URL));
+ if(forBundle){
+ AndroidUtil.showMessage(AndroidMode.getTextString("android_mode.dialog.cannot_export_bundle_title"),
+ AndroidMode.getTextString("android_mode.dialog.cannot_export_package_body", DISTRIBUTING_APPS_TUT_URL));
+ }else {
+ AndroidUtil.showMessage(AndroidMode.getTextString("android_mode.dialog.cannot_export_package_title"),
+ AndroidMode.getTextString("android_mode.dialog.cannot_export_package_body", DISTRIBUTING_APPS_TUT_URL));
+ }
return false;
}
return true;
}
- public boolean checkAppIcons(Sketch sketch, int comp) {
+ public boolean checkAppIcons(Sketch sketch, int comp, boolean forBundle) {
File sketchFolder = sketch.getFolder();
File[] launcherIcons = AndroidUtil.getFileList(sketchFolder, AndroidBuild.SKETCH_LAUNCHER_ICONS,
@@ -355,9 +360,14 @@ public boolean checkAppIcons(Sketch sketch, int comp) {
if (!allFilesExist) {
// The user did not set custom icons, show error and stop
- AndroidUtil.showMessage(AndroidMode.getTextString("android_mode.dialog.cannot_use_default_icons_title"),
- AndroidMode.getTextString("android_mode.dialog.cannot_use_default_icons_body", DISTRIBUTING_APPS_TUT_URL));
- return false;
+ if(forBundle){
+ AndroidUtil.showMessage(AndroidMode.getTextString("android_mode.dialog.cannot_use_default_icons_title_bundle"),
+ AndroidMode.getTextString("android_mode.dialog.cannot_use_default_icons_body", DISTRIBUTING_APPS_TUT_URL));
+ }else {
+ AndroidUtil.showMessage(AndroidMode.getTextString("android_mode.dialog.cannot_use_default_icons_title"),
+ AndroidMode.getTextString("android_mode.dialog.cannot_use_default_icons_body", DISTRIBUTING_APPS_TUT_URL));
+ }
+ return false;
}
return true;
}
@@ -419,4 +429,5 @@ static public String getTextString(String key, Object... arguments) {
}
return String.format(value, arguments);
}
-}
\ No newline at end of file
+}
+
diff --git a/mode/src/processing/mode/android/AndroidToolbar.java b/mode/src/processing/mode/android/AndroidToolbar.java
index 8632f768..cb4c08fa 100644
--- a/mode/src/processing/mode/android/AndroidToolbar.java
+++ b/mode/src/processing/mode/android/AndroidToolbar.java
@@ -46,6 +46,8 @@ public class AndroidToolbar extends EditorToolbar {
static protected final int OPEN = 3;
static protected final int SAVE = 4;
static protected final int EXPORT = 5;
+ static protected final int EXPORT_BUNDLE = 6;
+
private AndroidEditor aEditor;
@@ -77,6 +79,7 @@ static public String getTitle(int index, boolean shift) {
case SAVE: return "Save";
case EXPORT: return !shift ? AndroidMode.getTextString("menu.file.export_signed_package") :
AndroidMode.getTextString("menu.file.export_android_project");
+ case EXPORT_BUNDLE: return AndroidMode.getTextString("menu.file.export_signed_bundle");
}
return null;
}
@@ -250,4 +253,5 @@ public void deactivateStep() {
stepButton.setSelected(false);
repaint();
}
-}
\ No newline at end of file
+}
+
diff --git a/mode/src/processing/mode/android/KeyStoreManager.java b/mode/src/processing/mode/android/KeyStoreManager.java
index d32f42e0..298e90ce 100644
--- a/mode/src/processing/mode/android/KeyStoreManager.java
+++ b/mode/src/processing/mode/android/KeyStoreManager.java
@@ -64,14 +64,14 @@ public class KeyStoreManager extends JFrame {
JTextField country;
JTextField stateName;
- public KeyStoreManager(final AndroidEditor editor) {
+ public KeyStoreManager(final AndroidEditor editor, boolean forBundle) {
super("Android keystore manager");
this.editor = editor;
- createLayout();
+ createLayout(forBundle);
}
- private void createLayout() {
+ private void createLayout( boolean forBundleExport) {
Container outer = getContentPane();
outer.removeAll();
@@ -105,13 +105,21 @@ public void actionPerformed(ActionEvent e) {
localityName.getText(), stateName.getText(), country.getText());
setVisible(false);
- editor.startExportPackage(new String(passwordField.getPassword()));
+ if(forBundleExport){
+ editor.startExportBundle(new String(passwordField.getPassword()));
+ }else {
+ editor.startExportPackage(new String(passwordField.getPassword()));
+ }
} catch (Exception e1) {
e1.printStackTrace();
}
} else {
setVisible(false);
- editor.startExportPackage(new String(passwordField.getPassword()));
+ if(forBundleExport){
+ editor.startExportBundle(new String(passwordField.getPassword()));
+ }else {
+ editor.startExportPackage(new String(passwordField.getPassword()));
+ }
}
}
}
@@ -147,7 +155,7 @@ public void actionPerformed(ActionEvent e) {
setVisible(true);
} else {
keyStore = null;
- createLayout();
+ createLayout(forBundleExport);
}
}
}