Implement Android App Bundles (#2237)

This commit is contained in:
Diego Barreiro 2021-01-13 15:14:36 +01:00 committed by GitHub
parent 50af1648c9
commit 6516283d20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 908 additions and 206 deletions

View File

@ -683,4 +683,10 @@ public interface Images extends Resources {
*/
@Source("com/google/appinventor/images/YRLogo.png")
ImageResource YRLogo();
/**
* Download app icon
*/
@Source("com/google/appinventor/images/get-app.png")
ImageResource GetApp();
}

View File

@ -2270,7 +2270,7 @@ public class Ode implements EntryPoint {
* @return nonce
*/
public String generateNonce() {
int v = random.nextInt(1000000);
int v = random.nextInt(10000000);
nonce = Integer.toString(v, 36); // Base 36 string
return nonce;
}

View File

@ -625,21 +625,21 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages {
@Description("Label of the button leading to build related cascade items")
String buildTabName();
@DefaultMessage("App ( provide QR code for .apk )")
@Description("Label of item for building a project and show barcode")
String showBarcodeMenuItem();
@DefaultMessage("Android App (.apk)")
@Description("Label of item for building a project as apk and showing the qr+download dialog")
String showExportAndroidApk();
@DefaultMessage("App for Google Play ( provide QR code for .apk )")
@Description("Label of item for building a project and show barcode")
String showBarcodeMenuItem2();
@DefaultMessage("[2] Android App (.apk)")
@Description("Label of item for building a project as apk and showing the qr+download dialog")
String showExportAndroidApk2();
@DefaultMessage("App ( save .apk to my computer )")
@Description("Label of item for building a project and downloading")
String downloadToComputerMenuItem();
@DefaultMessage("Android App Bundle (.aab)")
@Description("Label of item for building a project as aab and showing the qr+download dialog")
String showExportAndroidAab();
@DefaultMessage("App for Google Play ( save .apk to my computer )")
@Description("Label of item for building a project and downloading")
String downloadToComputerMenuItem2();
@DefaultMessage("[2] Android App Bundle (.aab)")
@Description("Label of item for building a project as aab and showing the qr+download dialog")
String showExportAndroidAab2();
@DefaultMessage("Generate YAIL")
@Description("Label of the cascade item for generating YAIL for a project")
@ -1497,15 +1497,33 @@ public interface OdeMessages extends Messages, AutogeneratedOdeMessages {
// Used in explorer/commands/ShowBarcodeCommand.java
@DefaultMessage("Barcode link for {0}")
@Description("Title of barcode dialog.")
String barcodeTitle(String projectName);
@DefaultMessage("Android App for {0}")
@Description("Title of download apk dialog.")
String downloadApkDialogTitle(String projectName);
@DefaultMessage("Android App Bundle for {0} (to be used in Google Play Store)")
@Description("Title of download aab dialog.")
String downloadAabDialogTitle(String projectName);
@DefaultMessage("Download .apk now")
@Description("Download button shown in barcode dialog")
String barcodeDownloadApk();
@DefaultMessage("Download .aab now")
@Description("Download button shown in barcode dialog")
String barcodeDownloadAab();
@DefaultMessage("Note: this barcode is only valid for 2 hours. See {0} the FAQ {1} for info " +
"on how to share your app with others.")
@Description("Warning in barcode dialog.")
String barcodeWarning(String aTagStart, String aTagEnd);
@DefaultMessage("<b>Click the button to download the app, right-click on it to copy a download link, or scan the " +
"code with a barcode scanner to install.</b><br>" +
"Note: this link and barcode are only valid for 2 hours. See {0} the FAQ {1} for info on how to share your " +
"app with others.")
@Description("Warning in barcode dialog.")
String barcodeWarning2(String aTagStart, String aTagEnd);
// Used in explorer/project/Project.java
@DefaultMessage("Server error: could not load project. Please try again later!")

View File

@ -76,10 +76,10 @@ public class TopToolbar extends Composite {
private static final String WIDGET_NAME_CHECKPOINT = "Checkpoint";
private static final String WIDGET_NAME_MY_PROJECTS = "MyProjects";
private static final String WIDGET_NAME_BUILD = "Build";
private static final String WIDGET_NAME_BUILD_BARCODE = "Barcode";
private static final String WIDGET_NAME_BUILD_DOWNLOAD = "Download";
private static final String WIDGET_NAME_BUILD_BARCODE2 = "Barcode2";
private static final String WIDGET_NAME_BUILD_DOWNLOAD2 = "Download2";
private static final String WIDGET_NAME_BUILD_ANDROID_APK = "BuildApk";
private static final String WIDGET_NAME_BUILD_ANDROID_AAB = "BuildAab";
private static final String WIDGET_NAME_BUILD_ANDROID_APK2 = "BuildApk2";
private static final String WIDGET_NAME_BUILD_ANDROID_AAB2 = "BuildAab2";
private static final String WIDGET_NAME_BUILD_YAIL = "Yail";
private static final String WIDGET_NAME_CONNECT_TO = "ConnectTo";
private static final String WIDGET_NAME_WIRELESS_BUTTON = "Wireless";
@ -293,10 +293,10 @@ public class TopToolbar extends Composite {
private void createBuildMenu() {
List<DropDownItem> buildItems = Lists.newArrayList();
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_BARCODE, MESSAGES.showBarcodeMenuItem(),
new BarcodeAction(false)));
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_DOWNLOAD, MESSAGES.downloadToComputerMenuItem(),
new DownloadAction(false)));
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_ANDROID_APK, MESSAGES.showExportAndroidApk(),
new BarcodeAction(false, false)));
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_ANDROID_AAB, MESSAGES.showExportAndroidAab(),
new BarcodeAction(false, true)));
// Second Buildserver Menu Items
//
@ -314,10 +314,10 @@ public class TopToolbar extends Composite {
if (Ode.getInstance().hasSecondBuildserver()) {
buildItems.add(null);
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_BARCODE2, MESSAGES.showBarcodeMenuItem2(),
new BarcodeAction(true)));
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_DOWNLOAD2, MESSAGES.downloadToComputerMenuItem2(),
new DownloadAction(true)));
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_ANDROID_APK2, MESSAGES.showExportAndroidApk2(),
new BarcodeAction(true, false)));
buildItems.add(new DropDownItem(WIDGET_NAME_BUILD_ANDROID_AAB2, MESSAGES.showExportAndroidAab2(),
new BarcodeAction(true, true)));
}
if (AppInventorFeatures.hasYailGenerationOption() && Ode.getInstance().getUser().getIsAdmin()) {
@ -538,9 +538,11 @@ public class TopToolbar extends Composite {
private class BarcodeAction implements Command {
private boolean secondBuildserver = false;
private boolean isAab;
public BarcodeAction(boolean secondBuildserver) {
public BarcodeAction(boolean secondBuildserver, boolean isAab) {
this.secondBuildserver = secondBuildserver;
this.isAab = isAab;
}
@Override
@ -550,10 +552,10 @@ public class TopToolbar extends Composite {
String target = YoungAndroidProjectNode.YOUNG_ANDROID_TARGET_ANDROID;
ChainableCommand cmd = new SaveAllEditorsCommand(
new GenerateYailCommand(
new BuildCommand(target, secondBuildserver,
new BuildCommand(target, secondBuildserver, isAab,
new ShowProgressBarCommand(target,
new WaitForBuildResultCommand(target,
new ShowBarcodeCommand(target)), "BarcodeAction"))));
new ShowBarcodeCommand(target, isAab)), "BarcodeAction"))));
if (!Ode.getInstance().getWarnBuild(secondBuildserver)) {
cmd = new WarningDialogCommand(target, secondBuildserver, cmd);
Ode.getInstance().setWarnBuild(secondBuildserver, true);
@ -568,38 +570,6 @@ public class TopToolbar extends Composite {
}
}
private class DownloadAction implements Command {
private boolean secondBuildserver = false;
DownloadAction(boolean secondBuildserver) {
this.secondBuildserver = secondBuildserver;
}
@Override
public void execute() {
ProjectRootNode projectRootNode = Ode.getInstance().getCurrentYoungAndroidProjectRootNode();
if (projectRootNode != null) {
String target = YoungAndroidProjectNode.YOUNG_ANDROID_TARGET_ANDROID;
ChainableCommand cmd = new SaveAllEditorsCommand(
new GenerateYailCommand(
new BuildCommand(target, secondBuildserver,
new ShowProgressBarCommand(target,
new WaitForBuildResultCommand(target,
new DownloadProjectOutputCommand(target)), "DownloadAction"))));
if (!Ode.getInstance().getWarnBuild(secondBuildserver)) {
cmd = new WarningDialogCommand(target, secondBuildserver, cmd);
Ode.getInstance().setWarnBuild(secondBuildserver, true);
}
cmd.startExecuteChain(Tracking.PROJECT_ACTION_BUILD_DOWNLOAD_YA, projectRootNode,
new Command() {
@Override
public void execute() {
}
});
}
}
}
private static class ExportProjectAction implements Command {
@Override
public void execute() {
@ -1104,8 +1074,12 @@ public class TopToolbar extends Composite {
fileDropDown.setItemEnabled(MESSAGES.saveMenuItem(), false);
fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), false);
fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), false);
buildDropDown.setItemEnabled(MESSAGES.showBarcodeMenuItem(), false);
buildDropDown.setItemEnabled(MESSAGES.downloadToComputerMenuItem(), false);
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidApk(), false);
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidAab(), false);
if (Ode.getInstance().hasSecondBuildserver()) {
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidApk2(), false);
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidAab2(), false);
}
} else { // We have to be in the Designer/Blocks view
fileDropDown.setItemEnabled(MESSAGES.deleteProjectButton(), true);
fileDropDown.setItemEnabled(MESSAGES.trashProjectMenuItem(), true);
@ -1115,8 +1089,12 @@ public class TopToolbar extends Composite {
fileDropDown.setItemEnabled(MESSAGES.saveMenuItem(), true);
fileDropDown.setItemEnabled(MESSAGES.saveAsMenuItem(), true);
fileDropDown.setItemEnabled(MESSAGES.checkpointMenuItem(), true);
buildDropDown.setItemEnabled(MESSAGES.showBarcodeMenuItem(), true);
buildDropDown.setItemEnabled(MESSAGES.downloadToComputerMenuItem(), true);
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidApk(), true);
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidAab(), true);
if (Ode.getInstance().hasSecondBuildserver()) {
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidApk2(), true);
buildDropDown.setItemEnabled(MESSAGES.showExportAndroidAab2(), true);
}
}
updateKeystoreFileMenuButtons(true);
}

