/*
* Copyright (C) 2011 Virginia Tech Department of Computer Science
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sofia.internal;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.DisplayMetrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Map;
/**
* <p>
* Since we want to distribute Sofia as a single JAR (or a set of JARs), we
* cannot make use of the standard Android resource structure that regular
* APKs use, because all of the resources would have to be copied into the
* application projects.
* </p><p>
* Instead, for things like images, we store them embedded in the JARs, and
* this class provides a better interface for accessing them.
* </p>
*
* @author Tony Allevato
*/
public class JarResources
{
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* Get an image resource by name, taking the current device's DPI into
* account. The image may be a traditional Android resource, in which
* case the normal resource mechanism is used to look it up, or it may
* be stored in one of the packages listed. Alternatively, the
* image may be stored in the application/context package. The package(s)
* specified should have a subpackage named "images", with one or more of
* its own subpackages that match the DPI identifiers used in resources:
* "ldpi", "mdpi", "hdpi", and/or "xhdpi". Image files are held in these
* dpi-based subpackages. The "images" subpackage itself will also be
* searched, with images it contains treated at the same DPI as the device.
*
* @param context The context for determining the display resolution,
* and also the application's package, if the image isn't
* found in any of the specified package(s).
* @param name The name of the image file, including its extension.
* @param packageNames The packages where the images are located
* (can be omitted, to only search the application package).
* @return A {@code Bitmap} containing the image, or null if no
* image could be found.
*/
public static Bitmap getBitmap(
Context context, String name, String ... packageNames)
{
return getBitmap(context, name, true, true, packageNames);
}
// ----------------------------------------------------------
/**
* Get an image resource by name, taking the current device's DPI into
* account. The image may be a traditional Android resource, in which
* case the normal resource mechanism is used to look it up, or it may
* be stored in one of the packages listed. Alternatively, the
* image may be stored in the application/context package. The package(s)
* specified should have a subpackage named "images", with one or more of
* its own subpackages that match the DPI identifiers used in resources:
* "ldpi", "mdpi", "hdpi", and/or "xhdpi". Image files are held in these
* dpi-based subpackages. The "images" subpackage itself will also be
* searched, with images it contains treated at the same DPI as the device.
*
* @param context The context for determining the display resolution,
* and also the application's package, if needed.
* @param name The name of the image file, optionally including its
* extension.
* @param searchAppPkg If true, and no image is found with respect to
* the specified packages (if any), the search will
* also look in the application package determined by the
* context. If false, the application package will not
* be searched.
* @param scaleForDpi If true, the loaded image will be automatically
* upscaled or downscaled for the current device display
* density by the BitmapFactory. If false, the image will
* be loaded at its stored resolution regardless of the
* current display density.
* @param packageNames The packages where the images are located
* (can be omitted, to only search the application package).
* @return A {@code Bitmap} containing the image, or null if no
* image could be found.
*/
public static Bitmap getBitmap(Context context, String name,
boolean searchAppPkg, boolean scaleForDpi, String ... packageNames)
{
Bitmap result = getBitmapFromResource(context, name, scaleForDpi);
if (result != null)
{
return result;
}
for (String pkgName : packageNames)
{
result = getBitmapFromClasspath(
context, name, pkgName, scaleForDpi);
if (result != null)
{
return result;
}
}
if (searchAppPkg)
{
result = getBitmapFromClasspath(
context, name, context.getPackageName(), scaleForDpi);
}
return result;
}
// ----------------------------------------------------------
/**
* Get an image resource by name, taking the current device's DPI into
* account. The image must be a traditional Android resource, but it
* will be looked up by name, instead of by id.
*
* @param context The context for determining the display resolution.
* If null, mdpi device resolution will be assumed.
* @param name The name of the image file, optionally including its
* extension.
* @param scaleForDpi If true, the loaded image will be automatically
* upscaled or downscaled for the current device display
* density by the BitmapFactory. If false, the image will
* be loaded at its stored resolution regardless of the
* current display density.
* @return A {@code Bitmap} containing the image, or null if no
* image could be found.
*/
public static Bitmap getBitmapFromResource(
Context context, String name, boolean scaleForDpi)
{
// trim file extension, if present
int pos = name.lastIndexOf('.');
if (pos >= 0)
{
name = name.substring(0, pos);
}
//log.debug("looking for resource named {}", name);
// Look for cached bitmap first
Bitmap result = null;
{
WeakReference<Bitmap> ref = RESOURCE_CACHE.get(name);
if (ref != null)
{
result = ref.get();
if (result != null)
{
//log.debug("found cached resource for {}", name);
return result;
}
}
}
BitmapFactory.Options bfo = null;
if (!scaleForDpi)
{
bfo = new BitmapFactory.Options();
bfo.inScaled = false;
}
// First, try for a resource by this name:
int id = context.getResources().getIdentifier(
name, "drawable", context.getPackageName());
if (id != 0)
{
result = BitmapFactory.decodeResource(
context.getResources(), id, bfo);
}
if (result != null)
{
//log.debug("caching resource id {} for {}", id, name);
RESOURCE_CACHE.put(name, new WeakReference<Bitmap>(result));
}
else
{
//log.debug("cannot find resource {}", name);
}
return result;
}
// ----------------------------------------------------------
/**
* Get an image resource by name, taking the current device's DPI into
* account. The should be stored in the named package, which should
* have a subpackage named "images", with one or more of its own
* subpackages that match the DPI identifiers used in resources:
* "ldpi", "mdpi", "hdpi", and/or "xhdpi". Image files are held in these
* dpi-based subpackages. The "images" subpackage itself will also be
* searched, with images it contains treated at the same DPI as the device.
*
* @param context The context for determining the display resolution.
* If null, mdpi device resolution will be assumed.
* @param name The name of the image file, optionally including its
* extension.
* @param pkgName The name of the package where the images are
* located (i.e., the package containing "images/", not
* including the ".images" at the end of the package
* name).
* @param scaleForDpi If true, the loaded image will be automatically
* upscaled or downscaled for the current device display
* density by the BitmapFactory. If false, the image will
* be loaded at its stored resolution regardless of the
* current display density.
* @return A {@code Bitmap} containing the image, or null if no
* image could be found.
*/
public static Bitmap getBitmapFromClasspath(
Context context, String name, String pkgName, boolean scaleForDpi)
{
if (pkgName == null)
{
pkgName = "";
}
//log.debug("looking for image named {} in '{}'", name, pkgName);
// Look for cached bitmap first
Bitmap result = null;
Map<String, WeakReference<Bitmap>> pkgMap =
CLASSPATH_CACHE.get(pkgName);
if (pkgMap == null)
{
//log.debug("no package cache for '{}'", pkgName);
pkgMap = new java.util.TreeMap<String, WeakReference<Bitmap>>();
CLASSPATH_CACHE.put(pkgName, pkgMap);
}
else
{
WeakReference<Bitmap> ref = pkgMap.get(name);
if (ref != null)
{
result = ref.get();
if (result != null)
{
//log.debug("found cached image for {} in '{}'",
// name, pkgName);
return result;
}
}
}
boolean hasExtension = (name.lastIndexOf('.') >= 0);
int foundDensity = -1; // Density of device
int pattern = 0; // search pattern, index in SEARCH_PATTERN
if (context != null)
{
// If no resource was found ...
DisplayMetrics metrics =
context.getResources().getDisplayMetrics();
// Pattern is declared outside the loop because it is intended to
// be used after the loop
for (; pattern < CUTOFF.length; pattern++)
{
if (metrics.densityDpi < CUTOFF[pattern])
{
break;
}
}
// pattern now contains the search pattern, 0-3. If no pattern
// was found in CUTOFF, pattern == CUTOFF.length == 3, which
// defaults to the xhdpi pattern.
}
else
{
// Default if no metrics found is to search from highest
// resolution to lowest, and scale image down if necessary.
pattern = XHDPI;
}
InputStream stream = null;
// OK, now search using the specified package
String base = "";
if (pkgName.length() > 0)
{
base = pkgName.replace('.', '/') + "/";
}
base += "images/";
ClassLoader loader = JarResources.class.getClassLoader();
for (int attempt : SEARCH_PATTERN[pattern])
{
String dir = base + DENSITY_NAME[attempt] + "/";
if (hasExtension)
{
stream = loader.getResourceAsStream(dir + name);
}
else
{
for (String extension : EXTENSIONS)
{
stream =
loader.getResourceAsStream(dir + name + extension);
if (stream != null)
{
break;
}
}
}
if (stream != null)
{
foundDensity = attempt;
break;
}
}
if (stream == null)
{
// If we make it here, try for the default (no density) name
if (hasExtension)
{
stream = loader.getResourceAsStream(base + name);
}
else
{
for (String extension : EXTENSIONS)
{
stream =
loader.getResourceAsStream(base + name + extension);
if (stream != null)
{
break;
}
}
}
}
if (stream != null)
{
BitmapFactory.Options bfo = null;
if (foundDensity >= 0 && scaleForDpi)
{
bfo = new BitmapFactory.Options();
bfo.inDensity = DENSITY[foundDensity];
}
else if (!scaleForDpi)
{
bfo = new BitmapFactory.Options();
bfo.inScaled = false;
}
result = BitmapFactory.decodeStream(stream, null, bfo);
}
if (result != null)
{
//log.debug("caching image {} for package '{}'", name, pkgName);
pkgMap.put(name, new WeakReference<Bitmap>(result));
}
else
{
//log.debug("cannot find image {} in '{}'", name, pkgName);
}
return result;
}
//~ Fields ................................................................
private static final Logger log = LoggerFactory.getLogger(
JarResources.class);
// Map from ints 0-3 to corresponding density names here
private static final String[] DENSITY_NAME = {
"ldpi",
"mdpi",
"hdpi",
"xhdpi"
};
// constants for the int codes
private static final int LDPI = 0;
private static final int MDPI = 1;
private static final int HDPI = 2;
private static final int XHDPI = 3;
// DPI density for each name
private static final int[] DENSITY = {
120,
160,
240,
320
};
// See http://developer.android.com/guide/practices/screens_support.html
// Values based on info in Table 1 on that page.
private static final int[] CUTOFF = {
140, // upper limit for ldpi, which is ~120dpi
200, // upper limit for mdpi, which is ~160dpi
280 // upper limit for hdpi, which is ~240dpi
// 400 is upper limit for xhdpi, which is ~320dpi
// don't worry about xxhigh, since xhdpi should scale well
};
// Search starts with the preferred resolution, and then goes high-to-low
// on the assumption that higher res images scale down better than
// attempting to scale up lower res images.
private static final int[][] SEARCH_PATTERN = {
{ LDPI, XHDPI, HDPI, MDPI }, // for ldpi screens
{ MDPI, XHDPI, HDPI, LDPI }, // for mdpi screens
{ HDPI, XHDPI, MDPI, LDPI }, // for hdpi screens
{ XHDPI, HDPI, MDPI, LDPI } // for xhdpi screens
};
private static final String[] EXTENSIONS = {
".png", ".PNG", ".gif", ".GIF", ".jpg", ".JPG", ".JPEG", ".JPEG"
};
// These maps use weak references so the bitmaps can be reclaimed once
// nothing refers to them. We don't worry about the value-less entries
// that remain in these maps, since the expected number of keys is
// only a handful anyway.
private static final Map<String, WeakReference<Bitmap>> RESOURCE_CACHE =
java.util.Collections.synchronizedMap(
new java.util.TreeMap<String, WeakReference<Bitmap>>());
private static final Map<String, Map<String, WeakReference<Bitmap>>>
CLASSPATH_CACHE =
java.util.Collections.synchronizedMap(
new java.util.TreeMap<String, Map<String, WeakReference<Bitmap>>>()
);
}