Skip to content

AAB(Android App Bundle) Support #671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions mode/languages/mode.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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

# ---------------------------------------
Expand All @@ -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.<br><br>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.<br><br>For more info on distributing apps from Processing,<br>check <a href=\"%s\">this online tutorial</a>.
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.<br><br>For more info on distributing apps from Processing,<br>check <a href=\"%s\">this online tutorial</a>.
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.
Expand Down
3 changes: 2 additions & 1 deletion mode/languages/mode_ko.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 재설정
menu.android.reset_adb = ADB 재설정
126 changes: 111 additions & 15 deletions mode/src/processing/mode/android/AndroidBuild.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}


// ---------------------------------------------------------------------------
Expand All @@ -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
Expand All @@ -697,36 +767,45 @@ 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()) {
Messages.showWarning(AndroidMode.getTextString("android_build.warn.cannot_find_zipalign.title"),
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",
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -1014,4 +1110,4 @@ static private boolean versionCheck(String currentVersion, String minVersion) {

return false;
}
}
}
56 changes: 51 additions & 5 deletions mode/src/processing/mode/android/AndroidEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}


Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -753,4 +799,4 @@ public void actionPerformed(ActionEvent e) {
}
}
}
}
}
27 changes: 19 additions & 8 deletions mode/src/processing/mode/android/AndroidMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -419,4 +429,5 @@ static public String getTextString(String key, Object... arguments) {
}
return String.format(value, arguments);
}
}
}

Loading