View File

@ -31,14 +31,15 @@ public class BuildCommand extends ChainableCommand {
// Whether or not to use the second buildserver
private boolean secondBuildserver = false;
private boolean isAab;
/**
* Creates a new build command.
*
* @param target the build target
*/
public BuildCommand(String target, boolean secondBuildserver) {
this(target, secondBuildserver, null);
public BuildCommand(String target, boolean secondBuildserver, boolean isAab) {
this(target, secondBuildserver, isAab, null);
}
/**
@ -48,8 +49,9 @@ public class BuildCommand extends ChainableCommand {
* @param target the build target
* @param nextCommand the command to execute after the build has finished
*/
public BuildCommand(String target, boolean secondBuildserver, ChainableCommand nextCommand) {
public BuildCommand(String target, boolean secondBuildserver, boolean isAab, ChainableCommand nextCommand) {
super(nextCommand);
this.isAab = isAab;
this.target = target;
this.secondBuildserver = secondBuildserver;
}
@ -126,6 +128,6 @@ public class BuildCommand extends ChainableCommand {
};
String nonce = ode.generateNonce();
ode.getProjectService().build(node.getProjectId(), nonce, target, secondBuildserver, callback);
ode.getProjectService().build(node.getProjectId(), nonce, target, secondBuildserver, isAab, callback);
}
}

View File

@ -11,11 +11,17 @@ import com.google.appinventor.client.editor.youngandroid.BlocklyPanel;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.shared.rpc.project.ProjectNode;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
@ -23,8 +29,7 @@ import static com.google.appinventor.client.Ode.MESSAGES;
/**
* Command for displaying a barcode for the target of a project.
*
* <p/>This command is often chained with SaveAllEditorsCommand and BuildCommand.
* This command is often chained with SaveAllEditorsCommand and BuildCommand.
*
* @author markf@google.com (Mark Friedman)
*/
@ -32,17 +37,19 @@ public class ShowBarcodeCommand extends ChainableCommand {
// The build target
private String target;
private boolean isAab;
/**
* Creates a new command for showing a barcode for the target of a project.
*
* @param target the build target
*/
public ShowBarcodeCommand(String target) {
public ShowBarcodeCommand(String target, boolean isAab) {
// Since we don't know when the barcode dialog is finished, we can't
// support a command after this one.
super(null); // no next command
this.target = target;
this.isAab = isAab;
}
@Override
@ -54,55 +61,108 @@ public class ShowBarcodeCommand extends ChainableCommand {
public void execute(final ProjectNode node) {
// Display a barcode for an url pointing at our server's download servlet
String barcodeUrl = GWT.getHostPageBaseURL()
+ "b/" + Ode.getInstance().getNonce();
+ "b/" + Ode.getInstance().getNonce();
OdeLog.log("Barcode url is: " + barcodeUrl);
new BarcodeDialogBox(node.getName(), barcodeUrl).center();
new BarcodeDialogBox(node.getName(), barcodeUrl, isAab).center();
}
static class BarcodeDialogBox extends DialogBox {
BarcodeDialogBox(String projectName, String appInstallUrl) {
BarcodeDialogBox(String projectName, final String appInstallUrl, boolean isAab) {
super(false, true);
setStylePrimaryName("ode-DialogBox");
setText(MESSAGES.barcodeTitle(projectName));
setText(isAab ? MESSAGES.downloadAabDialogTitle(projectName) : MESSAGES.downloadApkDialogTitle(projectName));
// Main layout panel
VerticalPanel contentPanel = new VerticalPanel();
// Container
HorizontalPanel container = new HorizontalPanel();
// Container > Left
VerticalPanel left = new VerticalPanel();
// Container > Left > Download Button
ClickHandler downloadHandler = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
Window.open(appInstallUrl, "_self", "enabled");
}
};
HorizontalPanel downloadPanel = new HorizontalPanel();
downloadPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
Anchor downloadButton = new Anchor();
downloadButton.setHref(appInstallUrl);
downloadButton.addStyleName("gwt-Button");
downloadButton.addStyleName("download-button");
// Container > Left > Download Button > Image
Image downloadIcon = new Image(Ode.getImageBundle().GetApp());
downloadIcon.setSize("110px", "100px");
downloadIcon.addStyleName("download-icon");
downloadButton.getElement().appendChild(downloadIcon.getElement());
// Container > Left > Download Button > Inner Text
SpanElement text = Document.get().createSpanElement();
text.setInnerHTML(isAab ? MESSAGES.barcodeDownloadAab() : MESSAGES.barcodeDownloadApk());
downloadButton.getElement().appendChild(text);
downloadButton.addClickHandler(downloadHandler);
downloadPanel.add(downloadButton);
downloadPanel.setSize("100%", "30px");
left.add(downloadPanel);
// Container > Left
container.add(left);
// The Android App Bundle should only be used to publish the app through Google Play Store. Thus,
// it does not make sense to provide a QR code which the user might think they can scan and directly
// install in the phone directly.
if (!isAab) {
// Container > Right
VerticalPanel right = new VerticalPanel();
// Container > Right > Barcode
HTML barcodeQrcode = new HTML("<center>" + BlocklyPanel.getQRCode(appInstallUrl) + "</center>");
barcodeQrcode.addStyleName("download-barcode");
right.add(barcodeQrcode);
// Container > Right
container.add(right);
}
// Container
contentPanel.add(container);
// Warning
// The warning label is added only in APK files, as there is no QR code for the AAB. It is supposed that
// users download the bundle just when they get it.
if (!isAab) {
HorizontalPanel warningPanel = new HorizontalPanel();
warningPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_LEFT);
HTML warningLabel = new HTML(MESSAGES.barcodeWarning2(
"<a href=\"" + "http://appinventor.mit.edu/explore/ai2/share.html" +
"\" target=\"_blank\">",
"</a>"));
warningLabel.setWordWrap(true);
warningLabel.setWidth("400px"); // set width to get the text to wrap
warningLabel.getElement().getStyle().setMarginTop(10, Style.Unit.PX);
warningPanel.add(warningLabel);
contentPanel.add(warningPanel);
}
// OK button
ClickHandler buttonHandler = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
hide();
}
};
Button cancelButton = new Button(MESSAGES.cancelButton());
cancelButton.addClickHandler(buttonHandler);
Button okButton = new Button(MESSAGES.okButton());
okButton.addClickHandler(buttonHandler);
HTML barcodeQrcode = new HTML("<center>" + BlocklyPanel.getQRCode(appInstallUrl) + "</center>");
HTML linkQrcode = new HTML("<center><a href=\"" + appInstallUrl + "\" target=\"_blank\">" + appInstallUrl + "</a></center>");
HorizontalPanel buttonPanel = new HorizontalPanel();
buttonPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
HTML warningLabel = new HTML(MESSAGES.barcodeWarning(
"<a href=\"" + "http://appinventor.mit.edu/explore/ai2/share.html" +
"\" target=\"_blank\">",
"</a>"));
warningLabel.setWordWrap(true);
warningLabel.setWidth("200px"); // set width to get the text to wrap
HorizontalPanel warningPanel = new HorizontalPanel();
warningPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_LEFT);
warningPanel.add(warningLabel);
// The cancel button is removed from the panel since it has no meaning in this
// context. But the logic is still here in case we want to restore it, and as
// an example of how to code this stuff in GWT.
// buttonPanel.add(cancelButton);
Button okButton = new Button(MESSAGES.dismissButton());
okButton.addClickHandler(buttonHandler);
buttonPanel.add(okButton);
buttonPanel.setSize("100%", "24px");
VerticalPanel contentPanel = new VerticalPanel();
contentPanel.add(barcodeQrcode);
contentPanel.add(linkQrcode);
contentPanel.add(buttonPanel);
contentPanel.add(warningPanel);
// contentPanel.setSize("320px", "100%");
add(contentPanel);
}
}

