package com.koushikdutta.ion; import android.annotation.TargetApi; import android.app.Fragment; import android.content.Context; import android.net.Uri; 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.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.util.FileCache; import com.koushikdutta.async.util.FileUtility; 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.LoadBuilder; import com.koushikdutta.ion.conscrypt.ConscryptMiddleware; import com.koushikdutta.ion.cookie.CookieMiddleware; import com.koushikdutta.ion.loader.AssetLoader; 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.ResourceLoader; import com.koushikdutta.ion.loader.VideoLoader; import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; import java.io.File; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.net.ssl.SSLContext; /** * 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 * @param context * @return */ public static LoadBuilder<Builders.Any.B> with(Context context) { return getDefault(context).build(context); } /** * the default Ion object instance and begin building a request * @param fragment * @return */ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) public static LoadBuilder<Builders.Any.B> with(Fragment fragment) { return getDefault(fragment.getActivity()).build(fragment); } /** * the default Ion object instance and begin building a request * @param fragment * @return */ public static LoadBuilder<Builders.Any.B> with(android.support.v4.app.Fragment fragment) { return getDefault(fragment.getActivity()).build(fragment); } /** * 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) { if (context == null) throw new NullPointerException("Can not pass null context in to retrieve ion instance"); 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) { return getDefault(imageView.getContext()).build(imageView); } AsyncHttpClient httpClient; ConscryptMiddleware conscryptMiddleware; CookieMiddleware cookieMiddleware; ResponseCacheMiddleware responseCache; FileCache storeCache; HttpLoader httpLoader; ContentLoader contentLoader; ResourceLoader resourceLoader; AssetLoader assetLoader; VideoLoader videoLoader; PackageIconLoader packageIconLoader; FileLoader fileLoader; String logtag; int logLevel; Gson 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; IonImageViewRequestBuilder bitmapBuilder = new IonImageViewRequestBuilder(this); private Ion(Context context, String name) { this.context = context = context.getApplicationContext(); this.name = name; httpClient = new AsyncHttpClient(new AsyncServer("ion-" + name)); httpClient.getSSLSocketMiddleware().setHostnameVerifier(new BrowserCompatHostnameVerifier()); httpClient.getSSLSocketMiddleware().setSpdyEnabled(true); httpClient.insertMiddleware(conscryptMiddleware = new ConscryptMiddleware(context, httpClient.getSSLSocketMiddleware())); File ionCacheDir = new File(context.getCacheDir(), name); try { responseCache = ResponseCacheMiddleware.addCache(httpClient, ionCacheDir, 10L * 1024L * 1024L); } catch (IOException e) { IonLog.w("unable to set up response cache, clearing", e); FileUtility.deleteDirectory(ionCacheDir); try { responseCache = ResponseCacheMiddleware.addCache(httpClient, ionCacheDir, 10L * 1024L * 1024L); } catch (IOException ex) { IonLog.w("unable to set up response cache, failing", e); } } storeCache = new FileCache(new File(context.getFilesDir(), name), Long.MAX_VALUE, false); // 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(resourceLoader = new ResourceLoader()) .addLoader(assetLoader = new AssetLoader()) .addLoader(fileLoader = new FileLoader()); } public static ExecutorService getBitmapLoadExecutorService() { return bitmapExecutorService; } public static ExecutorService getIoExecutorService() { return ioExecutorService; } /** * Begin building a request * @param context * @return */ public LoadBuilder<Builders.Any.B> build(Context context) { return new IonRequestBuilder(ContextReference.fromContext(context), this); } /** * Begin building a request * @param fragment * @return */ public LoadBuilder<Builders.Any.B> build(Fragment fragment) { return new IonRequestBuilder(new ContextReference.FragmentContextReference(fragment), this); } /** * Begin building a request * @param fragment * @return */ public LoadBuilder<Builders.Any.B> build(android.support.v4.app.Fragment fragment) { return new IonRequestBuilder(new ContextReference.SupportFragmentContextReference(fragment), 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 static Comparator<DeferredLoadBitmap> DEFERRED_COMPARATOR = new Comparator<DeferredLoadBitmap>() { @Override public int compare(DeferredLoadBitmap lhs, DeferredLoadBitmap rhs) { // higher is more recent if (lhs.priority == rhs.priority) return 0; if (lhs.priority < rhs.priority) return 1; return -1; } }; 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; Collections.sort(deferred, DEFERRED_COMPARATOR); 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(this)); } /** * Get or put an item from the cache * @return */ public FileCacheStore cache(String key) { return new FileCacheStore(this, responseCache.getFileCache(), key); } public FileCache getCache() { return responseCache.getFileCache(); } /** * Get or put an item in the persistent store * @return */ public FileCacheStore store(String key) { return new FileCacheStore(this, storeCache, key); } public FileCache getStore() { return storeCache; } public String getName() { return name; } /** * Get the Cookie middleware that is attached to the AsyncHttpClient instance. * @return */ public CookieMiddleware getCookieMiddleware() { return cookieMiddleware; } public ConscryptMiddleware getConscryptMiddleware() { return conscryptMiddleware; } /** * 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; } public SSLContext createSSLContext(String algorithm) throws NoSuchAlgorithmException { conscryptMiddleware.initialize(); return SSLContext.getInstance(algorithm); } /** * Get the Gson object in use by this Ion instance. * This can be used to customize serialization and deserialization * from java objects. * @return */ public synchronized Gson getGson() { if (gson == null) gson = new Gson(); 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.getSSLSocketMiddleware().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, Headers headers) { AsyncHttpRequest request = new AsyncHttpRequest(uri, method, headers); if (!TextUtils.isEmpty(userAgent)) request.getHeaders().set("User-Agent", 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; } }