/* * Copyright (C) 2005-2009 Team XBMC * http://xbmc.org * * This Program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This Program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with XBMC Remote; see the file license. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * http://www.gnu.org/copyleft/gpl.html * */ package org.xbmc.httpapi.client; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import org.xbmc.android.util.Base64; import org.xbmc.android.util.ClientFactory; import org.xbmc.android.util.ImportUtilities; import org.xbmc.api.business.INotifiableManager; import org.xbmc.api.object.ICoverArt; import org.xbmc.api.type.ThumbSize; import org.xbmc.api.type.ThumbSize.Dimension; import org.xbmc.httpapi.Connection; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Bitmap.CompressFormat; import android.util.Log; /** * Abstract super class of all (media) clients. * * @author Team XBMC */ abstract class Client { public static final String TAG = "Client-HTTPAPI"; protected final Connection mConnection; /** * Class constructor needs reference to HTTP client connection * @param connection */ Client(Connection connection) { mConnection = connection; } /** * Downloads a cover. * * Since base64-download is a REAL pita, we'll check for XBMC revision and * dispatch to direct download via the /thumbs accessor if possible. * * @param manager Postback manager * @param cover Cover object * @param size Minmal size to pre-resize to. * @param url URL to primary cover * @param fallbackUrl URL to fallback cover * @return Bitmap */ protected Bitmap getCover(INotifiableManager manager, ICoverArt cover, int size, String url, String fallbackUrl) { try { if (ClientFactory.XBMC_REV >= ClientFactory.MICROHTTPD_REV) { return getCoverFromMicroHTTPd(manager, cover, size, url, fallbackUrl); } else { return getCoverFromLibGoAhead(manager, cover, size, url, fallbackUrl); } } catch (OutOfMemoryError e) { manager.onError(new Exception("Out of memory. We're aware of this problem and we're working on it. Restarting the app should help.")); return null; } } /** * Downloads a cover "the old way", meaning the base64-encoded result is * stored in a String and decoded from there. I've tried using a * Base64.InputStream directly with libgoahead crashing as a result. * * @param manager Postback manager * @param cover Cover object * @param size Minmal size to pre-resize to. * @param url URL to primary cover * @param fallbackUrl URL to fallback cover * @return Bitmap */ private Bitmap getCoverFromLibGoAhead(INotifiableManager manager, ICoverArt cover, int size, String url, String fallbackUrl) { final int mediaType = cover.getMediaType(); // don't fetch small sizes size = size < ThumbSize.BIG ? ThumbSize.MEDIUM : ThumbSize.BIG; try { String b64enc = mConnection.query("FileDownload", url, manager); if (b64enc.length() <= 0) { if (fallbackUrl != null) { Log.i(TAG, "*** Downloaded cover has size null, retrying with fallback:"); b64enc = mConnection.query("FileDownload", fallbackUrl, manager); } else { b64enc = null; } } if (b64enc != null) { byte[] bytes = Base64.decode(b64enc); if (bytes.length > 0) { final BitmapFactory.Options opts = prefetch(manager, bytes, size); final Dimension dim = ThumbSize.getTargetDimension(size, mediaType, opts.outWidth, opts.outHeight); final int ss = ImportUtilities.calculateSampleSize(opts, dim); opts.inDither = true; opts.inSampleSize = ss; opts.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts); if (ss == 1) { bitmap = blowup(bitmap); } return bitmap; } } } catch (IOException e) { e.printStackTrace(); } return null; } /** * Downloads a cover using microhttpd. * * First, only boundaries are downloaded in order to determine the sample * size. Setting sample size > 1 will do two things: * <ol><li>Only a fragment of the total size will be downloaded</li> * <li>Resizing will be smooth and not pixelated as before</li></ol> * There is no base64-decoding since we're accessing the /thumb accessor * directly. The returned size is the next bigger (but smaller than the * double) size of the original image. * * @param manager Postback manager * @param cover Cover object * @param size Minmal size to pre-resize to. * @param url URL to primary cover * @param fallbackUrl URL to fallback cover * @return Bitmap */ private Bitmap getCoverFromMicroHTTPd(INotifiableManager manager, ICoverArt cover, int size, String url, String fallbackUrl) { final int mediaType = cover.getMediaType(); // don't fetch small sizes size = size < ThumbSize.BIG ? ThumbSize.MEDIUM : ThumbSize.BIG; InputStream is = null; try { Log.i(TAG, "Starting download (" + url + ") - microhttpd"); BitmapFactory.Options opts = prefetch(manager, url, size); Dimension dim = ThumbSize.getTargetDimension(size, mediaType, opts.outWidth, opts.outHeight); Log.i(TAG, "Pre-fetch: " + opts.outWidth + "x" + opts.outHeight + " => " + dim); if (opts.outWidth < 1) { if (fallbackUrl != null) { Log.i(TAG, "Starting fallback download (" + fallbackUrl + ")"); opts = prefetch(manager, fallbackUrl, size); dim = ThumbSize.getTargetDimension(size, mediaType, opts.outWidth, opts.outHeight); Log.i(TAG, "FALLBACK-Pre-fetch: " + opts.outWidth + "x" + opts.outHeight + " => " + dim); if (opts.outWidth < 1) { return null; } else { url = fallbackUrl; } } else { Log.i(TAG, "Fallback url is null, returning null-bitmap"); return null; } } final int ss = ImportUtilities.calculateSampleSize(opts, dim); Log.i(TAG, "Sample size: " + ss); is = new BufferedInputStream(mConnection.getThumbInputStreamForMicroHTTPd(url, manager), 8192); opts.inDither = true; opts.inSampleSize = ss; opts.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts); if (ss == 1) { bitmap = blowup(bitmap); } is.close(); if (bitmap == null) { Log.i(TAG, "Fetch: Bitmap is null!!"); return null; } else { Log.i(TAG, "Fetch: Bitmap: " + bitmap.getWidth() + "x" + bitmap.getHeight()); return bitmap; } } catch (FileNotFoundException e) { Log.i(TAG, "Fetch: Bitmap not found"); } catch (IOException e) { manager.onError(e); e.printStackTrace(); } finally { try { if (is != null) { is.close(); } } catch (IOException e) { } } return null; } /** * Only downloads as much as boundaries of the image in order to find out * its size. * @param manager Postback manager * @param url URL to primary cover * @param size Minmal size to pre-resize to. * @return */ private BitmapFactory.Options prefetch(INotifiableManager manager, String url, int size) { BitmapFactory.Options opts = new BitmapFactory.Options(); try { InputStream is = new BufferedInputStream(mConnection.getThumbInputStreamForMicroHTTPd(url, manager), 8192); opts.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, opts); } catch (FileNotFoundException e) { return opts; } return opts; } /** * Only decodes as much as boundaries of the image in order to find out * its size. * @param manager Postback manager * @param data Image data as byte array * @param size Minmal size to pre-resize to. * @return */ private BitmapFactory.Options prefetch(INotifiableManager manager, byte[] data, int size) { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, opts); return opts; } /** * Doubles the size of a bitmap and re-reads it with samplesize 2. I've * found no other way to smoothely resize images with samplesize = 1. * @param source * @return */ private Bitmap blowup(Bitmap source) { if (source != null) { Bitmap big = Bitmap.createScaledBitmap(source, source.getWidth() * 2, source.getHeight() * 2, true); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 2; ByteArrayOutputStream os = new ByteArrayOutputStream(); big.compress(CompressFormat.PNG, 100, os); byte[] array = os.toByteArray(); return BitmapFactory.decodeByteArray(array, 0, array.length, opts); } return null; } }