View File

@ -225,7 +225,7 @@ function checkValidDrop(e) {
top.HTML5DragDrop_confirmOverwriteKey(doUploadKeystore(item));
} else if (isExtension(item) && top.HTML5DragDrop_isProjectEditorOpen()) {
uploadExtension(item);
} else if (goog.string.endsWith(item.name, '.apk')) {
} else if (goog.string.endsWith(item.name, '.apk') || goog.string.endsWith(item.name, '.aab')) {
top.HTML5DragDrop_reportError(2);
} else if (top.HTML5DragDrop_isProjectEditorOpen()) {
uploadAsset(item);

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

View File

@ -48,7 +48,7 @@ public final class FileExporterImpl implements FileExporter {
// There should never be more than one .apk file.
for (String fileName : files) {
if (fileName.endsWith(".apk")) {
if (fileName.endsWith(".apk") || fileName.endsWith(".aab")) {
byte[] content = storageIo.downloadRawFile(userId, projectId, fileName);
return new RawFile(StorageUtil.basename(fileName), content);
}

View File

@ -564,11 +564,11 @@ public class ProjectServiceImpl extends OdeRemoteServiceServlet implements Proje
* @return results of build
*/
@Override
public RpcResult build(long projectId, String nonce, String target, boolean secondBuildserver) {
public RpcResult build(long projectId, String nonce, String target, boolean secondBuildserver, boolean isAab) {
// Dispatch
final String userId = userInfoProvider.getUserId();
return getProjectRpcImpl(userId, projectId).build(
userInfoProvider.getUser(), projectId, nonce, target, secondBuildserver);
userInfoProvider.getUser(), projectId, nonce, target, secondBuildserver, isAab);
}
/**

View File

@ -335,7 +335,7 @@ public abstract class CommonProjectService {
*
* @return build results
*/
public abstract RpcResult build(User user, long projectId, String nonce, String target, boolean secondBuildserver);
public abstract RpcResult build(User user, long projectId, String nonce, String target, boolean secondBuildserver, boolean isAab);
/**
* Gets the result of a build command for the project.

View File

@ -24,6 +24,7 @@ import com.google.appinventor.server.project.CommonProjectService;
import com.google.appinventor.server.project.utils.Security;
import com.google.appinventor.server.properties.json.ServerJsonParser;
import com.google.appinventor.server.storage.StorageIo;
import com.google.appinventor.server.util.UriBuilder;
import com.google.appinventor.shared.properties.json.JSONParser;
import com.google.appinventor.shared.rpc.RpcResult;
import com.google.appinventor.shared.rpc.ServerLayout;
@ -685,7 +686,7 @@ public final class YoungAndroidProjectService extends CommonProjectService {
*/
@Override
public RpcResult build(User user, long projectId, String nonce, String target,
boolean secondBuildserver) {
boolean secondBuildserver, boolean isAab) {
String userId = user.getUserId();
String projectName = storageIo.getProjectName(userId, projectId);
String outputFileDir = BUILD_FOLDER + '/' + target;
@ -708,7 +709,8 @@ public final class YoungAndroidProjectService extends CommonProjectService {
userId,
projectId,
secondBuildserver,
outputFileDir));
outputFileDir,
isAab));
HttpURLConnection connection = (HttpURLConnection) buildServerUrl.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
@ -946,9 +948,9 @@ public final class YoungAndroidProjectService extends CommonProjectService {
}
String buildErrorMsg(String exceptionName, URL buildURL, String userId, long projectId) {
return "Request to build failed with " + exceptionName
+ ", user=" + userId + ", project=" + projectId
+ ", build URL is " + (buildURL != null ? buildURL : "null") + " ["
return "Request to build failed with " + exceptionName
+ ", user=" + userId + ", project=" + projectId
+ ", build URL is " + (buildURL != null ? buildURL : "null") + " ["
+ (buildURL != null ? buildURL.toString().length() : "n/a") + "]";
}
@ -956,21 +958,22 @@ public final class YoungAndroidProjectService extends CommonProjectService {
// a little more complicated when we want to get the URL from an App Engine config file or
// command line argument.
private String getBuildServerUrlStr(String userName, String userId,
long projectId, boolean secondBuildserver, String fileName)
throws UnsupportedEncodingException, EncryptionException {
return "http://" + (secondBuildserver ? buildServerHost2.get() : buildServerHost.get()) +
"/buildserver/build-all-from-zip-async"
+ "?uname=" + URLEncoder.encode(userName, "UTF-8")
+ (sendGitVersion.get()
? "&gitBuildVersion="
+ URLEncoder.encode(GitBuildId.getVersion(), "UTF-8")
: "")
+ "&callback="
+ URLEncoder.encode("http://" + getCurrentHost() + ServerLayout.ODE_BASEURL_NOAUTH
+ ServerLayout.RECEIVE_BUILD_SERVLET + "/"
+ Security.encryptUserAndProjectId(userId, projectId)
+ "/" + fileName,
"UTF-8");
long projectId, boolean secondBuildserver, String fileName, boolean isAab)
throws EncryptionException {
UriBuilder uriBuilder = new UriBuilder(
"http://"
+ (secondBuildserver ? buildServerHost2.get() : buildServerHost.get())
+ "/buildserver/build-all-from-zip-async")
.add("uname", userName)
.add("callback", "http://" + getCurrentHost() + ServerLayout.ODE_BASEURL_NOAUTH +
ServerLayout.RECEIVE_BUILD_SERVLET + "/" +
Security.encryptUserAndProjectId(userId, projectId) + "/" +
fileName)
.add("ext", isAab ? "aab" : "apk");
if (sendGitVersion.get()) {
uriBuilder.add("gitBuildVersion", GitBuildId.getVersion());
}
return uriBuilder.build();
}
private String getCurrentHost() {

View File

@ -1510,7 +1510,7 @@ public class ObjectifyStorageIo implements StorageIo {
if (!useGcs) // Using legacy blob store solution
return false;
boolean shouldUse = fileName.contains("assets/")
|| fileName.endsWith(".apk");
|| fileName.endsWith(".apk") || fileName.endsWith(".aab");
if (shouldUse)
return true; // Use GCS for package output and assets
boolean mayUse = (fileName.contains("src/") && fileName.endsWith(".blk")) // AI1 Blocks Files

View File

@ -147,9 +147,9 @@ public interface StorageIo {
*
* @param userId user ID
* @param projectId project ID
* @param boolean flag
* @param flag
*/
void setMoveToTrashFlag(final String userId, final long projectId,boolean flag);
void setMoveToTrashFlag(final String userId, final long projectId, boolean flag);
/**
* Returns an array with the user's projects.

View File

@ -315,7 +315,7 @@ public interface ProjectService extends RemoteService {
*
* @return results of invoking the build command
*/
RpcResult build(long projectId, String nonce, String target, boolean secondBuildserver);
RpcResult build(long projectId, String nonce, String target, boolean secondBuildserver, boolean isAab);
/**
* Gets the result of a build command for the project from the back-end.

View File

@ -16,7 +16,6 @@ import java.util.List;
* Interface for the service providing project information. All declarations
* in this interface are mirrored in {@link ProjectService}. For further
* information see {@link ProjectService}.
*
*/
public interface ProjectServiceAsync {
@ -134,7 +133,7 @@ public interface ProjectServiceAsync {
/**
* @see ProjectService#loadraw(long, String)
*/
void loadraw(long projectId, String fileId, AsyncCallback<byte []> callback);
void loadraw(long projectId, String fileId, AsyncCallback<byte[]> callback);
/**
* @see ProjectService#loadraw2(long, String)
@ -171,7 +170,7 @@ public interface ProjectServiceAsync {
/**
* @see ProjectService#build(long, String, String, boolean)
*/
void build(long projectId, String nonce, String target, boolean secondBuildserver, AsyncCallback<RpcResult> callback);
void build(long projectId, String nonce, String target, boolean secondBuildserver, boolean isAab, AsyncCallback<RpcResult> callback);
/**
* @see ProjectService#getBuildResult(long, String)

View File

@ -1,5 +1,5 @@
<!-- Copyright 2007-2009 Google Inc. All Rights Reserved. -->
<!-- Copyright 2011-2016 Massachusetts Institute of Technology. All Rights Reserved. -->
<!-- Copyright 2011-2020 Massachusetts Institute of Technology. All Rights Reserved. -->
<!DOCTYPE html>
<html>
<head>

View File

@ -2712,3 +2712,30 @@ div.dropdiv p {
margin: 0;
position: relative;
}
.download-button {
display: grid;
max-width: 130px;
margin: 0 !important;
margin-top: 12px !important;
margin-left: -3px !important;
height: 130px;
}
.download-icon {
height: 110px;
width: 110px;
margin-left: auto;
margin-right: auto;
}
.download-barcode:before {
content: "";
background-color: #888;
position: absolute;
width: 1px;
height: 160px;
top: 40px;
left: 50%;
display: block;
}

View File

@ -63,6 +63,10 @@
<ant inheritAll="false" useNativeBasedir="true" dir="buildserver" target="PlayApp"/>
</target>
<target name="PlayAppAab">
<ant inheritAll="false" useNativeBasedir="true" dir="buildserver" target="PlayAppAab"/>
</target>
<target name="PlayAppExtras">
<ant inheritAll="false" useNativeBasedir="true" dir="buildserver" target="PlayAppExtras"/>
</target>

View File

@ -78,6 +78,7 @@
<property name="classes.tools.dir" location="${BuildServer-class.dir}/tools" />
<copy todir="${classes.tools.dir}">
<fileset dir="${lib.dir}/android/tools" includes="*/aapt" />
<fileset dir="${lib.dir}/android/tools" includes="*/aapt2" />
<fileset dir="${lib.dir}/android/tools" includes="*/libwinpthread-1.dll" />
<fileset dir="${lib.dir}/android/tools" includes="*/lib64/*" />
</copy>
@ -174,7 +175,8 @@
===================================================================== -->
<target name="CheckPlayApp"
depends="GenPlayAppSrcZip,BuildServer">
<uptodate property="PlayApp.uptodate" targetfile="${public.build.dir}/MIT AI2 Companion.apk">
<property name="ext" value="apk"/>
<uptodate property="PlayApp.uptodate" targetfile="${public.build.dir}/MIT AI2 Companion.${ext}">
<srcfiles file="${local.build.dir}/aiplayapp.zip"/>
<srcfiles dir="${run.lib.dir}" includes="*.jar"/>
</uptodate>
@ -186,6 +188,11 @@
<target name="PlayApp"
depends="CheckPlayApp"
unless="PlayApp.uptodate">
<condition property="ext" value="apk">
<not>
<isset property="ext"/>
</not>
</condition>
<java classname="com.google.appinventor.buildserver.Main" fork="true" failonerror="true">
<classpath>
<fileset dir="${run.lib.dir}" includes="*.jar" />
@ -203,9 +210,16 @@
<arg value="${public.build.dir}" />
<arg value="--dexCacheDir" />
<arg value="${public.build.dir}/dexCache" />
<arg value="--ext" />
<arg value="${ext}" />
</java>
</target>
<target name="PlayAppAab">
<property name="ext" value="aab"/>
<antcall target="PlayApp"/>
</target>
<target name="CheckPlayAppExtras">
<uptodate property="PlayAppExtras.uptodate" targetfile="${public.build.dir}/MITAI2Companion-full.apk">
<srcfiles file="${local.build.dir}/aiplayapp.zip"/>

View File

@ -0,0 +1,323 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;
import com.google.appinventor.buildserver.util.AabZipper;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* This Callable class will convert the compiled files into an Android App Bundle.
* An AAB file structure looks like this:
* - assets.pb
* - resources.pb
* - native.pb
* - manifest/AndroidManifest.xml
* - dex/
* - res/
* - assets/
* - lib/
*/
public class AabCompiler implements Callable<Boolean> {
private PrintStream out;
private File buildDir;
private int mx;
private AabPaths aab;
private String originalDexDir = null;
private File originalLibsDir = null;
private String bundletool = null;
private String jarsigner = null;
private String deploy = null;
private String keystore = null;
private class AabPaths {
private File root = null;
private File base = null;
private File protoApk = null;
private File assetsDir = null;
private File dexDir = null;
private File libDir = null;
private File manifestDir = null;
private File resDir = null;
public File getRoot() {
return root;
}
public void setRoot(File root) {
this.root = root;
}
public File getBase() {
return base;
}
public void setBase(File base) {
this.base = base;
}
public File getProtoApk() {
return protoApk;
}
public void setProtoApk(File protoApk) {
this.protoApk = protoApk;
}
public File getAssetsDir() {
return assetsDir;
}
public void setAssetsDir(File assetsDir) {
this.assetsDir = assetsDir;
}
public File getDexDir() {
return dexDir;
}
public void setDexDir(File dexDir) {
this.dexDir = dexDir;
}
public File getLibDir() {
return libDir;
}
public void setLibDir(File libDir) {
this.libDir = libDir;
}
public File getManifestDir() {
return manifestDir;
}
public void setManifestDir(File manifestDir) {
this.manifestDir = manifestDir;
}
public File getResDir() {
return resDir;
}
public void setResDir(File resDir) {
this.resDir = resDir;
}
}
public AabCompiler(PrintStream out, File buildDir, int mx) {
assert out != null;
assert buildDir != null;
assert mx > 0;
this.out = out;
this.buildDir = buildDir;
this.mx = mx;
aab = new AabPaths();
}
public AabCompiler setDexDir(String dexDir) {
this.originalDexDir = dexDir;
return this;
}
public AabCompiler setLibsDir(File libsDir) {
this.originalLibsDir = libsDir;
return this;
}
public AabCompiler setBundletool(String bundletool) {
this.bundletool = bundletool;
return this;
}
public AabCompiler setDeploy(String deploy) {
this.deploy = deploy;
return this;
}
public AabCompiler setKeystore(String keystore) {
this.keystore = keystore;
return this;
}
public AabCompiler setJarsigner(String jarsigner) {
this.jarsigner = jarsigner;
return this;
}
public AabCompiler setProtoApk(File apk) {
aab.setProtoApk(apk);
return this;
}
private static File createDir(File parentDir, String name) {
File dir = new File(parentDir, name);
if (!dir.exists()) {
dir.mkdir();
}
return dir;
}
@Override
public Boolean call() {
out.println("___________Creating structure");
aab.setRoot(createDir(buildDir, "aab"));
if (!createStructure()) {
return false;
}
out.println("___________Extracting protobuf resources");
if (!extractProtobuf()) {
return false;
}
out.println("________Running bundletool");
if (!bundletool()) {
return false;
}
out.println("________Signing bundle");
if (!jarsigner()) {
return false;
}
return true;
}
private boolean createStructure() {
// Manifest is extracted from the protobuffed APK
aab.setManifestDir(createDir(aab.root, "manifest"));
// Resources are extracted from the protobuffed APK
aab.setResDir(createDir(aab.root, "res"));
// Assets are extracted from the protobuffed APK
aab.setAssetsDir(createDir(aab.root, "assets"));
aab.setDexDir(createDir(aab.root, "dex"));
File[] dexFiles = new File(originalDexDir).listFiles();
if (dexFiles != null) {
for (File dex : dexFiles) {
if (dex.isFile()) {
try {
Files.move(dex, new File(aab.dexDir, dex.getName()));
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
}
aab.setLibDir(createDir(aab.root, "lib"));
File[] libFiles = originalLibsDir.listFiles();
if (libFiles != null) {
for (File lib : libFiles) {
try {
Files.move(lib, new File(createDir(aab.root, "lib"), lib.getName()));
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
return true;
}
private boolean extractProtobuf() {
try (ZipInputStream is = new ZipInputStream(new FileInputStream(aab.getProtoApk()))) {
ZipEntry entry;
byte[] buffer = new byte[1024];
while ((entry = is.getNextEntry()) != null) {
String n = entry.getName();
File f = null;
if (n.equals("AndroidManifest.xml")) {
f = new File(aab.getManifestDir(), n);
} else if (n.equals("resources.pb")) {
f = new File(aab.getRoot(), n);
} else if (n.startsWith("assets")) {
f = new File(aab.getAssetsDir(), n.substring(("assets").length()));
} else if (n.startsWith("res")) {
f = new File(aab.getResDir(), n.substring(("res").length()));
}
if (f != null) {
f.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(f)) {
int len;
while ((len = is.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
return true;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
private boolean bundletool() {
aab.setBase(new File(buildDir, "base.zip"));
if (!AabZipper.zipBundle(aab.getRoot(), aab.getBase(), aab.getRoot().getName() + File.separator)) {
return false;
}
List<String> bundletoolCommandLine = new ArrayList<String>();
bundletoolCommandLine.add(System.getProperty("java.home") + "/bin/java");
bundletoolCommandLine.add("-jar");
bundletoolCommandLine.add("-mx" + mx + "M");
bundletoolCommandLine.add(bundletool);
bundletoolCommandLine.add("build-bundle");
bundletoolCommandLine.add("--modules=" + aab.getBase());
bundletoolCommandLine.add("--output=" + deploy);
String[] bundletoolBuildCommandLine = bundletoolCommandLine.toArray(new String[0]);
return Execution.execute(null, bundletoolBuildCommandLine, System.out, System.err);
}
private boolean jarsigner() {
List<String> jarsignerCommandLine = new ArrayList<String>();
jarsignerCommandLine.add(jarsigner);
jarsignerCommandLine.add("-sigalg");
jarsignerCommandLine.add("SHA256withRSA");
jarsignerCommandLine.add("-digestalg");
jarsignerCommandLine.add("SHA-256");
jarsignerCommandLine.add("-keystore");
jarsignerCommandLine.add(keystore);
jarsignerCommandLine.add("-storepass");
jarsignerCommandLine.add("android");
jarsignerCommandLine.add(deploy);
jarsignerCommandLine.add("AndroidKey");
String[] jarsignerSignCommandLine = jarsignerCommandLine.toArray(new String[0]);
return Execution.execute(null, jarsignerSignCommandLine, System.out, System.err);
}
}

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;
@ -147,6 +147,7 @@ public class BuildServer {
@Option(name = "--debug",
usage = "Turn on debugging, which enables the non-async calls of the buildserver.")
boolean debug = false;
@Option(name = "--dexCacheDir",
usage = "the directory to cache the pre-dexed libraries")
String dexCacheDir = null;
@ -400,7 +401,7 @@ public class BuildServer {
@POST
@Path("build-from-zip")
@Produces("application/vnd.android.package-archive;charset=utf-8")
public Response buildFromZipFile(@QueryParam("uname") String userName, File zipFile)
public Response buildFromZipFile(@QueryParam("uname") String userName, @QueryParam("ext") String ext, File zipFile)
throws IOException {
// Set the inputZip field so we can delete the input zip file later in cleanUp.
inputZip = zipFile;
@ -410,8 +411,10 @@ public class BuildServer {
return Response.status(Response.Status.FORBIDDEN).type(MediaType.TEXT_PLAIN_TYPE)
.entity("Entry point unavailable unless debugging.").build();
boolean isAab = Main.AAB_EXTENSION_VALUE.equals(ext);
try {
build(userName, zipFile, null);
build(userName, zipFile, isAab, null);
String attachedFilename = outputApk.getName();
FileInputStream outputApkDeleteOnClose = new DeleteFileOnCloseFileInputStream(outputApk);
// Set the outputApk field to null so that it won't be deleted in cleanUp().
@ -443,7 +446,7 @@ public class BuildServer {
@POST
@Path("build-all-from-zip")
@Produces("application/zip;charset=utf-8")
public Response buildAllFromZipFile(@QueryParam("uname") String userName, File inputZipFile)
public Response buildAllFromZipFile(@QueryParam("uname") String userName, @QueryParam("ext") String ext, File inputZipFile)
throws IOException, JSONException {
// Set the inputZip field so we can delete the input zip file later in cleanUp.
inputZip = inputZipFile;
@ -453,8 +456,10 @@ public class BuildServer {
return Response.status(Response.Status.FORBIDDEN).type(MediaType.TEXT_PLAIN_TYPE)
.entity("Entry point unavailable unless debugging.").build();
boolean isAab = Main.AAB_EXTENSION_VALUE.equals(ext);
try {
buildAndCreateZip(userName, inputZipFile, null);
buildAndCreateZip(userName, inputZipFile, isAab, null);
String attachedFilename = outputZip.getName();
FileInputStream outputZipDeleteOnClose = new DeleteFileOnCloseFileInputStream(outputZip);
// Set the outputZip field to null so that it won't be deleted in cleanUp().
@ -501,6 +506,7 @@ public class BuildServer {
@QueryParam("uname") final String userName,
@QueryParam("callback") final String callbackUrlStr,
@QueryParam("gitBuildVersion") final String gitBuildVersion,
@QueryParam("ext") final String ext,
final File inputZipFile) throws IOException {
// Set the inputZip field so we can delete the input zip file later in
// cleanUp.
@ -508,6 +514,8 @@ public class BuildServer {
inputZip.deleteOnExit(); // In case build server is killed before cleanUp executes.
String requesting_host = (new URL(callbackUrlStr)).getHost();
final boolean isAab = Main.AAB_EXTENSION_VALUE.equals(ext);
//for the request for update part, the file should be empty
if (inputZip.length() == 0L) {
cleanUp();
@ -562,7 +570,7 @@ public class BuildServer {
try {
LOG.info("START NEW BUILD " + count);
checkMemory();
buildAndCreateZip(userName, inputZipFile, new ProgressReporter(callbackUrlStr));
buildAndCreateZip(userName, inputZipFile, isAab, new ProgressReporter(callbackUrlStr));
// Send zip back to the callbackUrl
LOG.info("CallbackURL: " + callbackUrlStr);
URL callbackUrl = new URL(callbackUrlStr);
@ -621,12 +629,12 @@ public class BuildServer {
// are now handled via a callback mechanism. The "50" here is just a plug
// number.
return Response.ok().type(MediaType.TEXT_PLAIN_TYPE)
.entity("" + 50).build();
.entity("" + 0).build();
}
private void buildAndCreateZip(String userName, File inputZipFile, ProgressReporter reporter)
private void buildAndCreateZip(String userName, File inputZipFile, boolean isAab, ProgressReporter reporter)
throws IOException, JSONException {
Result buildResult = build(userName, inputZipFile, reporter);
Result buildResult = build(userName, inputZipFile, isAab, reporter);
boolean buildSucceeded = buildResult.succeeded();
outputZip = File.createTempFile(inputZipFile.getName(), ".zip");
outputZip.deleteOnExit(); // In case build server is killed before cleanUp executes.
@ -664,7 +672,7 @@ public class BuildServer {
return buildOutputJsonObj.toString();
}
private Result build(String userName, File zipFile, ProgressReporter reporter) throws IOException {
private Result build(String userName, File zipFile, boolean isAab, ProgressReporter reporter) throws IOException {
outputDir = Files.createTempDir();
// We call outputDir.deleteOnExit() here, in case build server is killed before cleanUp
// executes. However, it is likely that the directory won't be empty and therefore, won't
@ -673,7 +681,7 @@ public class BuildServer {
outputDir.deleteOnExit();
Result buildResult = projectBuilder.build(userName, new ZipFile(zipFile), outputDir, null,
false, false, false, null,
commandLineOptions.childProcessRamMb, commandLineOptions.dexCacheDir, reporter);
commandLineOptions.childProcessRamMb, commandLineOptions.dexCacheDir, reporter, isAab);
String buildOutput = buildResult.getOutput();
LOG.info("Build output: " + buildOutput);
String buildError = buildResult.getError();

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2020 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
@ -55,11 +55,15 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.imageio.ImageIO;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
@ -193,6 +197,15 @@ public final class Compiler {
private static final String WINDOWS_ZIPALIGN_TOOL =
"/tools/windows/zipalign";
private static final String LINUX_AAPT2_TOOL =
"/tools/linux/aapt2";
private static final String MAC_AAPT2_TOOL =
"/tools/mac/aapt2";
private static final String WINDOWS_AAPT2_TOOL =
"/tools/windows/aapt2";
private static final String BUNDLETOOL_JAR =
RUNTIME_FILES_DIR + "bundletool.jar";
@VisibleForTesting
static final String YAIL_RUNTIME = RUNTIME_FILES_DIR + "runtime.scm";
@ -255,6 +268,11 @@ public final class Compiler {
*/
private File mergedResDir;
/**
* Zip file containing all compiled resources with AAPT2
*/
private File resourcesZip;
// TODO(Will): Remove the following Set once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
@ -897,7 +915,6 @@ public final class Compiler {
writeDialogTheme(out, "AIAlertDialog", "Theme.AppCompat.Dialog.Alert");
}
}
out.write("<style name=\"TextAppearance.AppCompat.Button\">\n");
out.write("<item name=\"textAllCaps\">false</item>\n");
out.write("</style>\n");
@ -1333,7 +1350,7 @@ public final class Compiler {
boolean isForCompanion, boolean isForEmulator,
boolean includeDangerousPermissions, String keystoreFilePath,
int childProcessRam, String dexCacheDir, String outputFileName,
BuildServer.ProgressReporter reporter) throws IOException, JSONException {
BuildServer.ProgressReporter reporter, boolean isAab) throws IOException, JSONException {
long start = System.currentTimeMillis();
// Create a new compiler instance for the compilation
@ -1341,6 +1358,11 @@ public final class Compiler {
isForCompanion, isForEmulator, includeDangerousPermissions, childProcessRam, dexCacheDir,
reporter);
// Set initial progress to 0%
if (reporter != null) {
reporter.report(0);
}
compiler.generateAssets();
compiler.generateActivities();
compiler.generateMetadata();
@ -1473,11 +1495,20 @@ public final class Compiler {
out.println("________Invoking AAPT");
File deployDir = createDir(buildDir, "deploy");
String tmpPackageName = deployDir.getAbsolutePath() + SLASH +
project.getProjectName() + ".ap_";
project.getProjectName() + "." + (isAab ? "apk" : "ap_");
File srcJavaDir = createDir(buildDir, "generated/src");
File rJavaDir = createDir(buildDir, "generated/symbols");
if (!compiler.runAaptPackage(manifestFile, resDir, tmpPackageName, srcJavaDir, rJavaDir)) {
return false;
if (isAab) {
if (!compiler.runAapt2Compile(resDir)) {
return false;
}
if (!compiler.runAapt2Link(manifestFile, tmpPackageName, rJavaDir)) {
return false;
}
} else {
if (!compiler.runAaptPackage(manifestFile, resDir, tmpPackageName, srcJavaDir, rJavaDir)) {
return false;
}
}
if (reporter != null) {
reporter.report(30);
@ -1486,6 +1517,8 @@ public final class Compiler {
// Create class files.
out.println("________Compiling source files");
File classesDir = createDir(buildDir, "classes");
File tmpDir = createDir(buildDir, "tmp");
String dexedClassesDir = tmpDir.getAbsolutePath();
if (!compiler.generateRClasses(classesDir)) {
return false;
}
@ -1513,8 +1546,6 @@ public final class Compiler {
// method of identifying via a hash of the path won't work when files
// are copied into temporary storage) and processed via a hacked up version of
// Android SDK's Dex Ant task
File tmpDir = createDir(buildDir, "tmp");
String dexedClassesDir = tmpDir.getAbsolutePath();
if (!compiler.runMultidex(classesDir, dexedClassesDir)) {
return false;
}
@ -1522,30 +1553,36 @@ public final class Compiler {
reporter.report(85);
}
// Seal the apk with ApkBuilder
out.println("________Invoking ApkBuilder");
String fileName = outputFileName;
if (fileName == null) {
fileName = project.getProjectName() + ".apk";
}
String apkAbsolutePath = deployDir.getAbsolutePath() + SLASH + fileName;
if (!compiler.runApkBuilder(apkAbsolutePath, tmpPackageName, dexedClassesDir)) {
return false;
}
if (reporter != null) {
reporter.report(95);
}
if (isAab) {
if (!compiler.bundleTool(buildDir, childProcessRam, tmpPackageName, outputFileName, deployDir, keystoreFilePath, dexedClassesDir)) {
return false;
}
} else {
// Seal the apk with ApkBuilder
out.println("________Invoking ApkBuilder");
String fileName = outputFileName;
if (fileName == null) {
fileName = project.getProjectName() + ".apk";
}
String apkAbsolutePath = deployDir.getAbsolutePath() + SLASH + fileName;
if (!compiler.runApkBuilder(apkAbsolutePath, tmpPackageName, dexedClassesDir)) {
return false;
}
if (reporter != null) {
reporter.report(95);
}
// ZipAlign the apk file
out.println("________ZipAligning the apk file");
if (!compiler.runZipAlign(apkAbsolutePath, tmpDir)) {
return false;
}
// ZipAlign the apk file
out.println("________ZipAligning the apk file");
if (!compiler.runZipAlign(apkAbsolutePath, tmpDir)) {
return false;
}
// Sign the apk file
out.println("________Signing the apk file");
if (!compiler.runApkSigner(apkAbsolutePath, keystoreFilePath)) {
return false;
// Sign the apk file
out.println("________Signing the apk file");
if (!compiler.runApkSigner(apkAbsolutePath, keystoreFilePath)) {
return false;
}
}
if (reporter != null) {
@ -2282,7 +2319,7 @@ public final class Compiler {
aaptPackageCommandLineArgs.add("--output-text-symbols");
aaptPackageCommandLineArgs.add(symbolOutputDir.getAbsolutePath());
aaptPackageCommandLineArgs.add("--no-version-vectors");
appRJava = new File(sourceOutputDir, packageName.replaceAll("\\.", "/") + "/R.java");
appRJava = new File(sourceOutputDir, packageName.replaceAll("\\.", SLASH) + SLASH + "R.java");
appRTxt = new File(symbolOutputDir, "R.txt");
}
String[] aaptPackageCommandLine = aaptPackageCommandLineArgs.toArray(new String[aaptPackageCommandLineArgs.size()]);
@ -2304,6 +2341,142 @@ public final class Compiler {
return true;
}
private boolean runAapt2Compile(File resDir) {
resourcesZip = new File(resDir, "resources.zip");
String aaptTool;
String aapt2Tool;
String osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) {
aaptTool = MAC_AAPT_TOOL;
aapt2Tool = MAC_AAPT2_TOOL;
} else if (osName.equals("Linux")) {
aaptTool = LINUX_AAPT_TOOL;
aapt2Tool = LINUX_AAPT2_TOOL;
} else if (osName.startsWith("Windows")) {
aaptTool = WINDOWS_AAPT_TOOL;
aapt2Tool = WINDOWS_AAPT2_TOOL;
} else {
LOG.warning("YAIL compiler - cannot run AAPT2 on OS " + osName);
err.println("YAIL compiler - cannot run AAPT2 on OS " + osName);
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT2"));
return false;
}
if (!mergeResources(resDir, project.getBuildDirectory(), aaptTool)) {
LOG.warning("Unable to merge resources");
err.println("Unable to merge resources");
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT"));
return false;
}
libSetup(); // Setup /tmp/lib64 on Linux
List<String> aapt2CommandLine = new ArrayList<>();
aapt2CommandLine.add(getResource(aapt2Tool));
aapt2CommandLine.add("compile");
aapt2CommandLine.add("--dir");
aapt2CommandLine.add(mergedResDir.getAbsolutePath());
aapt2CommandLine.add("-o");
aapt2CommandLine.add(resourcesZip.getAbsolutePath());
aapt2CommandLine.add("--no-crunch");
aapt2CommandLine.add("-v");
String[] aapt2CompileCommandLine = aapt2CommandLine.toArray(new String[0]);
long startAapt2 = System.currentTimeMillis();
if (!Execution.execute(null, aapt2CompileCommandLine, System.out, System.err)) {
LOG.warning("YAIL compiler - AAPT2 compile execution failed.");
err.println("YAIL compiler - AAPT2 compile execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT2 compile"));
return false;
}
String aaptTimeMessage = "AAPT2 compile time: " + ((System.currentTimeMillis() - startAapt2) / 1000.0) + " seconds";
out.println(aaptTimeMessage);
LOG.info(aaptTimeMessage);
return true;
}
private boolean runAapt2Link(File manifestFile, String tmpPackageName, File symbolOutputDir) {
String aapt2Tool;
String osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) {
aapt2Tool = MAC_AAPT2_TOOL;
} else if (osName.equals("Linux")) {
aapt2Tool = LINUX_AAPT2_TOOL;
} else if (osName.startsWith("Windows")) {
aapt2Tool = WINDOWS_AAPT2_TOOL;
} else {
LOG.warning("YAIL compiler - cannot run AAPT2 on OS " + osName);
err.println("YAIL compiler - cannot run AAPT2 on OS " + osName);
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT2"));
return false;
}
appRTxt = new File(symbolOutputDir, "R.txt");
List<String> aapt2CommandLine = new ArrayList<>();
aapt2CommandLine.add(getResource(aapt2Tool));
aapt2CommandLine.add("link");
aapt2CommandLine.add("--proto-format");
aapt2CommandLine.add("-o");
aapt2CommandLine.add(tmpPackageName);
aapt2CommandLine.add("-I");
aapt2CommandLine.add(getResource(ANDROID_RUNTIME));
aapt2CommandLine.add("-R");
aapt2CommandLine.add(resourcesZip.getAbsolutePath());
aapt2CommandLine.add("-A");
aapt2CommandLine.add(createDir(project.getBuildDirectory(), ASSET_DIR_NAME).getAbsolutePath());
aapt2CommandLine.add("--manifest");
aapt2CommandLine.add(manifestFile.getAbsolutePath());
aapt2CommandLine.add("--output-text-symbols");
aapt2CommandLine.add(appRTxt.getAbsolutePath());
aapt2CommandLine.add("--auto-add-overlay");
aapt2CommandLine.add("--no-version-vectors");
aapt2CommandLine.add("--no-auto-version");
aapt2CommandLine.add("--no-version-transitions");
aapt2CommandLine.add("--no-resource-deduping");
aapt2CommandLine.add("-v");
String[] aapt2LinkCommandLine = aapt2CommandLine.toArray(new String[0]);
long startAapt2 = System.currentTimeMillis();
if (!Execution.execute(null, aapt2LinkCommandLine, System.out, System.err)) {
LOG.warning("YAIL compiler - AAPT2 link execution failed.");
err.println("YAIL compiler - AAPT2 link execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT2 link"));
return false;
}
String aaptTimeMessage = "AAPT2 link time: " + ((System.currentTimeMillis() - startAapt2) / 1000.0) + " seconds";
out.println(aaptTimeMessage);
LOG.info(aaptTimeMessage);
return true;
}
private boolean bundleTool(File buildDir, int childProcessRam, String tmpPackageName,
String outputFileName, File deployDir, String keystoreFilePath, String dexedClassesDir) {
try {
String jarsignerTool = "jarsigner";
String fileName = outputFileName;
if (fileName == null) {
fileName = project.getProjectName() + ".aab";
}
AabCompiler aabCompiler = new AabCompiler(out, buildDir, childProcessRam - 200)
.setLibsDir(libsDir)
.setProtoApk(new File(tmpPackageName))
.setJarsigner(jarsignerTool)
.setBundletool(getResource(BUNDLETOOL_JAR))
.setDeploy(deployDir.getAbsolutePath() + SLASH + fileName)
.setKeystore(keystoreFilePath)
.setDexDir(dexedClassesDir);
Future<Boolean> aab = Executors.newSingleThreadExecutor().submit(aabCompiler);
return aab.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return false;
}
private boolean insertNativeLibs(File buildDir){
/**
* Native libraries are targeted for particular processor architectures.

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2019 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
@ -23,28 +23,31 @@ import java.util.zip.ZipFile;
*/
public final class Main {
public final static String APK_EXTENSION_VALUE = "apk";
public final static String AAB_EXTENSION_VALUE = "aab";
static class CommandLineOptions {
@Option(name = "--isForCompanion", usage = "create the MIT AI2 Companion APK")
boolean isForCompanion = false;
@Option(name = "--inputZipFile", required = true,
usage = "the ZIP file of the project to build")
usage = "the ZIP file of the project to build")
File inputZipFile;
@Option(name = "--userName", required = true,
usage = "the name of the user building the project")
usage = "the name of the user building the project")
String userName;
@Option(name = "--outputDir", required = true,
usage = "the directory in which to put the output of the build")
usage = "the directory in which to put the output of the build")
File outputDir;
@Option(name = "--childProcessRamMb",
usage = "Maximum ram that can be used by a child processes, in MB.")
usage = "Maximum ram that can be used by a child processes, in MB.")
int childProcessRamMb = 2048;
@Option(name = "--dexCacheDir",
usage = "the directory to cache the pre-dexed libraries")
usage = "the directory to cache the pre-dexed libraries")
String dexCacheDir = null;
@Option(name = "--includeDangerousPermissions",
@ -63,6 +66,10 @@ public final class Main {
@Option(name = "--isForEmulator",
usage = "Exclude native libraries for emulator.")
boolean isForEmulator = false;
@Option(name = "--ext",
usage = "Specifies the build type to use.")
String ext = "apk";
}
private static CommandLineOptions commandLineOptions = new CommandLineOptions();
@ -76,7 +83,7 @@ public final class Main {
/**
* Main entry point.
*
* @param args command line arguments
* @param args command line arguments
*/
public static void main(String[] args) {
@ -106,7 +113,9 @@ public final class Main {
commandLineOptions.includeDangerousPermissions,
commandLineOptions.extensions,
commandLineOptions.childProcessRamMb,
commandLineOptions.dexCacheDir, null);
commandLineOptions.dexCacheDir,
null,
AAB_EXTENSION_VALUE.equals(commandLineOptions.ext));
System.exit(result.getResult());
}

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2017 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2019 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
@ -121,7 +121,7 @@ public final class ProjectBuilder {
Result build(String userName, ZipFile inputZip, File outputDir, String outputFileName,
boolean isForCompanion, boolean isForEmulator, boolean includeDangerousPermissions, String[] extraExtensions,
int childProcessRam, String dexCachePath, BuildServer.ProgressReporter reporter) {
int childProcessRam, String dexCachePath, BuildServer.ProgressReporter reporter, boolean isAab) {
try {
// Download project files into a temporary directory
File projectRoot = createNewTempDir();
@ -168,7 +168,7 @@ public final class ProjectBuilder {
boolean success =
Compiler.compile(project, componentTypes, componentBlocks, console, console, userErrors,
isForCompanion, isForEmulator, includeDangerousPermissions, keyStorePath,
childProcessRam, dexCachePath, outputFileName, reporter);
childProcessRam, dexCachePath, outputFileName, reporter, isAab);
console.close();
userErrors.close();
@ -181,7 +181,7 @@ public final class ProjectBuilder {
// Locate output file
String fileName = outputFileName;
if (fileName == null) {
fileName = project.getProjectName() + ".apk";
fileName = project.getProjectName() + (isAab ? ".aab" : ".apk");
}
File outputFile = new File(projectRoot,
"build/deploy/" + fileName);

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

View File

@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
/**

View File

@ -1,5 +1,5 @@
;;; Copyright 2009-2011 Google, All Rights reserved
;;; Copyright 2011-2018 MIT, All rights reserved
;;; Copyright 2011-2021 MIT, All rights reserved
;;; Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt
;;; These are the functions that define the YAIL (Young Android Intermediate Language) runtime They

View File

@ -0,0 +1,79 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2021 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* AabZipper receives the source directory with the Android App Bundle previosuly generated by
* {@link com.google.appinventor.buildserver.AabCompiler} and builds from it a ZIP file, placed into the specified
* destination file.
*
* @author diego@barreiro.xyz (Diego Barreiro)
*/
public class AabZipper {
private AabZipper() {
}
public static boolean zipBundle(File src, File dest, String root) {
try (
FileOutputStream fos = new FileOutputStream(dest);
ZipOutputStream zipOut = new ZipOutputStream(fos);
) {
zipFile(src, src.getName(), zipOut, root);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
private static void zipFile(File fileToZip, String fileName, ZipOutputStream zipOut, String root) throws IOException {
if (fileToZip.isHidden()) {
return;
}
String zipFileName = fileName;
if (zipFileName.startsWith(root)) {
zipFileName = zipFileName.substring(root.length());
}
boolean windows = !File.separator.equals("/");
if (windows) {
zipFileName = zipFileName.replace(File.separator, "/");
}
if (fileToZip.isDirectory()) {
if (zipFileName.endsWith("/")) {
zipOut.putNextEntry(new ZipEntry(zipFileName));
} else {
zipOut.putNextEntry(new ZipEntry(zipFileName + "/"));
}
zipOut.closeEntry();
File[] children = fileToZip.listFiles();
assert children != null;
for (File childFile : children) {
zipFile(childFile, fileName + File.separator + childFile.getName(), zipOut, root);
}
return;
}
FileInputStream fis = new FileInputStream(fileToZip);
ZipEntry zipEntry = new ZipEntry(zipFileName);
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
fis.close();
}
}

View File

@ -168,6 +168,7 @@
<copy toFile="${public.deps.dir}/android.jar" file="${android.lib}" />
<copy toFile="${public.deps.dir}/dx.jar" file="${lib.dir}/android/tools/dx.jar" />
<copy toFile="${public.deps.dir}/apksigner.jar" file="${lib.dir}/android/tools/apksigner.jar" />
<copy toFile="${public.deps.dir}/bundletool.jar" file="${lib.dir}/android/tools/bundletool-all-0.15.0.jar" />
<copy toFile="${public.deps.dir}/CommonVersion.jar" file="${build.dir}/common/CommonVersion.jar" />
<!-- Add extension libraries here -->

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -9,8 +9,6 @@ reports/
*~
*.out
.DS_Store*
*.out
*~
*.iml
.idea/
.externalToolBuilders/