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; } }