// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"
package com.theartofdev.fastimageloader;
import android.app.Application;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import com.theartofdev.fastimageloader.adapter.IdentityAdapter;
import com.theartofdev.fastimageloader.impl.DecoderImpl;
import com.theartofdev.fastimageloader.impl.DiskCacheImpl;
import com.theartofdev.fastimageloader.impl.DownloaderImpl;
import com.theartofdev.fastimageloader.impl.LoaderHandler;
import com.theartofdev.fastimageloader.impl.MemoryPoolImpl;
import com.theartofdev.fastimageloader.impl.NativeHttpClient;
import com.theartofdev.fastimageloader.impl.OkHttpClient;
import com.theartofdev.fastimageloader.impl.util.FILLogger;
import com.theartofdev.fastimageloader.impl.util.FILUtils;
import com.theartofdev.fastimageloader.target.TargetHelper;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
/**
* TODO:a add doc
*/
public final class FastImageLoader {
//region: Fields and Consts
/**
* Single instance of the class
*/
private static final FastImageLoader INST = new FastImageLoader();
/**
* The defined image loading specs
*/
private final Map<String, ImageLoadSpec> mSpecs = new HashMap<>();
/**
* Handler for image loading logic
*/
private LoaderHandler mLoaderHandler;
/**
* Android application to init by
*/
private Application mApplication;
/**
* used to convert image URI by spec for image service (Thumbor\imgIX\etc.)
*/
private ImageServiceAdapter mDefaultImageServiceAdapter;
/**
* The folder to use for disk caching.
*/
private File mCacheFolder;
/**
* The max size of the cache (50MB)
*/
private long mCacheMaxSize = 50 * 1024 * 1024;
/**
* The max time image is cached without use before delete
*/
private long mCacheTtl = 8 * DateUtils.DAY_IN_MILLIS;
/**
* Used to decode images from the disk to bitmap.
*/
private Decoder mDecoder;
/**
* the memory pool to use
*/
private MemoryPool mMemoryPool;
/**
* the disk cache to use
*/
private DiskCache mDiskCache;
/**
* the downloader to use
*/
private Downloader mDownloader;
/**
* The HTTP client to be used to download images
*/
private HttpClient mHttpClient;
//endregion
/**
* Prevent init.
*/
private FastImageLoader() {
}
/**
* Initialize the image loader with given android application context.<br>
* Image loader can be initialized only once where you can set all the configuration
* properties:
* {@link #setDefaultImageServiceAdapter(ImageServiceAdapter)},
* {@link #setHttpClient(HttpClient)},
* {@link #setDebugIndicator(boolean)}.
*
* @param application the android mApplication instance
* @throws IllegalStateException already initialized
*/
public static FastImageLoader init(Application application) {
FILUtils.notNull(application, "context");
if (INST.mLoaderHandler == null) {
FILLogger.debug("Init fast image loader...");
FILUtils.MainThreadId = application.getMainLooper().getThread().getId();
TargetHelper.mDensity = application.getResources().getDisplayMetrics().density;
INST.mApplication = application;
return INST;
} else {
throw new IllegalStateException("Fast Image Loader is already initialized");
}
}
/**
* used to convert image URI by spec for image service (Thumbor\imgIX\etc.)
*/
public FastImageLoader setDefaultImageServiceAdapter(ImageServiceAdapter imageServiceAdapter) {
mDefaultImageServiceAdapter = imageServiceAdapter;
return INST;
}
/**
* Set the folder to use for disk caching.<br>
* This setter is ignored if {@link #setDiskCache(DiskCache)} is used.
*/
public FastImageLoader setCacheFolder(File cacheFolder) {
mCacheFolder = cacheFolder;
return INST;
}
/**
* The max size of the disk cache (default 50MB).<br>
* This setter is ignored if {@link #setDiskCache(DiskCache)} is used.
*/
public FastImageLoader setCacheMaxSize(long cacheMaxSize) {
mCacheMaxSize = cacheMaxSize;
return INST;
}
/**
* The max time image is cached without use before is delete (default 8 days).<br>
* This setter is ignored if {@link #setDiskCache(DiskCache)} is used.
*/
public FastImageLoader setCacheTtl(long cacheTtl) {
mCacheTtl = cacheTtl;
return INST;
}
/**
* Used to decode images from the disk to bitmap.
*/
public FastImageLoader setDecoder(Decoder decoder) {
mDecoder = decoder;
return INST;
}
/**
* Set the memory pool handler to be used.
*/
public FastImageLoader setMemoryPool(MemoryPool memoryPool) {
mMemoryPool = memoryPool;
return INST;
}
/**
* Set the disk cache handler to be used.
*/
public FastImageLoader setDiskCache(DiskCache diskCache) {
mDiskCache = diskCache;
return INST;
}
/**
* Set the downloader handler to be used.
*/
public FastImageLoader setDownloader(Downloader downloader) {
mDownloader = downloader;
return INST;
}
/**
* The HTTP client to be used to download images
* This setter is ignored if {@link #setDownloader(Downloader)} is used.
*/
public FastImageLoader setHttpClient(HttpClient httpClient) {
mHttpClient = httpClient;
return INST;
}
/**
* If to write logs to logcat (Default: false).
*/
public FastImageLoader setWriteLogsToLogcat(boolean enable) {
FILLogger.mLogcatEnabled = enable;
return INST;
}
/**
* The min log level to write logs at, logs below this level are ignored (Default: INFO).<br>
* Use: {@link Log#DEBUG}, {@link Log#INFO}, {@link Log#WARN}, {@link Log#ERROR}, {@link Log#ASSERT}.
*/
public FastImageLoader setLogLevel(int level) {
FILLogger.mLogLevel = level;
return INST;
}
/**
* Set appender to use to send logs to, allow client to log this library inner logs into custom framework.
*/
public FastImageLoader setLogAppender(LogAppender appender) {
FILLogger.mAppender = appender;
return INST;
}
/**
* Is to show indicator if the image was loaded from MEMORY/DISK/NETWORK.
*/
public FastImageLoader setDebugIndicator(boolean enable) {
TargetHelper.debugIndicator = enable;
return INST;
}
/**
* Create {@link com.theartofdev.fastimageloader.ImageLoadSpec} using
* {@link com.theartofdev.fastimageloader.ImageLoadSpecBuilder}.<br>
* <br><br>
* Must be initialized first using {@link #init(android.app.Application)}.
*
* @param key the unique key of the spec used for identification
* @throws IllegalStateException NOT initialized
* @throws IllegalArgumentException spec with the given key already defined
*/
public static ImageLoadSpecBuilder buildSpec(String key) {
FILUtils.notNullOrEmpty(key, "key");
FILUtils.verifyOnMainThread();
INST.finishInit();
if (INST.mSpecs.containsKey(key)) {
throw new IllegalArgumentException("Spec with the same key already exists");
}
FILLogger.debug("Create image load spec... [{}]", key);
return new ImageLoadSpecBuilder(key, INST.mApplication, INST.mDefaultImageServiceAdapter);
}
/**
* Get {@link com.theartofdev.fastimageloader.ImageLoadSpec} for the given key if exists.
*
* @param key the unique key of the spec used for identification
* @return spec instance or null if no matching spec found
*/
public static ImageLoadSpec getSpec(String key) {
FILUtils.notNullOrEmpty(key, "key");
return INST.mSpecs.get(key);
}
/**
* Prefetch image (uri+spec) to be available in disk cache.<br>
*
* @param uri the URI of the image to prefetch
* @param specKey the spec to prefetch the image by
*/
public static void prefetchImage(String uri, String specKey) {
FILUtils.notNullOrEmpty(specKey, "specKey");
FILUtils.verifyOnMainThread();
if (!TextUtils.isEmpty(uri)) {
INST.finishInit();
ImageLoadSpec spec = INST.mSpecs.get(specKey);
if (spec == null) {
throw new IllegalArgumentException("Invalid spec key, no spec defined for the given key: " + specKey);
}
FILLogger.debug("Prefetch image... [{}] [{}]", uri, spec);
INST.mLoaderHandler.prefetchImage(uri, spec);
}
}
/**
* Load image by and to the given target.<br>
* Handle transformation on the image, image dimension specification and dimension fallback.<br>
* If the image of the requested dimensions is not found in memory cache we try to find the fallback dimension, if
* found it will be set to the target, and the requested dimension image will be loaded async.<br>
* <br><br>
* Must be initialized first using {@link #init(android.app.Application)}.
*
* @param target the target to load the image to, use it's URL and Spec
* @param altSpecKey optional: alternative specification to load image from cache if primary is no available in
* cache.
* @throws IllegalStateException NOT initialized
*/
public static void loadImage(Target target, String altSpecKey) {
FILUtils.notNull(target, "target");
FILUtils.verifyOnMainThread();
INST.finishInit();
ImageLoadSpec spec = INST.mSpecs.get(target.getSpecKey());
ImageLoadSpec altSpec = altSpecKey != null ? INST.mSpecs.get(altSpecKey) : null;
if (spec == null) {
throw new IllegalArgumentException("Invalid spec key, no spec defined for the given key: " + target.getSpecKey());
}
if (altSpecKey != null && altSpec == null) {
throw new IllegalArgumentException("Invalid alternative spec key, no spec defined for the given key: " + altSpecKey);
}
FILLogger.debug("Load image... [{}] [{}] [{}]", target, spec, altSpecKey);
INST.mLoaderHandler.loadImage(target, spec, altSpec);
}
/**
* Clear the disk image cache, deleting all cached images.
* <br><br>
* Must be initialized first using {@link #init(android.app.Application)}.
*
* @throws IllegalStateException NOT initialized.
*/
public static void clearDiskCache() {
INST.finishInit();
FILLogger.debug("Clear image cache...");
INST.mLoaderHandler.clearDiskCache();
}
/**
* Add the given image load spec to the defined specs.
*/
static void addSpec(ImageLoadSpec spec) {
FILLogger.debug("Image load spec created... [{}]", spec);
INST.mSpecs.put(spec.getKey(), spec);
}
//region: Private methods
/**
* Finish the initialization process.
*
* @throws IllegalStateException NOT initialized.
*/
private void finishInit() {
if (INST.mLoaderHandler == null) {
if (INST.mApplication != null) {
FILUtils.verifyOnMainThread();
if (INST.mDefaultImageServiceAdapter == null) {
FILLogger.debug("Use default identity image service adapter...");
INST.mDefaultImageServiceAdapter = new IdentityAdapter();
}
if (mMemoryPool == null) {
FILLogger.debug("Use default memory pool...");
mMemoryPool = new MemoryPoolImpl();
}
if (mDecoder == null) {
FILLogger.debug("Use default decoder...");
mDecoder = new DecoderImpl();
}
if (mDiskCache == null) {
if (mCacheFolder == null) {
FILLogger.debug("Use default cache folder...");
mCacheFolder = new File(FILUtils.pathCombine(mApplication.getCacheDir().getPath(), "ImageCache"));
}
FILLogger.debug("Use default disk cache... [{}]", mCacheFolder);
mDiskCache = new DiskCacheImpl(mApplication, mCacheFolder, mCacheMaxSize, mCacheTtl);
}
if (mDownloader == null) {
initHttpClient();
FILLogger.debug("Use default downloader...");
mDownloader = new DownloaderImpl(mHttpClient);
}
FILLogger.debug("Create load handler... [{}] [{}] [{}]", mMemoryPool, mDiskCache, mDownloader);
INST.mLoaderHandler = new LoaderHandler(mApplication, mMemoryPool, mDiskCache, mDownloader, mDecoder);
} else {
throw new IllegalStateException("Fast Image Loader is NOT initialized, call init(...)");
}
}
}
/**
* Init HTTP client to be used by the downloader.<br>
* 1. If one given externally use it.<br>
* 2. Try using OK HTTP client, won't work if there is no OK HTTP dependency<br>
* 3. Use native Android URL connection.<br>
*/
private void initHttpClient() {
if (INST.mHttpClient == null) {
try {
FILLogger.debug("Try create OK HTTP client...");
INST.mHttpClient = new OkHttpClient();
} catch (Throwable e) {
if (e.getClass().isAssignableFrom(NoClassDefFoundError.class)) {
FILLogger.debug("OK HTTP dependency no found, use native Android URL Connection");
} else {
FILLogger.warn("Failed to init OK HTTP client, use native Android URL Connection", e);
}
INST.mHttpClient = new NativeHttpClient();
}
}
}
//endregion
}