All Downloads are FREE. Search and download functionalities are using the official Maven repository.

playn.android.AndroidAssets Maven / Gradle / Ivy

/**
 * Copyright 2011 The PlayN Authors
 *
 * 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 playn.android;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;

import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;

import playn.core.*;

public class AndroidAssets extends Assets {

  /** Extends {@link BitmapFactory.Options} with PlayN asset scale configuration. */
  public class BitmapOptions extends BitmapFactory.Options {
    /** The asset scale for the resulting image. This will be populated with the appropriate scale
     * prior to the call to {@link BitmapOptionsAdjuster#adjustOptions} and can be adjusted by the
     * adjuster if it, for example, adjusts {@link BitmapFactory.Options#inSampleSize} to
     * downsample the image. */
    public Scale scale;
  }

  /** See {@link #setBitmapOptionsAdjuster}. */
  public interface BitmapOptionsAdjuster {
    /** Adjusts the {@link BitmapOptions} based on the app's special requirements.
     * @param path the path passed to {@link #getImage} or URL passed to {@link #getRemoteImage}.*/
    void adjustOptions(String path, BitmapOptions options);
  }

  /** Configures from where our assets are loaded. By default assets are loaded via the Android
    * {@link AssetManager}, but this can be used to load assets from an APK expansion file if
    * desired.
    */
  public interface AssetSource {
    /** Opens an input stream to the asset at {@code assetPath}.
      * The path will already have the path prefix prepended. */
    public InputStream openStream (String assetPath) throws IOException;

    /** Obtains a file descriptor for the asset at {@code assetPath}.
      * The path will already have the path prefix prepended. */
    public AssetFileDescriptor getFileDescriptor (String assetPath) throws IOException;
  }

  private final AndroidPlatform plat;
  private final AssetManager assetMgr;
  private String pathPrefix = ""; // 'assets/' is always prepended by AssetManager
  private Scale assetScale = null;

  private BitmapOptionsAdjuster optionsAdjuster = new BitmapOptionsAdjuster() {
    public void adjustOptions(String path, BitmapOptions options) {} // noop!
  };

  private AssetSource assetSource;

  public AndroidAssets (AndroidPlatform plat) {
    super(plat.exec());
    this.plat = plat;
    this.assetMgr = plat.activity.getResources().getAssets();

    assetSource = new AssetSource() {
      public InputStream openStream (String assetPath) throws IOException {
        return assetMgr.open(assetPath, AssetManager.ACCESS_STREAMING);
      }
      public AssetFileDescriptor getFileDescriptor (String assetPath) throws IOException {
        return assetMgr.openFd(assetPath);
      }
    };
  }

  /**
   * Configures a prefix which is prepended to all asset paths prior to loading. Android assets are
   * normally loaded via the Android AssetManager, which expects all assets to be in the {@code
   * assets} directory. This prefix is thus inserted between {@code assets} and the path supplied
   * to any of the various {@code get} methods.
   */
  public void setPathPrefix(String prefix) {
    if (prefix.startsWith("/") || prefix.endsWith("/")) {
      throw new IllegalArgumentException("Prefix must not start or end with '/'.");
    }
    pathPrefix = (prefix.length() == 0) ? prefix : (prefix + "/");
  }

  /**
   * Configures the default scale to use for assets. This allows one to use higher resolution
   * imagery than the device might normally. For example, one can supply scale 2 here, and
   * configure the graphics scale to 1.25 in order to use iOS Retina graphics (640x960) on a WXGA
   * (480x800) device.
   */
  public void setAssetScale(float scaleFactor) {
    assert scaleFactor > 0;
    this.assetScale = new Scale(scaleFactor);
  }

  /**
   * Configures a class that will adjust the {@link BitmapOptions} used to decode loaded bitmaps.
   * An app may wish to use different bitmap configs for different images (say {@code RGB_565} for
   * its non-transparent images) or adjust the dithering settings.
   */
  public void setBitmapOptionsAdjuster(BitmapOptionsAdjuster optionsAdjuster) {
    assert optionsAdjuster != null;
    this.optionsAdjuster = optionsAdjuster;
  }

  /**
   * Configures the place from which our assets are loaded. By default assets are loaded via
   * {@link AssetManager}, but a custom source can be used to, for example, load assets from an
   * expansion APK.
   */
  public void setAssetSource (AssetSource source) {
    assert source != null;
    this.assetSource = source;
  }

  /**
   * Loads a typeface from {@code path}. This can then be registered via
   * {@link AndroidGraphics#registerFont}.
   */
  public Typeface getTypeface(String path) {
    return Typeface.createFromAsset(assetMgr, normalizePath(pathPrefix + path));
  }

  @Override public Image getRemoteImage(final String url, int width, int height) {
    final ImageImpl image = createImage(true, width, height, url);
    exec.invokeAsync(new Runnable() {
      public void run () {
        try {
          BitmapOptions options = createOptions(url, false, Scale.ONE);
          Bitmap bmp = downloadBitmap(url, options);
          image.succeed(new ImageImpl.Data(options.scale, bmp, bmp.getWidth(), bmp.getHeight()));
        } catch (Throwable error) {
          image.fail(error);
        }
      }
    });
    return image;
  }

  @Override
  public Sound getSound(String path) {
    return plat.audio().createSound(path + ".mp3");
  }

  @Override
  public Sound getMusic(String path) {
    return plat.audio().createMusic(path + ".mp3");
  }

  @Override
  public String getTextSync(String path) throws Exception {
    InputStream is = openAsset(path);
    try {
      StringBuilder fileData = new StringBuilder(1000);
      BufferedReader reader = new BufferedReader(new InputStreamReader(is));
      char[] buf = new char[1024];
      int numRead = 0;
      while ((numRead = reader.read(buf)) != -1) {
        String readData = String.valueOf(buf, 0, numRead);
        fileData.append(readData);
      }
      reader.close();
      return fileData.toString();
    } finally {
      is.close();
    }
  }

  @Override
  public ByteBuffer getBytesSync(String path) throws Exception {
    InputStream is = openAsset(path);
    try {
      int size = is.available();
      byte[] data = new byte[size];
      is.read(data);
      return ByteBuffer.wrap(data);
    } finally {
      is.close();
    }
  }

  @Override protected ImageImpl createImage(boolean async, int rwid, int rhei, String source) {
    return new AndroidImage(plat, async, rwid, rhei, source);
  }

  @Override protected ImageImpl.Data load (String path) throws Exception {
    Exception error = null;
    for (Scale.ScaledResource rsrc : assetScale().getScaledResources(path)) {
      try {
        InputStream is = openAsset(rsrc.path);
        try {
          BitmapOptions options = createOptions(path, true, rsrc.scale);
          Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
          return new ImageImpl.Data(options.scale, bitmap, bitmap.getWidth(), bitmap.getHeight());
        } finally {
          is.close();
        }
      } catch (FileNotFoundException fnfe) {
        error = fnfe; // keep going, checking for lower resolution images
      } catch (Exception e) {
        error = e;
        break; // the image was broken not missing, stop here
      }
    }
    plat.reportError("Could not load image: " + pathPrefix + path, error);
    throw error != null ? error : new FileNotFoundException(path);
  }

  AssetFileDescriptor openAssetFd(String path) throws IOException {
    return assetSource.getFileDescriptor(normalizePath(pathPrefix + path));
  }

  /**
   * Attempts to open the asset with the given name, throwing an {@link IOException} in case of
   * failure.
   */
  InputStream openAsset(String path) throws IOException {
    String fullPath = normalizePath(pathPrefix + path);
    InputStream is = assetSource.openStream(fullPath);
    if (is == null) throw new FileNotFoundException("Missing resource: " + fullPath);
    return is;
  }

  protected Scale assetScale () {
    return (assetScale != null) ? assetScale : plat.graphics().scale();
  }

  protected BitmapOptions createOptions(String path, boolean purgeable, Scale scale) {
    BitmapOptions options = new BitmapOptions();
    options.inScaled = false; // don't scale bitmaps based on device parameters
    options.inDither = true;
    options.inPreferredConfig = plat.graphics().preferredBitmapConfig;
    options.inPurgeable = purgeable;
    options.inInputShareable = true;
    options.scale = scale;
    // give the game an opportunity to customize the bitmap options based on the image path
    optionsAdjuster.adjustOptions(path, options);
    return options;
  }

  // Taken from
  // http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html
  protected Bitmap downloadBitmap(String url, BitmapOptions options) throws Exception {
    AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
    HttpGet getRequest = new HttpGet(url);

    try {
      HttpResponse response = client.execute(getRequest);
      int statusCode = response.getStatusLine().getStatusCode();
      if (statusCode != HttpStatus.SC_OK) {
        // we could check the status, but we'll just assume that if there's a Location header,
        // we should follow it
        Header[] headers = response.getHeaders("Location");
        if (headers != null && headers.length > 0) {
          return downloadBitmap(headers[headers.length-1].getValue(), options);
        }
        throw new Exception("Error " + statusCode + " while retrieving bitmap from " + url);
      }

      HttpEntity entity = response.getEntity();
      if (entity == null)
        throw new Exception("Error: getEntity returned null for " + url);

      InputStream inputStream = null;
      try {
        inputStream = entity.getContent();
        return BitmapFactory.decodeStream(inputStream, null, options);
      } finally {
        if (inputStream != null)
          inputStream.close();
        entity.consumeContent();
      }

    } catch (Exception e) {
      // Could provide a more explicit error message for IOException or IllegalStateException
      getRequest.abort();
      plat.reportError("Error while retrieving bitmap from " + url, e);
      throw e;

    } finally {
      client.close();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy