package com.koushikdutta.ion;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.widget.ImageView;
import com.google.gson.Gson;
import com.koushikdutta.async.AsyncServer;
import com.koushikdutta.async.future.Future;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.async.future.SimpleFuture;
import com.koushikdutta.async.future.TransformFuture;
import com.koushikdutta.async.http.AsyncHttpClient;
import com.koushikdutta.async.http.AsyncHttpRequest;
import com.koushikdutta.async.http.ResponseCacheMiddleware;
import com.koushikdutta.async.http.libcore.DiskLruCache;
import com.koushikdutta.async.http.libcore.RawHeaders;
import com.koushikdutta.async.util.HashList;
import com.koushikdutta.ion.bitmap.BitmapInfo;
import com.koushikdutta.ion.bitmap.IonBitmapCache;
import com.koushikdutta.ion.builder.Builders;
import com.koushikdutta.ion.builder.FutureBuilder;
import com.koushikdutta.ion.builder.LoadBuilder;
import com.koushikdutta.ion.cookie.CookieMiddleware;
import com.koushikdutta.ion.loader.AsyncHttpRequestFactory;
import com.koushikdutta.ion.loader.ContentLoader;
import com.koushikdutta.ion.loader.FileLoader;
import com.koushikdutta.ion.loader.HttpLoader;
import com.koushikdutta.ion.loader.PackageIconLoader;
import com.koushikdutta.ion.loader.VideoLoader;
/**
* Created by koush on 5/21/13.
*/
public class Ion {
static final Handler mainHandler = new Handler(Looper.getMainLooper());
static int availableProcessors = Runtime.getRuntime().availableProcessors();
static ExecutorService ioExecutorService = Executors.newFixedThreadPool(4);
static ExecutorService bitmapExecutorService = availableProcessors > 2 ? Executors.newFixedThreadPool(availableProcessors - 1) : Executors.newFixedThreadPool(1);
static HashMap<String, Ion> instances = new HashMap<String, Ion>();
/**
* Get the default Ion object instance and begin building a request
* with the given uri
* @param context
* @param uri
* @return
*/
public static Builders.Any.B with(Context context, String uri) {
return getDefault(context).build(context, uri);
}
/**
* Get the default Ion object instance and begin building a request
* @param context
* @return
*/
public static LoadBuilder<Builders.Any.B> with(Context context) {
return getDefault(context).build(context);
}
/**
* Get the default Ion object instance and begin building an operation
* on the given file
* @param context
* @param file
* @return
*/
public static FutureBuilder with(Context context, File file) {
return getDefault(context).build(context, file);
}
/**
* Get the default Ion instance
* @param context
* @return
*/
public static Ion getDefault(Context context) {
return getInstance(context, "ion");
}
/**
* Get the given Ion instance by name
* @param context
* @param name
* @return
*/
public static Ion getInstance(Context context, String name) {
Ion instance = instances.get(name);
if (instance == null)
instances.put(name, instance = new Ion(context, name));
return instance;
}
/**
* Create a ImageView bitmap request builder
* @param imageView
* @return
*/
public static Builders.IV.F<? extends Builders.IV.F<?>> with(ImageView imageView) {
Ion ion = getDefault(imageView.getContext());
return ion.build(imageView);
}
AsyncHttpClient httpClient;
CookieMiddleware cookieMiddleware;
ResponseCacheMiddleware responseCache;
DiskLruCache storeCache;
HttpLoader httpLoader;
ContentLoader contentLoader;
VideoLoader videoLoader;
PackageIconLoader packageIconLoader;
FileLoader fileLoader;
String logtag;
int logLevel;
Gson gson = new Gson();
String userAgent;
ArrayList<Loader> loaders = new ArrayList<Loader>();
String name;
HashList<FutureCallback<BitmapInfo>> bitmapsPending = new HashList<FutureCallback<BitmapInfo>>();
Config config = new Config();
IonBitmapCache bitmapCache;
Context context;
IonBitmapRequestBuilder bitmapBuilder = new IonBitmapRequestBuilder(this);
private Ion(Context context, String name) {
httpClient = new AsyncHttpClient(new AsyncServer());
this.context = context = context.getApplicationContext();
this.name = name;
try {
responseCache = ResponseCacheMiddleware.addCache(httpClient, new File(context.getCacheDir(), name), 10L * 1024L * 1024L);
}
catch (Exception e) {
IonLog.w("unable to set up response cache", e);
}
try {
storeCache = DiskLruCache.open(new File(context.getFilesDir(), name), 1, 1, Long.MAX_VALUE);
}
catch (Exception e) {
}
// TODO: Support pre GB?
if (Build.VERSION.SDK_INT >= 9)
addCookieMiddleware();
httpClient.getSocketMiddleware().setConnectAllAddresses(true);
httpClient.getSSLSocketMiddleware().setConnectAllAddresses(true);
bitmapCache = new IonBitmapCache(this);
configure()
.addLoader(videoLoader = new VideoLoader())
.addLoader(packageIconLoader = new PackageIconLoader())
.addLoader(httpLoader = new HttpLoader())
.addLoader(contentLoader = new ContentLoader())
.addLoader(fileLoader = new FileLoader());
}
public static ExecutorService getBitmapLoadExecutorService() {
return bitmapExecutorService;
}
public static ExecutorService getIoExecutorService() {
return ioExecutorService;
}
/**
* Begin building an operation on the given file
* @param context
* @param file
* @return
*/
public FutureBuilder build(Context context, File file) {
return new IonRequestBuilder(context, this).load(file);
}
/**
* Begin building a request with the given uri
* @param context
* @param uri
* @return
*/
public Builders.Any.B build(Context context, String uri) {
return new IonRequestBuilder(context, this).load(uri);
}
/**
* Begin building a request
* @param context
* @return
*/
public LoadBuilder<Builders.Any.B> build(Context context) {
return new IonRequestBuilder(context, this);
}
/**
* Create a builder that can be used to build an network request
* @param imageView
* @return
*/
public Builders.IV.F<? extends Builders.IV.F<?>> build(ImageView imageView) {
if (Thread.currentThread() != Looper.getMainLooper().getThread())
throw new IllegalStateException("must be called from UI thread");
bitmapBuilder.reset();
bitmapBuilder.ion = this;
return bitmapBuilder.withImageView(imageView);
}
int groupCount(Object group) {
FutureSet members;
synchronized (this) {
members = inFlight.get(group);
}
if (members == null)
return 0;
return members.size();
}
private Runnable processDeferred = new Runnable() {
@Override
public void run() {
if (BitmapFetcher.shouldDeferImageView(Ion.this))
return;
ArrayList<DeferredLoadBitmap> deferred = null;
for (String key: bitmapsPending.keySet()) {
Object owner = bitmapsPending.tag(key);
if (owner instanceof DeferredLoadBitmap) {
DeferredLoadBitmap deferredLoadBitmap = (DeferredLoadBitmap)owner;
if (deferred == null)
deferred = new ArrayList<DeferredLoadBitmap>();
deferred.add(deferredLoadBitmap);
}
}
if (deferred == null)
return;
int count = 0;
for (DeferredLoadBitmap deferredLoadBitmap: deferred) {
bitmapsPending.tag(deferredLoadBitmap.key, null);
bitmapsPending.tag(deferredLoadBitmap.fetcher.bitmapKey, null);
deferredLoadBitmap.fetcher.execute();
count++;
// do MAX_IMAGEVIEW_LOAD max. this may end up going over the MAX_IMAGEVIEW_LOAD threshhold
if (count > BitmapFetcher.MAX_IMAGEVIEW_LOAD)
return;
}
}
};
void processDeferred() {
mainHandler.removeCallbacks(processDeferred);
mainHandler.post(processDeferred);
}
/**
* Cancel all pending requests associated with the request group
* @param group
*/
public void cancelAll(Object group) {
FutureSet members;
synchronized (this) {
members = inFlight.remove(group);
}
if (members == null)
return;
for (Future future: members.keySet()) {
if (future != null)
future.cancel();
}
}
void addFutureInFlight(Future future, Object group) {
if (group == null || future == null || future.isDone() || future.isCancelled())
return;
FutureSet members;
synchronized (this) {
members = inFlight.get(group);
if (members == null) {
members = new FutureSet();
inFlight.put(group, members);
}
}
members.put(future, true);
}
/**
* Cancel all pending requests
*/
public void cancelAll() {
ArrayList<Object> groups;
synchronized (this) {
groups = new ArrayList<Object>(inFlight.keySet());
}
for (Object group: groups)
cancelAll(group);
}
/**
* Cancel all pending requests associated with the given context
* @param context
*/
public void cancelAll(Context context) {
cancelAll((Object)context);
}
public int getPendingRequestCount(Object group) {
synchronized (this) {
FutureSet members = inFlight.get(group);
if (members == null)
return 0;
int ret = 0;
for (Future future: members.keySet()) {
if (!future.isCancelled() && !future.isDone())
ret++;
}
return ret;
}
}
public void dump() {
bitmapCache.dump();
Log.i(logtag, "Pending bitmaps: " + bitmapsPending.size());
Log.i(logtag, "Groups: " + inFlight.size());
for (FutureSet futures: inFlight.values()) {
Log.i(logtag, "Group size: " + futures.size());
}
}
/**
* Get the application Context object in use by this Ion instance
* @return
*/
public Context getContext() {
return context;
}
static class FutureSet extends WeakHashMap<Future, Boolean> {
}
// maintain a list of futures that are in being processed, allow for bulk cancellation
WeakHashMap<Object, FutureSet> inFlight = new WeakHashMap<Object, FutureSet>();
private void addCookieMiddleware() {
httpClient.insertMiddleware(cookieMiddleware = new CookieMiddleware(context, name));
}
/**
* Get or put an item from the cache
* @return
*/
public DiskLruCacheStore cache() {
return new DiskLruCacheStore(this, responseCache.getDiskLruCache());
}
/**
* Get or put an item in the persistent store
* @return
*/
public DiskLruCacheStore store() {
return new DiskLruCacheStore(this, responseCache.getDiskLruCache());
}
public String getName() {
return name;
}
/**
* Get the Cookie middleware that is attached to the AsyncHttpClient instance.
* @return
*/
public CookieMiddleware getCookieMiddleware() {
return cookieMiddleware;
}
/**
* Get the AsyncHttpClient object in use by this Ion instance
* @return
*/
public AsyncHttpClient getHttpClient() {
return httpClient;
}
/**
* Get the AsyncServer reactor in use by this Ion instance
* @return
*/
public AsyncServer getServer() {
return httpClient.getServer();
}
public class Config {
public HttpLoader getHttpLoader() {
return httpLoader;
}
public VideoLoader getVideoLoader() {
return videoLoader;
}
public PackageIconLoader getPackageIconLoader() {
return packageIconLoader;
}
public ContentLoader getContentLoader() {
return contentLoader;
}
public FileLoader getFileLoader() {
return fileLoader;
}
public ResponseCacheMiddleware getResponseCache() {
return responseCache;
}
/**
* Get the Gson object in use by this Ion instance.
* This can be used to customize serialization and deserialization
* from java objects.
* @return
*/
public Gson getGson() {
return gson;
}
/**
* Set the log level for all requests made by Ion.
* @param logtag
* @param logLevel
* @return
*/
public Config setLogging(String logtag, int logLevel) {
Ion.this.logtag = logtag;
Ion.this.logLevel = logLevel;
return this;
}
/**
* Route all http requests through the given proxy.
* @param host
* @param port
*/
public void proxy(String host, int port) {
httpClient.getSocketMiddleware().enableProxy(host, port);
}
/**
* Route all https requests through the given proxy.
* Note that https proxying requires that the Android device has the appropriate
* root certificate installed to function properly.
* @param host
* @param port
*/
public void proxySecure(String host, int port) {
httpClient.getSSLSocketMiddleware().enableProxy(host, port);
}
/**
* Disable routing of http requests through a previous provided proxy
*/
public void disableProxy() {
httpClient.getSocketMiddleware().disableProxy();
}
/**
* Disable routing of https requests through a previous provided proxy
*/
public void disableSecureProxy() {
httpClient.getSocketMiddleware().disableProxy();
}
/**
* Set the Gson object in use by this Ion instance.
* This can be used to customize serialization and deserialization
* from java objects.
* @param gson
*/
public void setGson(Gson gson) {
Ion.this.gson = gson;
}
AsyncHttpRequestFactory asyncHttpRequestFactory = new AsyncHttpRequestFactory() {
@Override
public AsyncHttpRequest createAsyncHttpRequest(URI uri, String method, RawHeaders headers) {
AsyncHttpRequest request = new AsyncHttpRequest(uri, method, headers);
if (!TextUtils.isEmpty(userAgent))
request.getHeaders().setUserAgent(userAgent);
return request;
}
};
public AsyncHttpRequestFactory getAsyncHttpRequestFactory() {
return asyncHttpRequestFactory;
}
public Config setAsyncHttpRequestFactory(AsyncHttpRequestFactory asyncHttpRequestFactory) {
this.asyncHttpRequestFactory = asyncHttpRequestFactory;
return this;
}
public String userAgent() {
return userAgent;
}
public Config userAgent(String userAgent) {
Ion.this.userAgent = userAgent;
return this;
}
public Config addLoader(int index, Loader loader) {
loaders.add(index, loader);
return this;
}
public Config insertLoader(Loader loader) {
loaders.add(0, loader);
return this;
}
public Config addLoader(Loader loader) {
loaders.add(loader);
return this;
}
public List<Loader> getLoaders() {
return loaders;
}
}
public Config configure() {
return config;
}
/**
* Return the bitmap cache used by this Ion instance
* @return
*/
public IonBitmapCache getBitmapCache() {
return bitmapCache;
}
Future<AsyncHttpRequest> resolveRequest(AsyncHttpRequest request) {
return resolveRequest(request, null);
}
Future<AsyncHttpRequest> resolveRequest(AsyncHttpRequest request, SimpleFuture<AsyncHttpRequest> ret) {
// first attempt to resolve the url
for (Loader loader: loaders) {
Future<AsyncHttpRequest> resolved = loader.resolve(this, request);
if (resolved != null) {
if (ret == null)
ret = new SimpleFuture<AsyncHttpRequest>();
final SimpleFuture<AsyncHttpRequest> future = ret;
resolved.setCallback(new FutureCallback<AsyncHttpRequest>() {
@Override
public void onCompleted(Exception e, AsyncHttpRequest result) {
if (e != null) {
future.setComplete(e);
return;
}
resolveRequest(result, future);
}
});
return ret;
}
}
if (ret != null)
ret.setComplete(request);
return ret;
}
}