package de.blau.android.presets;
import java.io.FileInputStream;
import java.io.InputStream;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import de.blau.android.App;
import de.blau.android.util.Density;
import de.blau.android.util.Hash;
import de.blau.android.util.SavingHelper;
/**
* This class manages loading of Preset icons and asset files.
* Please see Preset.java for an explanation of possible data sources.
* @author Jan
*
*/
public class PresetIconManager {
/** context of own application */
private final Context context;
/** base path for downloaded icons */
private final String basePath;
private final static String ASSET_IMAGE_PREFIX = "images/";
/** Asset manager for default assets (e.g. icons) stored in a separate APK, if available */
private final AssetManager externalDefaultAssets;
/** Asset manager for internal preset assets */
private final AssetManager internalAssets;
/** Asset manager for preset assets stored in a separate APK, if available */
private final AssetManager externalAssets;
/** the name of an external package containing assets (may be null), used for debug output */
private final String externalAssetPackage;
private final static String EXTERNAL_DEFAULT_ASSETS_PACKAGE = "org.openstreetmap.vespucci.defaultpreset";
/**
* Creates a new PresetIconManager.
* @param context Vespucci context to use for loading data
* @param basePath Base path for images downloaded for this preset. May be null.
* @param externalAssetPackage Name of external package to use for loading assets. May be null.
*/
public PresetIconManager(Context context, String basePath, String externalAssetPackage) {
this.context = context;
this.basePath = basePath;
this.externalAssetPackage = externalAssetPackage;
AssetManager tmpExternalDefaultAssets = null;
try {
Context extCtx = context.createPackageContext(EXTERNAL_DEFAULT_ASSETS_PACKAGE, 0);
tmpExternalDefaultAssets = extCtx.getAssets();
} catch (NameNotFoundException e) {
Log.i("PresetIconManager", "External default asset package not installed");
} catch (Exception e) {
Log.e("PresetIconManager", "Exception while loading external default assets", e);
}
externalDefaultAssets = tmpExternalDefaultAssets;
AssetManager tmpExternalDataAssets = null;
if (externalAssetPackage != null) {
try {
Context extCtx = context.createPackageContext(externalAssetPackage, 0);
tmpExternalDataAssets = extCtx.getAssets();
} catch (NameNotFoundException e) {
Log.e("PresetIconManager", "External data asset package not found" + externalAssetPackage);
} catch (Exception e) {
Log.e("PresetIconManager", "Exception while loading external asset package " + externalAssetPackage, e);
}
}
externalAssets = tmpExternalDataAssets;
internalAssets = context.getAssets();
}
/**
* Gets a drawable for a URL.<br>
* If the URL is a HTTP(S) URL and a base path is given, it will be checked for the downloaded drawable.<br>
* Otherwise, the URL will be considered a relative path, checked for ".." to avoid path traversal,
* and it will be attempted to load the corresponding image from the asset image directory. Handles
* icons directly in a zipped presets director too.<br>
* @param url either a local preset url of the format "presets/xyz.png", or a http/https url
* @param size icon size in dp
* @return null if icon file not found or a drawable of [size]x[size] dp.
*/
public BitmapDrawable getDrawable(String url, int size) {
if (url == null) return null;
InputStream pngStream = null;
try {
if (basePath != null && externalAssetPackage == null) {
if ((url.startsWith("http://") || url.startsWith("https://"))) {
pngStream = new FileInputStream(basePath+"/"+hash(url)+".png");
} else if (url.endsWith(".png") && !url.contains("..")) {
pngStream = new FileInputStream(basePath+"/"+url);
}
} else if (!url.contains("..")) {
pngStream = openAsset(ASSET_IMAGE_PREFIX+url, true);
} else {
Log.e("PresetIconManager", "unknown icon URL type for " + url);
}
if (pngStream == null) return null;
BitmapDrawable drawable = new BitmapDrawable(App.resources(), BitmapFactory.decodeStream(pngStream)); // resources used only for density
drawable.getBitmap().setDensity(Bitmap.DENSITY_NONE);
int pxsize = Density.dpToPx(size);
drawable.setBounds(0, 0, pxsize, pxsize);
return drawable;
} catch (Exception e) {
Log.e("PresetIconManager", "Failed to load preset icon " + url, e);
return null;
} finally {
SavingHelper.close(pngStream);
}
}
/**
* Like {@link #getDrawable(String, int)}, but returns a transparent placeholder
* instead of null
*/
public Drawable getDrawableOrPlaceholder(String url, int size) {
Drawable result = getDrawable(url, size);
if (result != null) {
return result;
} else {
return getPlaceHolder(size);
}
}
/**
* Return a dummy icon
* @param size
* @return
*/
public Drawable getPlaceHolder(int size) {
Drawable placeholder = new ColorDrawable(ContextCompat.getColor(context, android.R.color.transparent));
int pxsize = Density.dpToPx(size);
placeholder.setBounds(0,0, pxsize, pxsize);
return placeholder;
}
/**
* Creates a unique identifier for the given value
* @param value the value to hash
* @return a unique, file-name safe identifier
*/
public static String hash(String value) {
return Hash.sha256(value).substring(0, 24);
}
/**
* Loads an asset, trying first the preset-specific external asset file (if given),
* then if allowDefaults is set the default external assets and default internal assets.
* @param path
* @param allowDefault if set to false, loading default assets will be suppressed,
* returning null if the external asset file does not contain this asset
* @return An InputStream returned by {@link AssetManager#open(String)}, or null if no asset could be opened
*/
public InputStream openAsset(String path, boolean allowDefault) {
// First try external assets, if available
try {
if (externalAssets != null) return externalAssets.open(path);
} catch (Exception e) {} // ignore
if (!allowDefault) {
Log.e("PresetIconManager", "Failed to load preset-specific asset " + path +
"[externalAssetPackage="+externalAssetPackage+"]");
return null;
}
// then external default assets
try {
if (externalDefaultAssets != null) return externalDefaultAssets.open(path);
} catch (Exception e) {} // ignore
// and finally built-in assets
try {
return internalAssets.open(path);
} catch (Exception e) {} // ignore
// if everything fails
Log.e("PresetIconManager", "Could not load asset " + path + " from any source "+
"[externalAssetPackage="+externalAssetPackage+"]");
return null;
}
}