Commit 6aa70536 authored by Evan W. Patton's avatar Evan W. Patton Committed by Jeffrey Schiller

Implement UsesAssets support for extensions

Change-Id: Id0995d95294c4e604b17eebf495f3ee04b87c36d
parent a8f5b679
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2017 MIT, All rights reserved
// Copyright 2011-2018 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
......@@ -172,13 +172,19 @@ public final class AssetManager implements ProjectChangeListener {
allow = false;
// Filter : For files in directly in EXTERNAL_COMPS_FOLDER/COMP_FOLDER
if (StringUtils.countMatches(fileId, "/") == 3) {
int depth = StringUtils.countMatches(fileId, "/");
if (depth == 3) {
// Filter : For classes.jar File
if (name.equals("classes.jar")) {
allow = true;
}
} else if (depth > 3) {
String[] parts = fileId.split("/");
if (ASSETS_FOLDER.equals(parts[3])) {
return true;
}
}
}
}
......
......@@ -68,6 +68,8 @@
<!-- Map assets for build server -->
<fileset dir="${lib.dir}/leaflet" includes="leaflet.js,leaflet.css" />
<fileset dir="${lib.dir}/leaflet/assets" includes="*"/>
<!-- Component/extension assets (for testing) -->
<fileset dir="${appinventor.dir}/components/src" includes="**/assets/*"/>
</copy>
<copy toFile="${classes.files.dir}/osmdroid.aar" file="${lib.dir}/osmdroid/osmdroid-5.6.6.aar" />
<copy toFile="${classes.files.dir}/osmdroid.jar" file="${lib.dir}/osmdroid/osmdroid-5.6.6.jar" />
......
......@@ -114,6 +114,7 @@ public final class Compiler {
private static final String ARMEABI_DIR_NAME = "armeabi";
private static final String ARMEABI_V7A_DIR_NAME = "armeabi-v7a";
private static final String ASSET_DIR_NAME = "assets";
private static final String EXT_COMPS_DIR_NAME = "external_comps";
private static final String DEFAULT_APP_NAME = "";
......@@ -1467,7 +1468,7 @@ public final class Compiler {
private boolean runAaptPackage(File manifestFile, File resDir, String tmpPackageName, File sourceOutputDir, File symbolOutputDir) {
// Need to make sure assets directory exists otherwise aapt will fail.
createDir(project.getAssetsDirectory());
final File mergedAssetsDir = createDir(project.getBuildDirectory(), ASSET_DIR_NAME);
String aaptTool;
String osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) {
......@@ -1498,7 +1499,7 @@ public final class Compiler {
aaptPackageCommandLineArgs.add("-S");
aaptPackageCommandLineArgs.add(mergedResDir.getAbsolutePath());
aaptPackageCommandLineArgs.add("-A");
aaptPackageCommandLineArgs.add(project.getAssetsDirectory().getAbsolutePath());
aaptPackageCommandLineArgs.add(mergedAssetsDir.getAbsolutePath());
aaptPackageCommandLineArgs.add("-I");
aaptPackageCommandLineArgs.add(getResource(ANDROID_RUNTIME));
aaptPackageCommandLineArgs.add("-F");
......@@ -1619,25 +1620,28 @@ public final class Compiler {
}
private boolean attachCompAssets() {
createDir(project.getAssetsDirectory()); // Needed to insert resources.
createDir(project.getBuildDirectory()); // Needed to insert resources.
try {
// Gather non-library assets to be added to apk's Asset directory.
// The assets directory have been created before this.
File compAssetDir = createDir(project.getAssetsDirectory(),
ASSET_DIRECTORY);
File mergedAssetDir = createDir(project.getBuildDirectory(), ASSET_DIR_NAME);
// Copy component/extension assets to build/assets
for (String type : assetsNeeded.keySet()) {
for (String assetName : assetsNeeded.get(type)) {
File targetDir = compAssetDir;
String sourcePath = "";
String pathSuffix = RUNTIME_FILES_DIR + assetName;
File targetDir = mergedAssetDir;
String sourcePath;
if (simpleCompTypes.contains(type)) {
String pathSuffix = RUNTIME_FILES_DIR + assetName;
sourcePath = getResource(pathSuffix);
} else if (extCompTypes.contains(type)) {
sourcePath = getExtCompDirPath(type) + pathSuffix;
targetDir = createDir(targetDir, EXT_COMPS_DIR_NAME);
targetDir = createDir(targetDir, type);
final String extCompDir = getExtCompDirPath(type);
sourcePath = getExtAssetPath(extCompDir, assetName);
// If targetDir's location is changed here, you must update Form.java in components to
// reference the new location. The path for assets in compiled apps is assumed to be
// assets/EXTERNAL-COMP-PACKAGE/ASSET-NAME
targetDir = createDir(targetDir, basename(extCompDir));
} else {
userErrors.print(String.format(ERROR_IN_STAGE, "Assets"));
return false;
......@@ -1646,6 +1650,16 @@ public final class Compiler {
Files.copy(new File(sourcePath), new File(targetDir, assetName));
}
}
// Copy project assets to build/assets
File[] assets = project.getAssetsDirectory().listFiles();
if (assets != null) {
for (File asset : assets) {
if (asset.isFile()) {
Files.copy(asset, new File(mergedAssetDir, asset.getName()));
}
}
}
return true;
} catch (IOException e) {
e.printStackTrace();
......@@ -1958,4 +1972,12 @@ public final class Compiler {
}
throw new IllegalStateException("Project lacks extension directory for " + type);
}
private static String basename(String path) {
return new File(path).getName();
}
private static String getExtAssetPath(String extCompDir, String assetName) {
return extCompDir + File.separator + ASSET_DIR_NAME + File.separator + assetName;
}
}
......@@ -6,15 +6,21 @@
package com.google.appinventor.components.runtime;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.content.res.AssetManager;
import android.support.v7.app.ActionBar;
import android.app.Activity;
import android.app.Dialog;
......@@ -109,6 +115,8 @@ public class Form extends AppInventorCompatActivity
public static final String APPINVENTOR_URL_SCHEME = "appinventor";
public static final String ASSETS_PREFIX = "file:///android_asset/";
private static final int DEFAULT_PRIMARY_COLOR_DARK = PaintUtil.hexStringToInt(ComponentConstants.DEFAULT_PRIMARY_DARK_COLOR);
private static final int DEFAULT_ACCENT_COLOR = PaintUtil.hexStringToInt(ComponentConstants.DEFAULT_ACCENT_COLOR);
......@@ -2273,4 +2281,38 @@ public class Form extends AppInventorCompatActivity
public boolean isDarkTheme() {
return usesDarkTheme;
}
/**
* Determines a WebView compatible, REPL-sensitive path for an asset provided by a given
* extension.
*
* @param component The extension that is requesting an asset
* @param asset The asset filename
* @return A string containing the path to the asset
* @throws FileNotFoundException if the asset cannot be located
*/
public String getAssetPathForExtension(Component component, String asset) throws FileNotFoundException {
String extPkgName = component.getClass().getPackage().getName();
return ASSETS_PREFIX + extPkgName + "/" + asset;
}
/**
* Opens an asset for reading as an InputStream. If the asset cannot be found, an IOException will
* be raised.
*
* @param component The extension that is requesting an asset
* @param asset The asset filename
* @return A new input stream for the requested asset. The caller is responsible for closing the
* stream to prevent resource leaking.
* @throws IOException if the asset is not found or cannot be read
*/
public InputStream openAssetForExtension(Component component, String asset) throws IOException {
String path = getAssetPathForExtension(component, asset);
if (path.startsWith(ASSETS_PREFIX)) {
final AssetManager am = getAssets();
return am.open(path.substring(ASSETS_PREFIX.length()));
} else {
return new FileInputStream(new File(URI.create(path)));
}
}
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Copyright 2011-2018 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
......@@ -26,6 +27,7 @@ import android.support.v7.internal.widget.TintImageView;
import android.text.Html;
import android.view.View;
import android.view.ViewGroup;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.common.ComponentConstants;
import com.google.appinventor.components.runtime.util.AppInvHTTPD;
......@@ -52,6 +54,7 @@ import android.widget.Toast;
public class ReplForm extends Form {
private static final String LOG_TAG = ReplForm.class.getSimpleName();
private AppInvHTTPD httpdServer = null;
public static ReplForm topform;
private static final String REPL_ASSET_DIR =
......@@ -74,7 +77,7 @@ public class ReplForm extends Form {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Log.d("ReplForm", "onCreate");
Log.d(LOG_TAG, "onCreate");
loadedExternalDexs = new ArrayList<String>();
Intent intent = getIntent();
processExtras(intent, false);
......@@ -115,7 +118,7 @@ public class ReplForm extends Form {
public void setFormName(String formName) {
this.formName = formName;
Log.d("ReplForm", "formName is now " + formName);
Log.d(LOG_TAG, "formName is now " + formName);
}
@Override
......@@ -124,7 +127,7 @@ public class ReplForm extends Form {
}
protected void setResult(Object result) {
Log.d("ReplForm", "setResult: " + result);
Log.d(LOG_TAG, "setResult: " + result);
replResult = result;
replResultFormName = formName;
}
......@@ -186,15 +189,15 @@ public class ReplForm extends Form {
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d("ReplForm", "onNewIntent Called");
Log.d(LOG_TAG, "onNewIntent Called");
processExtras(intent, true);
}
void HandleReturnValues() {
Log.d("ReplForm", "HandleReturnValues() Called, replResult = " + replResult);
Log.d(LOG_TAG, "HandleReturnValues() Called, replResult = " + replResult);
if (replResult != null) { // Act as if it was returned
OtherScreenClosed(replResultFormName, replResult);
Log.d("ReplForm", "Called OtherScreenClosed");
Log.d(LOG_TAG, "Called OtherScreenClosed");
replResult = null;
}
}
......@@ -202,14 +205,14 @@ public class ReplForm extends Form {
protected void processExtras(Intent intent, boolean restart) {
Bundle extras = intent.getExtras();
if (extras != null) {
Log.d("ReplForm", "extras: " + extras);
Log.d(LOG_TAG, "extras: " + extras);
Iterator<String> keys = extras.keySet().iterator();
while (keys.hasNext()) {
Log.d("ReplForm", "Extra Key: " + keys.next());
Log.d(LOG_TAG, "Extra Key: " + keys.next());
}
}
if ((extras != null) && extras.getBoolean("rundirect")) {
Log.d("ReplForm", "processExtras rundirect is true and restart is " + restart);
Log.d(LOG_TAG, "processExtras rundirect is true and restart is " + restart);
isDirect = true;
assetsLoaded = true;
if (restart) {
......@@ -238,10 +241,10 @@ public class ReplForm extends Form {
if (httpdServer == null) {
checkAssetDir();
httpdServer = new AppInvHTTPD(8001, new File(REPL_ASSET_DIR), secure, this); // Probably should make the port variable
Log.i("ReplForm", "started AppInvHTTPD");
Log.i(LOG_TAG, "started AppInvHTTPD");
}
} catch (IOException ex) {
Log.e("ReplForm", "Setting up NanoHTTPD: " + ex.toString());
Log.e(LOG_TAG, "Setting up NanoHTTPD: " + ex.toString());
}
}
......@@ -283,7 +286,7 @@ public class ReplForm extends Form {
File dexOutput = activeForm.$context().getDir("componentDexs", Context.MODE_PRIVATE);
File componentFolder = new File(REPL_COMP_DIR );
if (!checkComponentDir()) {
Log.d("ReplForm", "Unable to create components directory");
Log.d(LOG_TAG, "Unable to create components directory");
dispatchErrorOccurredEventDialog(this, "loadComponents", ErrorMessages.ERROR_EXTENSION_ERROR,
1, "App Inventor", "Unable to create component directory.");
return;
......@@ -299,7 +302,7 @@ public class ReplForm extends Form {
File loadComponent = new File(compFolder.getPath() + File.separator + compFolder.getName() + ".jar");
component.renameTo(loadComponent);
if (loadComponent.exists() && !loadedExternalDexs.contains(loadComponent.getName())) {
Log.d("ReplForm", "Loading component dex " + loadComponent.getAbsolutePath());
Log.d(LOG_TAG, "Loading component dex " + loadComponent.getAbsolutePath());
loadedExternalDexs.add(loadComponent.getName());
sb.append(File.pathSeparatorChar);
sb.append(loadComponent.getAbsolutePath());
......@@ -309,8 +312,8 @@ public class ReplForm extends Form {
DexClassLoader dexCloader = new DexClassLoader(sb.substring(1), dexOutput.getAbsolutePath(),
null, parentClassLoader);
Thread.currentThread().setContextClassLoader(dexCloader);
Log.d("ReplForm", Thread.currentThread().toString());
Log.d("ReplForm", Looper.getMainLooper().getThread().toString());
Log.d(LOG_TAG, Thread.currentThread().toString());
Log.d(LOG_TAG, Looper.getMainLooper().getThread().toString());
Looper.getMainLooper().getThread().setContextClassLoader(dexCloader);
}
......@@ -322,6 +325,40 @@ public class ReplForm extends Form {
updateTitle();
}
@Override
public String getAssetPathForExtension(Component component, String asset) throws FileNotFoundException {
// For testing extensions, we allow external = false, but still compile the assets into the
// companion for testing. When external = true, we are assuming this is an extension loaded
// into the production companion.
SimpleObject annotation = component.getClass().getAnnotation(SimpleObject.class);
if (annotation != null && !annotation.external()) {
return ASSETS_PREFIX + asset;
}
String extensionId = component.getClass().getName();
String pkgPath = null;
while (extensionId.contains(".")) {
File dir = new File(REPL_COMP_DIR + extensionId + "/assets");
if (dir.exists() && dir.isDirectory()) {
// found the extension directory
pkgPath = dir.getAbsolutePath();
break;
}
// Walk up the FQCN to determine possible extension identifier
extensionId = extensionId.substring(0, extensionId.lastIndexOf('.'));
}
if (pkgPath != null) {
File result = new File(pkgPath, asset);
Log.d(LOG_TAG, "result = " + result.getAbsolutePath());
if (result.exists()) {
return "file://" + result.getAbsolutePath();
}
}
throw new FileNotFoundException();
}
@Override
protected boolean isRepl() {
return true;
......
......@@ -906,7 +906,7 @@ class NativeOpenStreetMapController implements MapController, MapListener {
SVG markerSvg = null;
if (defaultMarkerSVG == null) {
try {
defaultMarkerSVG = SVG.getFromAsset(view.getContext().getAssets(), "component/marker.svg");
defaultMarkerSVG = SVG.getFromAsset(view.getContext().getAssets(), "marker.svg");
} catch (SVGParseException e) {
Log.e(TAG, "Invalid SVG in Marker asset", e);
} catch (IOException e) {
......
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2017 MIT, All rights reserved
// Copyright 2011-2018 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
......@@ -59,7 +59,8 @@ import javax.tools.FileObject;
* { "name": "PARAM-NAME",
* "type": "YAIL-TYPE"},*
* ]},+
* ]
* ],
* ("assets": ["FILENAME",*])?
* }
*
* @author lizlooney@google.com (Liz Looney)
......@@ -130,7 +131,19 @@ public final class ComponentDescriptorGenerator extends ComponentProcessor {
outputBlockMethod(method.name, method, sb, method.userVisible, method.deprecated);
separator = ",\n ";
}
sb.append("]}\n");
sb.append("]");
// Output assets for extensions (consumed by ExternalComponentGenerator and buildserver)
if (component.external && component.assets.size() > 0) {
sb.append(",\n \"assets\": [");
for (String asset : component.assets) {
sb.append("\"");
sb.append(asset.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\""));
sb.append("\",");
}
sb.setLength(sb.length() - 1);
sb.append("]");
}
sb.append("}\n");
}
private void outputProperty(String propertyName, DesignerProperty dp, StringBuilder sb) {
......
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2015 MIT, All rights reserved
// Copyright 2015-2018 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
......@@ -9,7 +9,6 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
......@@ -109,6 +108,7 @@ public class ExternalComponentGenerator {
generateExternalComponentDescriptors(name, entry.getValue());
for (ExternalComponentInfo info : entry.getValue()) {
copyIcon(name, info.type, info.descriptor);
copyAssets(name, info.type, info.descriptor);
}
generateExternalComponentBuildFiles(name, entry.getValue());
generateExternalComponentOtherFiles(name);
......@@ -230,6 +230,42 @@ public class ExternalComponentGenerator {
}
}
private static void copyAssets(String packageName, String type, JSONObject componentDescriptor) throws IOException, JSONException {
JSONArray assets = componentDescriptor.optJSONArray("assets");
if (assets == null) {
return;
}
// Get asset source directory
String packagePath = packageName.replace('.', File.separatorChar);
File sourceDir = new File(externalComponentsDirPath + File.separator + ".." + File.separator + ".." + File.separator + "src" + File.separator + packagePath);
File assetSrcDir = new File(sourceDir, "assets");
if (!assetSrcDir.exists() || !assetSrcDir.isDirectory()) {
return;
}
// Get asset dest directory
File destDir = new File(externalComponentsDirPath + File.separator + packageName + File.separator);
File assetDestDir = new File(destDir, "assets");
if (assetDestDir.exists() && !deleteRecursively(assetDestDir)) {
throw new IllegalStateException("Unable to delete the assets directory for the extension.");
}
if (!assetDestDir.mkdirs()) {
throw new IllegalStateException("Unable to create the assets directory for the extension.");
}
// Copy assets
for (int i = 0; i < assets.length(); i++) {
String asset = assets.getString(i);
if (!asset.isEmpty()) {
if (!copyFile(assetSrcDir.getAbsolutePath() + File.separator + asset,
assetDestDir.getAbsolutePath() + File.separator + asset)) {
throw new IllegalStateException("Unable to copy asset to destination.");
}
}
}
}
private static void generateExternalComponentOtherFiles(String packageName) throws IOException {
String extensionDirPath = externalComponentsDirPath + File.separator + packageName;
......@@ -346,5 +382,20 @@ public class ExternalComponentGenerator {
return componentPackage;
}
private static boolean deleteRecursively(File dirOrFile) {
if (dirOrFile.isFile()) {
return dirOrFile.delete();
} else {
boolean result = true;
File[] children = dirOrFile.listFiles();
if (children != null) {
for (File child : children) {
result = result && deleteRecursively(child);
}
}
return result && dirOrFile.delete();
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment