diff --git a/appinventor/appengine/src/com/google/appinventor/server/FileImporterImpl.java b/appinventor/appengine/src/com/google/appinventor/server/FileImporterImpl.java index b28e5011bfa..048bad031c3 100644 --- a/appinventor/appengine/src/com/google/appinventor/server/FileImporterImpl.java +++ b/appinventor/appengine/src/com/google/appinventor/server/FileImporterImpl.java @@ -53,7 +53,7 @@ public final class FileImporterImpl implements FileImporter { // Maximum size of an uploaded asset, in megabytes. - private static final Flag maxAssetSizeMegs = Flag.createFlag("max.asset.size.megs", 9f); + private static final Flag maxAssetSizeMegs = Flag.createFlag("max.asset.size.megs", 15f); private static final Logger LOG = Logger.getLogger(FileImporterImpl.class.getName()); diff --git a/appinventor/appengine/src/com/google/appinventor/server/storage/ObjectifyStorageIo.java b/appinventor/appengine/src/com/google/appinventor/server/storage/ObjectifyStorageIo.java index 31dc511f84c..77e35452849 100644 --- a/appinventor/appengine/src/com/google/appinventor/server/storage/ObjectifyStorageIo.java +++ b/appinventor/appengine/src/com/google/appinventor/server/storage/ObjectifyStorageIo.java @@ -1188,7 +1188,7 @@ public void run(Objectify datastore) { @Override public int getMaxJobSizeBytes() { // TODO(user): what should this mean? - return 5 * 1024 * 1024; + return 15 * 1024 * 1024; } @Override diff --git a/appinventor/buildserver/src/com/google/appinventor/buildserver/tasks/android/AttachAarLibs.java b/appinventor/buildserver/src/com/google/appinventor/buildserver/tasks/android/AttachAarLibs.java index b1c525b0def..222ea54687a 100644 --- a/appinventor/buildserver/src/com/google/appinventor/buildserver/tasks/android/AttachAarLibs.java +++ b/appinventor/buildserver/src/com/google/appinventor/buildserver/tasks/android/AttachAarLibs.java @@ -7,19 +7,32 @@ import com.google.appinventor.buildserver.interfaces.BuildType; import com.google.appinventor.buildserver.TaskResult; +import com.google.appinventor.buildserver.YoungAndroidConstants; import com.google.appinventor.buildserver.context.AndroidCompilerContext; +import com.google.appinventor.buildserver.context.AndroidPaths; import com.google.appinventor.buildserver.interfaces.AndroidTask; import com.google.appinventor.buildserver.util.AARLibraries; import com.google.appinventor.buildserver.util.AARLibrary; import com.google.appinventor.buildserver.util.ExecutorUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; - +import java.io.InputStream; +import java.io.OutputStream; import java.util.Arrays; +import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static com.google.appinventor.common.constants.YoungAndroidStructureConstants.ASSETS_FOLDER; /** @@ -31,19 +44,30 @@ public class AttachAarLibs implements AndroidTask { @Override public TaskResult execute(AndroidCompilerContext context) { final File explodedBaseDir = ExecutorUtils.createDir(context.getPaths().getBuildDir(), - "exploded-aars"); + "exploded-aars"); final File generatedDir = ExecutorUtils.createDir(context.getPaths().getBuildDir(), - "generated"); + "generated"); final File genSrcDir = ExecutorUtils.createDir(generatedDir, "src"); context.getComponentInfo().setExplodedAarLibs(new AARLibraries(genSrcDir)); final Set processedLibs = new HashSet<>(); // Attach the Android support libraries (needed by every app) context.getComponentInfo().getLibsNeeded().put("ANDROID", new HashSet<>(Arrays.asList( - context.getResources().getSupportAars()))); + context.getResources().getSupportAars()))); + + // Gather AAR assets to be added to apk's Asset directory. + // The assets directory have been created before this. + File mergedAssetDir = ExecutorUtils.createDir(context.getProject().getBuildDirectory(), + ASSETS_FOLDER); + + // Root directory for JNI native (.so) libraries + AndroidPaths paths = context.getPaths(); + File libsDir = ExecutorUtils.createDir(paths.getBuildDir(), YoungAndroidConstants.LIBS_DIR_NAME); + paths.setLibsDir(libsDir); // walk components list for libraries ending in ".aar" try { + final HashSet attachedAARs = new HashSet<>(); for (String type : context.getComponentInfo().getLibsNeeded().keySet()) { Iterator i = context.getComponentInfo().getLibsNeeded().get(type).iterator(); while (i.hasNext()) { @@ -63,11 +87,26 @@ public TaskResult execute(AndroidCompilerContext context) { context.getReporter().error("Unknown component type: " + type, true); return TaskResult.generateError("Error while attaching AAR libraries"); } - // explode libraries into ${buildDir}/exploded-aars// - AARLibrary aarLib = new AARLibrary(new File(sourcePath)); - aarLib.unpackToDirectory(explodedBaseDir); - context.getComponentInfo().getExplodedAarLibs().add(aarLib); - processedLibs.add(libname); + + File aarFile = new File(sourcePath); + // Resolve possible conflicts + final String packageName = getAarPackageName(aarFile); + if (packageName == null || packageName.trim().isEmpty()) { + context.getReporter().error("Unable to read packageName from: " + aarFile.getName(), true); + return TaskResult.generateError("Unable to read packageName from: " + aarFile.getName()); + } + if (!attachedAARs.contains(packageName)) { + // explode libraries into ${buildDir}/exploded-aars// + AARLibrary aarLib = new AARLibrary(aarFile); + aarLib.unpackToDirectory(explodedBaseDir); + context.getComponentInfo().getExplodedAarLibs().add(aarLib); + // Attach assets files & jni libraries if available + copyAssetsAndJni(aarFile, mergedAssetDir, libsDir); + processedLibs.add(libname); + attachedAARs.add(packageName); + } else { + System.out.println("Skip attaching duplicate AAR: " + aarFile.getName()); + } } } } @@ -79,4 +118,91 @@ public TaskResult execute(AndroidCompilerContext context) { return TaskResult.generateSuccess(); } + + private void copyAssetsAndJni(File aarFile, File mergedAssetDir, File libsDir) throws IOException { + try (ZipFile zip = new ZipFile(aarFile)) { + Enumeration entries = zip.entries(); + + final Set supportedABIs = new HashSet<>(); + supportedABIs.add("arm64-v8a"); + supportedABIs.add("armeabi-v7a"); + supportedABIs.add("x86"); + supportedABIs.add("x86_64"); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File targetFile = null; + final String entryName = entry.getName(); + + if (entryName.startsWith("assets/")) { + if (!entry.isDirectory()) { + targetFile = new File(mergedAssetDir, entryName.substring("assets/".length())); + if (!targetFile.getParentFile().exists()) { + // The target file may contain subfolders. + targetFile.getParentFile().mkdirs(); + } + } + } else if (entryName.startsWith("jni/") && entryName.endsWith(".so")) { + final String[] array = entryName.split("/", 3); + if (array.length < 3) { + continue; + } + final String abi = array[1]; + if (!supportedABIs.contains(abi)) { + System.out.println("Skip merging not supported ABI: " + abi); + continue; + } + final String libName = array[2]; + if (!entry.isDirectory()) { + File parentFile = new File(libsDir, abi); + if (!parentFile.exists()) { + // Create the parent ABI directory if absent + parentFile.mkdir(); + } + targetFile = new File(parentFile, libName); + } + } + if (targetFile != null && !targetFile.exists()) { + // Copy file contents from ZIP entry + try (InputStream is = zip.getInputStream(entry); + OutputStream os = new FileOutputStream(targetFile)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + os.write(buffer, 0, bytesRead); + } + } + } + } + } + } + + private String getAarPackageName(File aarFile) { + try (ZipFile zipFile = new ZipFile(aarFile)) { + // Look for AndroidManifest.xml inside the AAR + ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml"); + if (manifestEntry == null) { + return null; + } + + try (InputStream inputStream = zipFile.getInputStream(manifestEntry)) { + // Parse the manifest + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(inputStream); + document.getDocumentElement().normalize(); + + Element rootElement = document.getDocumentElement(); + String packageName = rootElement.getAttribute("package"); + + if (packageName != null && !packageName.isEmpty()) { + return packageName.toLowerCase(); + } else { + return null; + } + } + } catch (Exception e) { + return null; + } + } } diff --git a/appinventor/components/src/com/google/appinventor/components/scripts/ExternalComponentGenerator.java b/appinventor/components/src/com/google/appinventor/components/scripts/ExternalComponentGenerator.java index 438421b19a5..65d0a328ae4 100644 --- a/appinventor/components/src/com/google/appinventor/components/scripts/ExternalComponentGenerator.java +++ b/appinventor/components/src/com/google/appinventor/components/scripts/ExternalComponentGenerator.java @@ -12,8 +12,13 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + import org.json.JSONException; import org.json.JSONArray; import org.json.JSONObject; @@ -170,14 +175,17 @@ private static void generateExternalComponentBuildFiles(String packageName, List try { JSONArray librariesNeeded = componentBuildInfo.getJSONArray("libraries"); JSONArray librariesAar = new JSONArray(); + boolean ensureFreshDir = true; + for (int j = 0; j < librariesNeeded.length(); ++j) { // Copy Library files for Unjar and Jaring String library = librariesNeeded.getString(j); copyFile(buildServerClassDirPath + File.separator + library, extensionTempDirPath + File.separator + library); if (library.endsWith(".aar")) { - copyExternalAar(library, packageName); + copyExternalAar(library, packageName, ensureFreshDir); librariesAar.put(library); + ensureFreshDir = false; } } //empty the libraries meta-data to avoid redundancy @@ -349,8 +357,7 @@ private static Boolean copyFile(String srcPath, String dstPath) { return true; } - private static void copyExternalAar(String library, String packageName) - throws IOException { + private static void copyExternalAar(String library, String packageName, boolean ensureFreshDir) throws IOException { File sourceDir = new File(buildServerClassDirPath + File.separator); File aarFile = new File(sourceDir, library); if (!aarFile.exists() || !library.endsWith(".aar")) { @@ -359,7 +366,10 @@ private static void copyExternalAar(String library, String packageName) // Get aar dest directory File destDir = new File(externalComponentsDirPath + File.separator + packageName + File.separator); File aarDestDir = new File(destDir, "aars"); - ensureFreshDirectory(aarDestDir.getPath(), "Unable to delete the aars directory for the extension."); + if (ensureFreshDir) { + // Ensure fresh directory before put the first library + ensureFreshDirectory(aarDestDir.getPath(), "Unable to delete the aars directory for the extension."); + } System.out.println("Extensions : " + "Copying file aar " + library); copyFile(aarFile.getAbsolutePath(), aarDestDir.getAbsolutePath() + File.separator + library);