package org.xbmc.android.remote.business.cm;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import org.xbmc.android.jsonrpc.api.AbstractCall;
import org.xbmc.android.jsonrpc.api.call.Player;
import org.xbmc.android.jsonrpc.api.call.Playlist;
import org.xbmc.android.jsonrpc.api.model.ListModel;
import org.xbmc.android.jsonrpc.api.model.ListModel.Sort;
import org.xbmc.android.jsonrpc.api.model.PlayerModel;
import org.xbmc.android.jsonrpc.io.ApiCallback;
import org.xbmc.android.jsonrpc.io.ConnectionManager;
import org.xbmc.android.remote.business.AbstractThread;
import org.xbmc.android.remote.business.Command;
import org.xbmc.android.remote.business.DiskCacheThread;
import org.xbmc.android.remote.business.DownloadThread;
import org.xbmc.android.remote.business.MemCacheThread;
import org.xbmc.android.util.Crc32;
import org.xbmc.android.util.HostFactory;
import org.xbmc.android.util.ImportUtilities;
import org.xbmc.api.business.DataResponse;
import org.xbmc.api.business.INotifiableManager;
import org.xbmc.api.object.ICoverArt;
import org.xbmc.api.presentation.INotifiableController;
import org.xbmc.api.type.CacheType;
import org.xbmc.api.type.SortType;
import org.xbmc.api.type.ThumbSize;
import org.xbmc.api.type.ThumbSize.Dimension;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.util.Log;
public class AbstractManager implements INotifiableManager {
public static final String EMPTY_PLAYLIST_ITEM = "[Empty]";
public static final Integer PLAYLIST_MUSIC = 0;
public static final Integer PLAYLIST_VIDEO = 1;
public static final Integer PLAYLIST_PICTURE = 2;
public static final Boolean DEBUG = false;
protected static final String TAG = "cm.AbstractManager";
public static final String PREF_SORT_BY_PREFIX = "sort_by_";
public static final String PREF_SORT_ORDER_PREFIX = "sort_order_";
private static ConnectionManager connectionManager;
protected INotifiableController mController;
protected SharedPreferences mPref;
protected int mCurrentSortKey;
protected boolean mCurrentIgnoreArticle;
protected List<Runnable> failedRequests = new ArrayList<Runnable>();
public void setController(INotifiableController controller) {
this.mController = controller;
}
protected ConnectionManager getConnectionManager(Context context) {
if (connectionManager == null) {
connectionManager = new ConnectionManager(context,
HostFactory.host.toHostConfig());
// Why did we have this?
//connectionManager.setPreferHTTP();
}
return connectionManager;
}
public static void resetClient() {
if(connectionManager == null) {
return;
}
connectionManager.disconnect();
connectionManager = null;
}
protected abstract static class ApiHandler<T, S> {
public abstract T handleResponse(AbstractCall<S> apiCall);
}
protected <T, S> void call(AbstractCall<S> call,
final ApiHandler<T, S> handler, final DataResponse<T> response,
Context context) {
getConnectionManager(context).call(call, new ApiCallback<S>() {
public void onResponse(AbstractCall<S> apiCall) {
response.value = handler.handleResponse(apiCall);
AbstractManager.this.onFinish(response);
}
public void onError(int code, String message, String hint) {
AbstractManager.this.onError(new Exception(message));
}
});
}
protected <T, S> void callRaw(AbstractCall<S> call,
final ApiHandler<T, S> handler, Context context) {
getConnectionManager(context).call(call, new ApiCallback<S>() {
public void onResponse(AbstractCall<S> apiCall) {
handler.handleResponse(apiCall);
}
public void onError(int code, String message, String hint) {
AbstractManager.this.onError(new Exception(message));
}
});
}
public void postActivity() {
AbstractThread.quitThreads();
}
public void getCover(DataResponse<Bitmap> response, ICoverArt cover,
int thumbSize, Bitmap defaultCover, Context context,
boolean getFromCacheOnly) {
if (cover.getCrc() != 0L) {
// first, try mem cache (only if size = small, other sizes aren't
// mem-cached.
if (thumbSize == ThumbSize.SMALL || thumbSize == ThumbSize.MEDIUM) {
if (DEBUG)
Log.i(TAG,
"["
+ cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] Trying memory ("
+ Crc32.formatAsHexLowerCase(cover.getCrc())
+ ")");
getCoverFromMem(response, cover, thumbSize, defaultCover,
context, getFromCacheOnly);
} else {
if (getFromCacheOnly) {
Log.e(TAG,
"["
+ cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] ERROR: NOT downloading big covers is a bad idea because they are not cached!");
response.value = null;
onFinish(response);
} else {
if (DEBUG)
Log.i(TAG,
"[" + cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] Downloading directly");
getCoverFromNetwork(response, cover, thumbSize, context);
}
}
} else {
if (DEBUG)
Log.i(TAG, "[" + cover.getId() + ThumbSize.getDir(thumbSize)
+ "] no crc, skipping.");
response.value = null;
onFinish(response);
}
}
/**
* Tries to get small cover from memory, then from disk, then download it
* from XBMC.
*
* @param response
* Response object
* @param cover
* Get cover for this object
*/
protected void getCoverFromMem(final DataResponse<Bitmap> response,
final ICoverArt cover, final int thumbSize, Bitmap defaultCover,
final Context context, final boolean getFromCacheOnly) {
if (DEBUG)
Log.i(TAG, "[" + cover.getId() + "] Checking in mem cache..");
MemCacheThread.get().getCover(new DataResponse<Bitmap>() {
public void run() {
if (value == null) {
if (DEBUG)
Log.i(TAG,
"[" + cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] empty");
// then, try sdcard cache
getCoverFromDisk(response, cover, thumbSize, context,
getFromCacheOnly);
} else {
if (DEBUG)
Log.i(TAG,
"[" + cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] FOUND in memory!");
response.value = value;
response.cacheType = CacheType.MEMORY;
onFinish(response);
}
}
}, cover, thumbSize, mController, defaultCover);
}
/**
* Tries to get cover from disk, then download it from XBMC.
*
* @param response
* Response object
* @param cover
* Get cover for this object
* @param thumbSize
* Cover size
*/
protected void getCoverFromDisk(final DataResponse<Bitmap> response,
final ICoverArt cover, final int thumbSize, final Context context,
final boolean getFromCacheOnly) {
if (DEBUG)
Log.i(TAG, "[" + cover.getId() + "] Checking in disk cache..");
DiskCacheThread.get().getCover(new DataResponse<Bitmap>() {
public void run() {
if (value == null) {
if (DEBUG)
Log.i(TAG,
"[" + cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] Disk cache empty.");
if (response.postCache()) {
// well, let's download
if (getFromCacheOnly) {
if (DEBUG)
Log.i(TAG,
"[" + cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] Skipping download.");
response.value = null;
onFinish(response);
} else {
getCoverFromNetwork(response, cover, thumbSize,
context);
}
}
} else {
if (DEBUG)
Log.i(TAG,
"[" + cover.getId()
+ ThumbSize.getDir(thumbSize)
+ "] FOUND on disk!");
response.value = value;
response.cacheType = CacheType.SDCARD;
onFinish(response);
}
}
}, cover, thumbSize, mController);
}
/**
* Last stop: try to download from XBMC.
*
* @param response
* Response object
* @param cover
* Get cover for this object
* @param thumbSize
* Cover size
*/
protected void getCoverFromNetwork(final DataResponse<Bitmap> response,
final ICoverArt cover, final int thumbSize, final Context context) {
if (DEBUG)
Log.i(TAG, "[" + cover.getId() + "] Downloading..");
DownloadThread.get().getCover(new DataResponse<Bitmap>() {
public void run() {
if (value == null) {
if (DEBUG)
Log.i(TAG, "[" + cover.getId() + "] Download empty");
} else {
if (DEBUG)
Log.i(TAG, "[" + cover.getId() + "] DOWNLOADED ("
+ value.getWidth() + "x" + value.getHeight()
+ ")!");
response.cacheType = CacheType.NETWORK;
response.value = value;
}
onFinish(response); // callback in any case, since we don't go
// further than that.
}
}, cover, thumbSize, mController, this, context);
}
public Bitmap getCoverSync(ICoverArt cover, int thumbSize) {
if(MemCacheThread.isInCache(cover, thumbSize))
return MemCacheThread.getCover(cover, thumbSize);
else if(DiskCacheThread.isInCache(cover, thumbSize))
return DiskCacheThread.getCover(cover, thumbSize);
else
return null;
}
public boolean coverLoaded(ICoverArt cover, int thumbSize) {
return (MemCacheThread.isInCache(cover, thumbSize) || DiskCacheThread.isInCache(cover, thumbSize));
}
public void onFinish(DataResponse<?> response) {
if (mController != null) {
mController.runOnUI(response);
}
}
public void onWrongConnectionState(int state, Command<?> cmd) {
failedRequests.add(cmd);
if (mController != null)
mController.onWrongConnectionState(state, this, cmd);
}
public void onError(Exception e) {
if (mController != null) {
mController.onError(e);
}
}
public void onMessage(String message) {
if (mController != null) {
mController.onMessage(message);
}
}
public void onMessage(int code, String message) {
onMessage(message);
}
public void retryAll() {
// TODO make this queue work
}
/**
* Sets the static reference to the preferences object. Used to obtain
* current sort values.
*
* @param pref
*/
public void setPreferences(SharedPreferences pref) {
mPref = pref;
}
/**
* Sets which kind of view is currently active.
*
* @param sortKey
*/
public void setSortKey(int sortKey) {
mCurrentSortKey = sortKey;
}
public void setIgnoreArticle(boolean ignoreArticle) {
// FIXME: make this configurable and respected
mCurrentIgnoreArticle = ignoreArticle;
}
/**
* Returns currently saved "sort by" value. If the preference was not set
* yet, or if the current sort key is not set, return default value.
*
* @param type
* Default value
* @return Sort by field
*/
protected String getSortBy(String type) {
if (mPref != null) {
int sort = mPref.getInt(PREF_SORT_BY_PREFIX + mCurrentSortKey, -1);
switch (sort) {
case SortType.ALBUM:
return "album";
case SortType.ARTIST:
return "artist";
case SortType.GENRE:
return "genre";
case SortType.FILENAME:
return "file";
case SortType.TITLE:
return "title";
case SortType.YEAR:
return "year";
case SortType.EPISODE_TITLE:
return "episode";
case SortType.PLAYCOUNT:
return "playcount";
case SortType.DATE_ADDED:
return "dateadded";
case SortType.LASTPLAYED:
return "lastplayed";
default:
return type;
}
}
return type;
}
/**
* Returns an object representing the current sort.
*
* @param type
* sorting field
* @return Sort
*/
public Sort getSort(String method) {
return new Sort(mCurrentIgnoreArticle, getSortBy(method),
getSortOrder());
}
/**
* Returns currently saved "sort by" value. If the preference was not set
* yet, or if the current sort key is not set, return "ASC".
*
* @return Sort order
*/
protected String getSortOrder() {
String order = SortType.ORDER_ASC;
if (mPref != null) {
order = mPref.getString(PREF_SORT_ORDER_PREFIX + mCurrentSortKey,
SortType.ORDER_ASC);
}
if (order.equals(SortType.ORDER_ASC)) {
return "ascending";
}
return "descending";
}
/**
* 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(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 (" + HostFactory.host.getVfsUrl(url) + ")");
BitmapFactory.Options opts = prefetch(HostFactory.host.getVfsUrl(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) {
if (fallbackUrl != null) {
Log.i(TAG, "Starting fallback download (" + fallbackUrl
+ ")");
opts = prefetch(fallbackUrl, size, mediaType);
dim = ThumbSize.getTargetDimension(size, mediaType,
opts.outWidth, opts.outHeight);
Log.i(TAG, "FALLBACK-Pre-fetch: " + opts.outWidth + "x"
+ opts.outHeight + " => " + dim);
if (opts.outWidth < 0) {
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(getInputStream(HostFactory.host.getVfsUrl(url)), 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 InputStream getInputStream(String downloadURI) throws IOException {
final URL u = new URL(downloadURI);
Log.i(TAG, "Returning input stream for " + u.toString());
URLConnection uc;
uc = u.openConnection();
uc.setConnectTimeout(5000);
uc.setReadTimeout(HostFactory.host.getTimeout());
return uc.getInputStream();
}
private BitmapFactory.Options prefetch(String url, int size, int mediaType) {
BitmapFactory.Options opts = new BitmapFactory.Options();
try {
InputStream is = new BufferedInputStream(getInputStream(url), 8192);
opts.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, opts);
} catch (IOException e) {
Log.e("Client", e.getMessage(), 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;
}
protected void setPlaylist(int playlistid, DataResponse<Boolean> response,
int position, Context context) {
call(new Player.GoTo(playlistid, position),
new ApiHandler<Boolean, String>() {
@Override
public Boolean handleResponse(AbstractCall<String> apiCall) {
return "OK".equals(apiCall.getResult());
}
}, response, context);
}
protected void removeFromPlaylist(int playlistid,
DataResponse<Boolean> response, int position, Context context) {
call(new Playlist.Remove(playlistid, position),
new ApiHandler<Boolean, String>() {
@Override
public Boolean handleResponse(AbstractCall<String> apiCall) {
return "OK".equals(apiCall.getResult());
}
}, response, context);
}
protected void getPlaylist(int playlistid,
DataResponse<ArrayList<String>> response, Context context) {
call(new Playlist.GetItems(playlistid),
new ApiHandler<ArrayList<String>, ListModel.AllItems>() {
@Override
public ArrayList<String> handleResponse(
AbstractCall<ListModel.AllItems> apiCall) {
ArrayList<String> playlistItems = new ArrayList<String>();
ArrayList<ListModel.AllItems> items = apiCall
.getResults();
if (items == null || items.size() == 0) {
playlistItems.add(EMPTY_PLAYLIST_ITEM);
}
for (ListModel.AllItems item : items) {
playlistItems.add(item.label);
}
return playlistItems;
}
}, response, context);
}
protected void getPlaylistPosition(int playlistid,
DataResponse<Integer> response, Context context) {
call(new Player.GetProperties(playlistid, "position"),
new ApiHandler<Integer, PlayerModel.PropertyValue>() {
@Override
public Integer handleResponse(
AbstractCall<PlayerModel.PropertyValue> apiCall) {
PlayerModel.PropertyValue properties = apiCall
.getResult();
return properties.position;
}
}, response, context);
}
}