/*
* 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.jsonrpc.client;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.Iterator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;
import org.xbmc.android.util.ImportUtilities;
import org.xbmc.api.business.INotifiableManager;
import org.xbmc.api.object.ICoverArt;
import org.xbmc.api.type.SortType;
import org.xbmc.api.type.ThumbSize;
import org.xbmc.api.type.ThumbSize.Dimension;
import org.xbmc.jsonrpc.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
*/
public abstract class Client {
public static final String TAG = "Client-JSON-RPC";
public static final String PARAM_FIELDS = "fields";
public static final String PARAM_PROPERTIES = "properties";
public static final String PARAM_SORT = "sort";
public final static ObjectMapper MAPPER = new ObjectMapper();
public final static JsonNodeFactory FACTORY = JsonNodeFactory.instance;
protected final Connection mConnection;
/**
* Class constructor needs reference to HTTP client connection
* @param connection
*/
Client(Connection connection) {
mConnection = connection;
}
public int getActivePlayerId(INotifiableManager manager){
final JsonNode active = mConnection.getJson(manager, "Player.GetActivePlayers", null).get(0);
if(active == null)
return -1;
else
return getInt(active, "playerid");
}
/**
* Downloads a cover.
*
* 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>
* 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
*/
protected Bitmap getCover(INotifiableManager manager, ICoverArt cover, int size, String url) {
if(url == null)
return null;
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 + ")");
BitmapFactory.Options opts = prefetch(manager, url, size, mediaType);
Dimension dim = ThumbSize.getTargetDimension(size, mediaType, opts.outWidth, opts.outHeight);
Log.i(TAG, "Pre-fetch: " + opts.outWidth + "x" + opts.outHeight + " => " + dim);
if (opts.outWidth < 0) {
return null;
}
final int ss = ImportUtilities.calculateSampleSize(opts, dim);
Log.i(TAG, "Sample size: " + ss);
is = new BufferedInputStream(mConnection.getThumbInputStream(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) {
return null;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) { }
}
return null;
}
private BitmapFactory.Options prefetch(INotifiableManager manager, String url, int size, int mediaType) {
BitmapFactory.Options opts = new BitmapFactory.Options();
try {
InputStream is = new BufferedInputStream(mConnection.getThumbInputStream(url, manager), 8192);
opts.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, opts);
} catch (FileNotFoundException e) {
return 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;
}
/**
* Returns an SQL String of given sort options of albums query
* @param sortBy Sort field
* @param sortOrder Sort order
* @return SQL "ORDER BY" string
*/
protected static ObjNode sort(ObjNode params, int sortBy, String sortOrder) {
final String order = sortOrder.equals(SortType.ORDER_DESC) ? "descending" : "ascending";
final String sortby;
switch (sortBy) {
default:
case SortType.ALBUM:
sortby = "label";
break;
case SortType.ARTIST:
sortby = "artist";
break;
case SortType.TRACK:
sortby = "track";
break;
case SortType.TITLE:
sortby = "sorttitle";
break;
case SortType.YEAR:
sortby = "year";
break;
case SortType.RATING:
sortby = "rating";
break;
case SortType.DATE_ADDED:
sortby = "dateadded";
break;
}
params.p(PARAM_SORT, obj().p("ignorearticle", true).p("method", sortby).p("order", order));
return params;
}
public final static ObjNode obj() {
return new ObjNode(FACTORY);
}
public static class ObjNode extends ObjectNode {
public ObjNode(JsonNodeFactory nc) {
super(nc);
}
public ObjNode p(String fieldName, Object object) {
super.put(fieldName, (JsonNode) object);
return this;
}
public ObjNode p(String fieldName, String v) {
super.put(fieldName, v);
return this;
}
public ObjNode p(String fieldName, int v) {
super.put(fieldName, v);
return this;
}
public ObjNode p(String fieldName, boolean v) {
super.put(fieldName, v);
return this;
}
};
public final static ArrayNode arr() {
return MAPPER.createArrayNode();
}
public final static String getString(JsonNode obj, String key) {
if(obj.get(key) == null)
return "";
else if(obj.get(key).isArray()){
String retval = "";
for (Iterator<JsonNode> i = obj.get(key).getElements(); i.hasNext();) {
retval += i.next().getTextValue();
if(i.hasNext())
retval += ", ";
}
return retval;
}
else
return getString(obj, key, "");
}
public final static String getString(JsonNode obj, String key, String ifNullResult) {
return obj.get(key) == null ? ifNullResult : obj.get(key).getTextValue();
}
public final static int getInt(JsonNode obj, String key) {
return obj.get(key) == null ? -1 : obj.get(key).getIntValue();
}
public final static double getDouble(JsonNode obj, String key) {
if(obj.get(key) == null)
return -1;
DecimalFormat twoDForm = new DecimalFormat("#.0");
double val = -1;
try{
val = Double.valueOf(twoDForm.format(obj.get(key).getDoubleValue()).replace(',', '.'));
}
catch(NumberFormatException e){
val = -1;
}
return val;
}
}