@Beta
public enum AnimationMode {
    /**
     * Use no animation (same as using a normal {@link BitmapAsyncSetter})
     */
    NEVER,
    /**
     * Animate only if the bitmap is not already in the memory caches
     */
    NOT_IN_MEMORY,
    /**
     * Animate only if the bitmap was loaded from network
     */
    FROM_NETWORK,
    /**
     * Always animate (use with care: the performance impact can be noticeable when scrolling long
     * lists of bitmaps)
     */
    ALWAYS;
} @Beta
public interface BitmapCache extends ContentProxy {

    /**
     * Callback interface to retrieve the result of a Bitmap loading from a {@link BitmapCache}.
     *
     * The callback methods can be executed in an internal pool thread, not in the caller thread.
     * Make sure you are handling this when dealing with UI and non-thread safe code.
     */
    @Beta
    public interface OnBitmapRetrievalListener {

        /**
         * Called when a Bitmap has been retrieved from the cache.
         *
         * @param key    The {@link CacheUrlKey} of the bitmap
         * @param bitmap The retrieved Bitmap
         * @param source The {@link CacheSource} from where the bitmap has been retrieved
         */
        public void onBitmapRetrieved(@NonNull CacheUrlKey key, @NonNull Bitmap bitmap,
                                      @NonNull CacheSource source);

        /**
         * Called when a bitmap could not be retrieved.
         *
         * @param key The {@link CacheUrlKey} of the bitmap
         * @param e   The exception that caused the error (if any)
         */
        public void onBitmapRetrievalFailed(@NonNull CacheUrlKey key, @Nullable Exception e);
    }

    /**
     * Extension of {@link OnBitmapRetrievalListener} with methods related to setting a {@link
     * Bitmap} inside an {@link ImageView}.
     *
     * @since 1.0.2
     */
    @Beta
    public interface BitmapSetter extends OnBitmapRetrievalListener {

        /**
         * Sets a temporary {@link Drawable} placeholder to the image view or resets the image
         * drawable in the image view by passing null.
         *
         * Note: implementations must set the drawable synchronously, and the method should be
         * called from the UI thread.
         *
         * @param drawable The drawable placeholder (can be null)
         */
        public void setPlaceholder(@Nullable Drawable drawable);
    }

    /**
     * Simple {@link OnBitmapRetrievalListener} implementation to retrieve a successful retrieval of
     * a bitmap.
     */
    @Beta
    public static abstract class OnSuccessfulBitmapRetrievalListener implements
            OnBitmapRetrievalListener {
        @Override
        public final void onBitmapRetrievalFailed(@NonNull CacheUrlKey key, @Nullable Exception e) {
            // does nothing
        }
    }

    /**
     * Callback interface to be used when the caller needs to know if and when the bitmap has
     * actually been set into the image view.
     *
     * @author Marco Salis
     * @since 1.0
     */
    @Beta
    public interface OnBitmapSetListener {

        /**
         * Called from the UI thread when the retrieved bitmap image has been set into the {@link
         * ImageView}
         *
         * @param key    The {@link CacheUrlKey} of the bitmap
         * @param bitmap The set {@link Bitmap}
         * @param source The {@link CacheSource} of the bitmap
         */
        public void onBitmapSet(@NonNull CacheUrlKey key, @NonNull Bitmap bitmap,
                                @NonNull CacheSource source);
    }

    /**
     * <p> Builds a new instance of {@link BitmapSetterBuilder} for this cache. Retain this
     * reference for an activity, component or list view, to optimize bitmap retrieval and setting
     * into views.
     *
     * <p> See {@link BitmapSetterBuilder} documentation for more information.
     *
     * @param allowReuse true to create a {@link BitmapSetterBuilder} that can be reused locally and
     *                   holds caches to improve performances, false to create a one-off builder
     * @return The built instance
     */
    @NonNull
    public BitmapSetterBuilder newBitmapSetterBuilder(boolean allowReuse);

    /**
     * Retrieves a bitmap asynchronously with the specified {@link AccessPolicy}
     *
     * Use {@link #preloadBitmap(CacheUrlKey)} for {@link AccessPolicy#PRE_FETCH}.
     *
     * @param key      The {@link CacheUrlKey} of the bitmap
     * @param policy   The {@link AccessPolicy} to use
     * @param listener {@link OnBitmapRetrievalListener} to get the bitmap if successfully
     *                 retrieved
     * @throws IllegalArgumentException if policy is {@link AccessPolicy#PRE_FETCH}
     */
    public void getBitmapAsync(@NonNull CacheUrlKey key, @NonNull AccessPolicy policy,
                               @NonNull OnBitmapRetrievalListener listener);

    /**
     * Preloads a bitmap into the cache for future use. Does nothing if the bitmap is already in one
     * of the caches.
     *
     * @param key The {@link CacheUrlKey} of the bitmap
     */
    public void preloadBitmap(@NonNull CacheUrlKey key);

    /**
     * Asynchronously sets the retrieved bitmap into the passed image view.
     *
     * @param url  The string URL of the bitmap
     * @param view The {@link ImageView} to set the bitmap into
     */
    public void setBitmapAsync(@NonNull String url, @NonNull ImageView view);

    /**
     * Asynchronously sets the retrieved bitmap into the passed image view.
     *
     * @param key  The {@link CacheUrlKey} of the bitmap
     * @param view The {@link ImageView} to set the bitmap into
     */
    public void setBitmapAsync(@NonNull CacheUrlKey key, @NonNull ImageView view);

    /**
     * <strong>Note:</strong> this method is not officially part of the public interface, and its
     * direct use is not recommended as it is subject to backwards incompatible variations. Call
     * {@link #newBitmapSetterBuilder(boolean)} to get a builder to configure a personalized bitmap
     * setter.
     *
     * Asynchronously sets the retrieved bitmap into an image view with the passed custom
     * parameters.
     *
     * Use {@link #setBitmapAsync(CacheUrlKey, ImageView)} if custom parameters are not needed.
     *
     * @param key         The {@link CacheUrlKey} of the bitmap
     * @param policy      The {@link AccessPolicy} to use
     * @param setter      A {@link BitmapSetter} for the image view
     * @param placeholder A (optional) placeholder {@link Drawable} to set inside the image view
     *                    when the bitmap is not available in memory
     */
    public void setBitmapAsync(@NonNull CacheUrlKey key, @NonNull AccessPolicy policy,
                               @NonNull BitmapSetter setter, @Nullable Drawable placeholder);

} @Beta
@NotThreadSafe
public final class BitmapSetterBuilder {

    private final BitmapCache mCache;
    private final boolean mCachingEnabled;

    @Nullable
    private HashMap<String, CacheUrlKey> mKeysCache;

    private CacheUrlKey mKey;
    private AccessPolicy mPolicy = AccessPolicy.NORMAL;
    private AnimationMode mAnimationMode = AnimationMode.NEVER;
    private Drawable mPlaceholder;
    private OnBitmapSetListener mListener;

    /**
     * Do not use this constructor directly, call {@link BitmapCache#newBitmapSetterBuilder(boolean)}.
     *
     * @param cache
     */
    public BitmapSetterBuilder(@NonNull BitmapCache cache) {
        this(cache, true);
    }

    /**
     * Do not use this constructor directly, call {@link BitmapCache#newBitmapSetterBuilder(boolean)}.
     *
     * @param cache
     * @param reusable
     */
    public BitmapSetterBuilder(@NonNull BitmapCache cache, boolean reusable) {
        mCache = cache;
        mCachingEnabled = reusable;
    }

    /**
     * Sets the download URL for the bitmap.
     *
     * @param url The URL string
     * @return This builder for call chaining
     */
    @NonNull
    public BitmapSetterBuilder setAsync(@NonNull String url) {
        mKey = getKeyFromCacheOrCreate(url);
        return this;
    }

    private CacheUrlKey getKeyFromCacheOrCreate(@NonNull String url) {
        if (mCachingEnabled) {
            if (mKeysCache == null) { // lazily initialized
                mKeysCache = new HashMap<String, CacheUrlKey>();
            }
            CacheUrlKey key;
            if ((key = mKeysCache.get(url)) == null) {
                key = new SimpleCacheUrlKey(url);
                mKeysCache.put(url, key);
            }
            return key;
        } else {
            return new SimpleCacheUrlKey(url);
        }
    }

    /**
     * Sets the {@link CacheUrlKey} for the bitmap.
     *
     * <strong>Note: </strong> calling this method instead of {@link #setAsync(String)} would
     * prevent any internal key cache from saving on objects instantiation. Only use to provide a
     * {@link CacheUrlKey} customized behavior.
     *
     * @param key The {@link CacheUrlKey} for the bitmap
     * @return This builder for call chaining
     */
    @NonNull
    public BitmapSetterBuilder setAsync(@NonNull CacheUrlKey key) {
        mKey = key;
        return this;
    }

    /**
     * Sets a temporary placeholder to be set into the view when the requested bitmap is not in the
     * memory cache.
     *
     * If not set, the image view drawable will be just set to null.
     *
     * @param placeholder The placeholder {@link Drawable}
     * @return This builder for call chaining
     */
    @NonNull
    public BitmapSetterBuilder placeholder(@Nullable Drawable placeholder) {
        mPlaceholder = placeholder;
        return this;
    }

    /**
     * Sets the bitmap retrieval to the passed {@link AccessPolicy}. Defaults to {@link
     * AccessPolicy#NORMAL} if not called.
     *
     * @param policy The access policy to retrieve the bitmap
     * @return This builder for call chaining
     */
    @NonNull
    public BitmapSetterBuilder policy(@NonNull AccessPolicy policy) {
        mPolicy = policy;
        return this;
    }

    /**
     * Sets the animation mode when setting the bitmap into the view. Defaults to {@link
     * AnimationMode#NEVER}.
     *
     * @param mode The {@link AnimationMode} for the bitmap
     * @return This builder for call chaining
     */
    @NonNull
    public BitmapSetterBuilder animate(@NonNull AnimationMode mode) {
        mAnimationMode = mode;
        return this;
    }

    /**
     * Sets an {@link OnBitmapSetListener} to be notified when the bitmap is set into the view.
     *
     * @param listener The listener (can be safely a context, it won't be leaked)
     * @return This builder for call chaining
     */
    @NonNull
    public BitmapSetterBuilder listener(@NonNull OnBitmapSetListener listener) {
        mListener = listener;
        return this;
    }

    /**
     * Starts the asynchronous bitmap retrieval by calling the {@link BitmapCache}.
     *
     * After this call, all the parameters added to the builder are reset to the default state.
     *
     * @param view The {@link ImageView} to set the bitmap into
     * @throws NullPointerException if any of the mandatory parameters were not set
     */
    public void into(@NonNull ImageView view) {
        checkParameters();

        // build setter
        final BitmapSetter setter = getBitmapSetter(view);

        // start the asynchronous retrieval
        mCache.setBitmapAsync(mKey, mPolicy, setter, mPlaceholder);
        reset();
    }

    /**
     * @param view
     * @throws UnsupportedOperationException
     */
    public void asBackground(@NonNull View view) {
        throw new UnsupportedOperationException("Not yet supported");
    }

    /**
     * Resets the builder to its default parameters. This method does *<i>not</i>* clear the builder
     * local caches.
     */
    public void reset() {
        mKey = null;
        mPolicy = AccessPolicy.NORMAL;
        mAnimationMode = AnimationMode.NEVER;
        mPlaceholder = null;
        mListener = null;
    }

    /**
     * Clears the builder local caches. Call this when the "context" where the builder is used is
     * not valid or the dataset changes.
     */
    public void clearLocalCaches() {
        if (mKeysCache != null) {
            mKeysCache.clear();
        }
    }

    private void checkParameters() {
        Preconditions.checkNotNull(mKey);
        Preconditions.checkNotNull(mPolicy);
        Preconditions.checkNotNull(mAnimationMode);
    }

    @NonNull
    private BitmapSetter getBitmapSetter(@NonNull ImageView view) {
        if (mAnimationMode == AnimationMode.NEVER) {
            return new BitmapAsyncSetter(mKey, view, mListener);
        } else {
            return new BitmapAnimatedAsyncSetter(mKey, view, mAnimationMode, mListener, -1);
        }
    }

} public class KrakenDemoApplication extends DroidApplication {

    public enum CacheId {
        BITMAPS_130, // cache for 130x130 images
        BITMAPS_LARGE // cache for large images
    }

    private static KrakenDemoApplication mInstance;

    /**
     * Returns the application singleton.
     */
    public static KrakenDemoApplication get() {
        return mInstance;
    }

    private static void setInstance(KrakenDemoApplication instance) {
        mInstance = instance;
    }

    private final BaseCachesManager<CacheId> mCachesManager;

    public KrakenDemoApplication() {
        // instantiate the cache manager
        mCachesManager = new BaseCachesManager<CacheId>(DroidUtils.CPU_CORES);
    }

    @Override
    public void onCreate() {
        setInstance(this);
        super.onCreate();

        // initialize HTTP requests manager
        DefaultHttpRequestsManager.get().initialize();

        // initialize caches
        final BitmapCacheBuilder builder130 = new BitmapCacheBuilder(this);
        builder130.maxMemoryCachePercentage(10) //
                .cacheLogName("BITMAPS_130") //
                .diskCachePurgeableAfter(DroidUtils.DAY) //
                .diskCacheDirectoryName("bitmaps130");

        final BitmapCacheBuilder builderLarge = new BitmapCacheBuilder(this);
        builderLarge.maxMemoryCachePercentage(25) //
                .cacheLogName("BITMAPS_LARGE") //
                .diskCachePurgeableAfter(DroidUtils.HOUR * 6) //
                .diskCacheDirectoryName("bitmaps_large");

        // build caches and register them in the manager
        buildAndRegisterCache(CacheId.BITMAPS_130, builder130);
        buildAndRegisterCache(CacheId.BITMAPS_LARGE, builderLarge);
    }

    @Override
    public void onLowMemory() {
        // memory caches cleanup when running low
        mCachesManager.clearMemoryCaches();
        super.onLowMemory();
    }

    @Override
    @TargetApi(14)
    public void onTrimMemory(int level) {
        switch (level) {
            // memory caches cleanup when trimming memory
            case TRIM_MEMORY_COMPLETE:
            case TRIM_MEMORY_RUNNING_CRITICAL:
                mCachesManager.clearMemoryCaches();
                break;
        }
        super.onTrimMemory(level);
    }

    @NonNull
    public ContentProxy getCache(@NonNull CacheId id) {
        return mCachesManager.getContent(id);
    }

    @NonNull
    public BaseCachesManager<CacheId> getCachesManager() {
        return mCachesManager;
    }

    private void buildAndRegisterCache(@NonNull CacheId cacheId, @NonNull BitmapCacheBuilder builder) {
        try {
            final BitmapCache cache = builder.build();
            mCachesManager.registerContent(cacheId, cache);
        } catch (IOException e) {
            LogUtils.logException(e);
        }
    }

} public enum CacheId {
    BITMAPS_130, // cache for 130x130 images
    BITMAPS_LARGE // cache for large images
} @Immutable
@JsonIgnoreProperties(ignoreUnknown = true)
public class Photo extends JsonModel {

    private static final String PHOTO_ID = "photo_id";
    private static final String PHOTO_URL = "src";

    private final String photo_id;
    private final String photo_url;

    @JsonCreator
    public Photo(@JsonProperty(PHOTO_ID) String photo_id, @JsonProperty(PHOTO_URL) String photo_url) {
        this.photo_id = photo_id;
        this.photo_url = photo_url;
    }

    @JsonProperty(PHOTO_ID)
    public String getPhotoId() {
        return photo_id;
    }

    @JsonProperty(PHOTO_URL)
    public String getPhotoUrl() {
        return photo_url;
    }

} @Immutable
public class PhotosList extends JsonModel {

    private static final String DATA = "data";

    private final ImmutableList<Photo> photos;

    @JsonCreator
    @SuppressWarnings("unchecked")
    public PhotosList(@JsonProperty(DATA) List<Photo> photos) {
        this.photos = photos != null ? ImmutableList.copyOf(photos) : EMPTY_LIST;
    }

    @NonNull
    @JsonProperty(DATA)
    public ImmutableList<Photo> getPhotos() {
        return photos;
    }

} @Beta
@Immutable
public class DroidUtils {

    public static final String SMS_BODY_EXTRA = "sms_body";

    /* Time utils: conversions from seconds to other time units */
    public static final int MINUTE = 60;
    public static final int HOUR = 60 * MINUTE;
    public static final int DAY = 24 * HOUR;
    public static final int WEEK = 7 * DAY;
    public static final int MONTH = 30 * DAY;
    public static final int YEAR = 12 * MONTH;

    /**
     * Statically initialized constant that represents the number of total CPU cores in the device.
     * This number can be higher than the one returned by {@link Runtime.getRuntime().availableProcessors()},
     * as the latter returns the number of active processors that can be used by the current virtual
     * machine (and it's used as a fallback method when it's impossible to access the
     * <i>/sys/devices/system/cpu</i> folder, where this information is retrieved).
     *
     * See {@link http ://makingmoneywithandroid.com/forum/showthread.php?tid=298&pid=1663} for more
     * information on the method used.
     */
    public static final int CPU_CORES;

    static {
        CPU_CORES = getDeviceCpuCores();
    }

    private DroidUtils() {
        // hidden constructor, no instantiation needed
    }

    /**
     * Gets the number of cores available in this device, across all processors. Requires: Ability
     * to access the filesystem at "/sys/devices/system/cpu"
     *
     * @return The number of cores, or the value of {@link Runtime.getRuntime().availableProcessors()}
     * if failed to get result
     */
    private static int getDeviceCpuCores() {
        // private class to display only CPU devices in the directory listing
        class CpuFilter implements FileFilter {
            @Override
            public boolean accept(File pathname) {
                // Check if filename is "cpu", followed by a single digit number
                return Pattern.matches("cpu[0-9]", pathname.getName());
            }
        }
        try {
            // Get directory containing CPU info
            File dir = new File("/sys/devices/system/cpu/");
            // Filter to only list the devices we care about
            File[] files = dir.listFiles(new CpuFilter());
            // Return the number of cores (virtual CPU devices)
            final int cpuCount = files.length;
            if (cpuCount > 0) {
                return cpuCount;
            }
        } catch (Exception e) {
            // falls back to Runtime.getRuntime().availableProcessors()
        }
        return Runtime.getRuntime().availableProcessors();
    }

    /**
     * Returns a recommended size for thread pools that execute CPU-bound operations. The current
     * implementation just returns the value of the runtime method {@code
     * Runtime.getRuntime().availableProcessors()}, avoiding to add the common <i>+ 1</i> to avoid
     * putting too much overhead on the Android Dalvik threads scheduling system.
     */
    public static int getCpuBoundPoolSize() {
        return Runtime.getRuntime().availableProcessors();
    }

    /**
     * Returns a recommended size for thread pools that execute IO-bound operations (mostly network
     * requests, rather than disk accesses).
     *
     * You are guaranteed that this method will always return more than 1 and at least 1 more than
     * the {@link #getCpuBoundPoolSize()}.
     *
     * This trivial implementation just doubles the number returned by {@link
     * #getCpuBoundPoolSize()}.
     */
    public static int getIOBoundPoolSize() {
        return (getCpuBoundPoolSize() * 2);
    }

    /**
     * Returns the upper memory usage limit in bytes for the current application retrieved from
     * {@link ActivityManager#getMemoryClass()}. Using a percentage of this value is a reliable way
     * to limit memory caches.
     *
     * @param context The {@link Context} to retrieve the {@link ActivityManager}
     * @return The application memory class in bytes
     */
    public static int getApplicationMemoryClass(@NonNull Context context) {
        final ActivityManager manager = (ActivityManager) context
                .getSystemService(Context.ACTIVITY_SERVICE);
        return manager.getMemoryClass() * 1024 * 1024; // to bytes
    }

    /**
     * Retrieves the {@link DisplayMetrics} for the device's default display.
     *
     * @param context The {@link Context} to retrieve the {@link WindowManager}
     * @return The retrieved {@link DisplayMetrics}
     */
    @NonNull
    public static DisplayMetrics getDefaultDisplayMetrics(@NonNull Context context) {
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        final DisplayMetrics metrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(metrics);
        return metrics;
    }

    /**
     * Retrieves the screen size category for the device's default display from the {@link
     * Configuration} values.
     *
     * @param context The {@link Context} to use to retrieve the {@link WindowManager}
     * @return The retrieved screen size configuration, can be one of {@link
     * Configuration#SCREENLAYOUT_SIZE_SMALL}, {@link Configuration#SCREENLAYOUT_SIZE_NORMAL},
     * {@link Configuration#SCREENLAYOUT_SIZE_LARGE}, {@link Configuration#SCREENLAYOUT_SIZE_XLARGE}
     * (<b>use the int 4 for this configuration, constant available only from API 11</b>)
     */
    public static int getScreenSize(@NonNull Context context) {
        final Configuration conf = context.getResources().getConfiguration();
        return conf.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
    }

    /**
     * Returns whether the SDK version is the given level or higher.
     *
     * @see android.os.Build.VERSION_CODES
     * @see {@link AndroidUtils#isMinimumSdkLevel(int)}
     */
    public static boolean isMinimumSdkLevel(int minimumSdkLevel) {
        return Build.VERSION.SDK_INT >= minimumSdkLevel;
    }

    /**
     * Generates an unique, stable UID that identifies the device where the user is currently
     * logged.
     *
     * Requires the <code>READ_PHONE_STATE</code> permission in the Manifest.
     *
     * @param context A {@link Context} to retrieve data from the {@link TelephonyManager} and
     *                {@link ContentResolver}
     * @param appId   An unique and possibly secret app ID to "secure" the generated value
     * @return An alphanumeric (+ hyphens) {@link UUID} string
     */
    @NonNull
    public static String getDeviceUniqueIdentificator(@NonNull Context context,
                                                      @NonNull String appId) {
        String telephonyId = null;
        String androidId = null;

        if (hasTelephony(context)) {
            final TelephonyManager telephony = (TelephonyManager) context
                    .getSystemService(Context.TELEPHONY_SERVICE);
            telephonyId = telephony.getDeviceId();
        }
        if (telephonyId == null) {
            telephonyId = "";
        }

        if (Build.VERSION.SDK_INT >= 9) { // Build.VERSION_CODES.GINGERBREAD
            androidId = getAndroidBuildSerial();
        } else {
            androidId = android.provider.Settings.Secure.getString(context.getContentResolver(),
                    android.provider.Settings.Secure.ANDROID_ID);
        }
        // if ("9774d56d682e549c".equals(androidId)) { // broken Android ID
        // FIXME: fallback
        // }
        if (androidId == null) {
            androidId = "";
        }

        UUID deviceUuid = new UUID(androidId.hashCode(), ((long) telephonyId.hashCode() << 32)
                | appId.hashCode());

        return deviceUuid.toString();
    }

    /**
     * Returns the {@link #android.os.Build.SERIAL} string.<br> Not guaranteed to be unique and not
     * null, only available from API 9.
     */
    @TargetApi(9)
    private static String getAndroidBuildSerial() {
        return android.os.Build.SERIAL;
    }

    /**
     * Checks if the device has the {@link PackageManager#FEATURE_TELEPHONY}.
     *
     * @param context
     * @return true if the telephony is available, false otherwise
     */
    public static boolean hasTelephony(@NonNull Context context) {
        final PackageManager manager = context.getPackageManager();
        return manager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
    }

    /**
     * Sends an SMS message to the given number with the passed text.
     *
     * FIXME: SMS text is not displayed in Motorola devices
     *
     * Note: if the passed {@link Context} is not an activity, the flag {@link
     * Intent#FLAG_ACTIVITY_NEW_TASK} will automatically be set to avoid an Android runtime
     * exception.
     *
     * @param context The {@link Context} to start the intent with
     * @param number  The phone number to send the SMS to
     * @param text    The preset text for the SMS
     * @return true if the SMS composer has been opened, false if telephony was not available on the
     * device or the phone number wasn't in the accepted format by {@link
     * PhoneNumberUtils#isWellFormedSmsAddress(String)}. In the latter case, the SMS composer is
     * shown anyway with no recipients.
     */
    public static boolean sendSms(@NonNull Context context, @Nullable String number,
                                  @Nullable String text) {
        boolean success = true;
        if (hasTelephony(context)) {
            if (number == null || !PhoneNumberUtils.isWellFormedSmsAddress(number)) {
                number = ""; // show empty recipient
                success = false;
            }
            // we can send the sms
            Intent smsIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("sms:" + number));
            smsIntent.putExtra(SMS_BODY_EXTRA, text);
            // Intent.EXTRA_TEXT added only as a fallback
            smsIntent.putExtra(Intent.EXTRA_TEXT, text);

            if (!(context instanceof Activity)) {
                smsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            context.startActivity(smsIntent);
            return true;
        } else {
            success = false;
        }
        return success;
    }

    /**
     * FIXME: SMS text is not displayed in Motorola devices
     *
     * As {@link #sendSms(Context, String, String)} but allows to use multiple recipients for the
     * SMS message.
     *
     * @param context The {@link Context} to start the intent with
     * @param numbers An array of recipients phone numbers
     * @param text    The preset text for the SMS
     * @return true if the SMS composer has been opened, false otherwise (for example, because the
     * device has no telephony)
     */
    public static boolean sendSmsToMany(@NonNull Context context, @NonNull String[] numbers,
                                        @Nullable String text) {
        if (numbers.length == 1) { // one recipient only, call sendSms()
            return sendSms(context, numbers[0], text);
        } // more than one recipient
        if (hasTelephony(context)) {
            final List<String> filteredNumbers = new ArrayList<String>(numbers.length);
            for (String number : numbers) {
                if (number != null && PhoneNumberUtils.isWellFormedSmsAddress(number)) {
                    filteredNumbers.add(number);
                }
            }
            // we can send the SMS
            Joiner joiner = Joiner.on(getMultipleSmsJoinOn()).skipNulls();
            Uri smsUri = Uri.parse("sms:" + joiner.join(filteredNumbers));
            Intent smsIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
            smsIntent.putExtra(SMS_BODY_EXTRA, text);
            // Intent.EXTRA_TEXT added only as a fallback
            smsIntent.putExtra(Intent.EXTRA_TEXT, text);

            if (!(context instanceof Activity)) {
                smsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            context.startActivity(smsIntent);
            return true;
        }
        return false;
    }

    @NonNull
    private static String getMultipleSmsJoinOn() {
        final String manufacturer = android.os.Build.MANUFACTURER;
        if (manufacturer.toLowerCase(Locale.US).contains("samsung")) {
            // Samsung requires comma-separated numbers
            return ",";
        } else {
            return ";";
        }
    }

    /**
     * To be used with {@link android.content.Intent.ACTION_SENDTO} to send email, either in plain
     * text or HTML.
     *
     * @param email   The email address to send the message to
     * @param subject The email subject
     * @param body    The email body
     * @return The {@link Uri} for the intent to send the email
     */
    @NonNull
    public static Uri buildEmailUri(String email, String subject, CharSequence body) {
        StringBuilder builder = new StringBuilder();
        builder.append("mailto:").append(email);
        builder.append("?subject=").append(subject);
        builder.append("&body=").append(body);
        String uriText = builder.toString().replace(" ", "%20");
        return Uri.parse(uriText);
    }

    /**
     * Create a chooser intent to open the email composer with the specified single email recipient,
     * subject and message.
     *
     * @param context        The {@link Context} to start the chooser intent with
     * @param chooserMessage The (optional) message to display in the intent chooser
     * @param recipient      The email address to send the message to
     * @param subject        The subject of the message
     * @param message        The email message
     */
    public static void sendEmail(@NonNull Context context, @Nullable String chooserMessage,
                                 @Nullable String recipient, @Nullable String subject, @Nullable String message) {
        sendEmail(context, chooserMessage, new String[]{recipient}, subject, message);
    }

    /**
     * Create a chooser intent to open the email composer with the specified email recipients,
     * subject and message.
     *
     * Note: if the passed {@link Context} is not an activity, the flag {@link
     * Intent#FLAG_ACTIVITY_NEW_TASK} will automatically be set to avoid an Android runtime
     * exception.
     *
     * @param context        The {@link Context} to start the chooser intent with
     * @param chooserMessage The (optional) message to display in the intent chooser
     * @param recipients     The email addresses array to send the message to
     * @param subject        The subject of the message
     * @param message        The email message
     */
    public static void sendEmail(@NonNull Context context, @Nullable String chooserMessage,
                                 @NonNull String[] recipients, @Nullable String subject, @Nullable String message) {
        final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
        emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, recipients);
        emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
        emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message);
        emailIntent.setType("plain/text");
        final Intent intent = Intent.createChooser(emailIntent, chooserMessage);
        if (!(context instanceof Activity)) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        context.startActivity(intent);
    }

    /**
     * Returns an {@link Intent} with action {@link Intent#ACTION_VIEW} to open the Google Play page
     * for the passed package name.
     *
     * @param packageName A full, valid Google Play application package name
     */
    @NonNull
    public static Intent getApplicationMarketPage(@NonNull String packageName) {
        return getViewUrlIntent("market://details?id=" + packageName);
    }

    /**
     * Creates an {@link Intent} to open the passed URI with the default application that handles
     * {@link Intent#ACTION_VIEW} for that content.
     *
     * @param uri The URI string (must be non null)
     * @return The created intent
     */
    @NonNull
    public static Intent getViewUrlIntent(@NonNull String uri) {
        return new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
    }

    /**
     * Checks whether an action has a matching Intent in the current device
     *
     * @param context A Context
     * @param action  The action to check (see {@link Intent} docs
     * @return true if an Intent is available, false otherwise
     */
    public static boolean isIntentAvailable(@NonNull Context context, @NonNull String action) {
        final PackageManager packageManager = context.getPackageManager();
        final Intent intent = new Intent(action);
        final List<ResolveInfo> list = packageManager.queryIntentActivities(intent,
                PackageManager.MATCH_DEFAULT_ONLY);
        return list.size() > 0;
    }

    /**
     * Returns the {@link InputStream} of the given path file using the passed context to retrieve
     * the {@link AssetManager}.
     *
     * @param context The {@link Context} to use to retrieve the assets
     * @param path    The path, relative to the assets root, of the file to retrieve
     * @return The {@link InputStream} of the requested file, or null if an error occurred
     * @throws IOException
     */
    @Nullable
    public static InputStream getAsset(@NonNull Context context, @NonNull String path)
            throws IOException {
        return context.getAssets().open(path);
    }

} @Beta
public class LogUtils {

    @SuppressWarnings("unused")
    private static final String TAG = LogUtils.class.getSimpleName();

    private LogUtils() {
        // hidden constructor, no instantiation needed
    }

    /**
     * Logs a message in LogCat, only if library debugging is active.
     *
     * @param logLevel   The log level to use: must be one of the level constants provided by the
     *                   {@link Log} class.
     * @param logTag
     * @param logMessage
     */
    public static void log(int logLevel, String logTag, String logMessage) {
        if (DroidConfig.DEBUG) {
            switch (logLevel) {
                case Log.ASSERT:
                case Log.VERBOSE:
                    Log.v(logTag, logMessage);
                    break;
                case Log.DEBUG:
                    Log.d(logTag, logMessage);
                    break;
                case Log.INFO:
                    Log.i(logTag, logMessage);
                    break;
                case Log.WARN:
                    Log.w(logTag, logMessage);
                    break;
                case Log.ERROR:
                    Log.e(logTag, logMessage);
                    break;
            }
        }
    }

    /**
     * Prints on LogCat a stack track of an exception, only if library debugging is active.
     *
     * @param t The {@link Throwable} to get the stack trace from
     */
    public static void logException(Throwable t) {
        if (DroidConfig.DEBUG) {
            if (t != null) {
                t.printStackTrace();
            }
        }
    }

    /**
     * Prints on LogCat a log warning message with the exception stack, only if library debugging is
     * active.
     *
     * @param logTag
     * @param logMessage
     * @param t          The {@link Throwable} to get the stack trace from
     */
    public static void logException(String logTag, String logMessage, Throwable t) {
        if (DroidConfig.DEBUG) {
            if (t != null) {
                Log.w(logTag, logMessage, t);
            }
        }
    }

    /**
     * Prints on LogCat a log warning message with the exception's message string, only if library
     * debugging is active.
     *
     * @param t The {@link Throwable} to get the message from
     */
    public static void logExceptionMessage(String logTag, String logMessage, Throwable t) {
        if (DroidConfig.DEBUG) {
            if (t != null) {
                Log.w(logTag,
                        logMessage + "\n" + t.getClass().getSimpleName() + " message: "
                                + t.getMessage());
            }
        }
    }

} @Beta
@ThreadSafe
public abstract class ParallelAsyncTask<Params, Progress, Result> extends
        AsyncTask<Params, Progress, Result> {

    private static final int SDK_INT = android.os.Build.VERSION.SDK_INT;

    @GuardedBy("this")
    // always accessed in the UI thread
    private boolean mParallelExec;

    /**
     * Use this instead of {@link AsyncTask#execute(Object...)} to ensure multiple tasks are
     * executed in the parallel {@link AsyncTask#THREAD_POOL_EXECUTOR} starting from Honeycomb on.
     *
     * If the task is started with execute, an exception will be thrown in {@link #onPreExecute()}.
     */
    public final AsyncTask<Params, Progress, Result> parallelExec(Params... params) {
        mParallelExec = true;
        if (SDK_INT < 11) { // android.os.Build.VERSION_CODES.HONEYCOMB = 11
            return execute(params);
        } else { // Honeycomb and later
            return honeycombExecute(params);
        }
    }

    @TargetApi(11)
    private AsyncTask<Params, Progress, Result> honeycombExecute(Params... params) {
        return executeOnExecutor(THREAD_POOL_EXECUTOR, params);
    }

    /**
     * Always call back to the superclass when overriding.
     *
     * @throws IllegalArgumentException if the caller has used the {@link #execute(Object...)}
     *                                  method instead of {@link #parallelExec(Object...)}.
     */
    @Override
    @CallSuper
    protected void onPreExecute() {
        if (!mParallelExec) {
            throw new IllegalStateException("Task not started with parallel executor");
        }
    }

}
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ListFragment;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.marcosalis.kraken.cache.bitmap.AnimationMode;
import com.github.marcosalis.kraken.cache.bitmap.BitmapCache;
import com.github.marcosalis.kraken.cache.bitmap.BitmapSetterBuilder;
import com.github.marcosalis.kraken.demo.KrakenDemoApplication;
import com.github.marcosalis.kraken.demo.KrakenDemoApplication.CacheId;
import com.github.marcosalis.kraken.demo.R;
import com.github.marcosalis.kraken.demo.models.Photo;
import com.github.marcosalis.kraken.demo.models.PhotosList;
import com.github.marcosalis.kraken.utils.DroidUtils;
import com.github.marcosalis.kraken.utils.android.LogUtils;
import com.github.marcosalis.kraken.utils.android.ParallelAsyncTask;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/*
 * Copyright 2013 Marco Salis - fast3r(at)gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.marcosalis.kraken.demo.fragments;




/**
 * ListFragment that displays a list of images from a {@link BitmapCache}.
 *
 * @author Marco Salis
 * @since 1.0
 */
public class PhotosListFragment extends ListFragment {

    public static final String ARGS_PHOTOS_SIZE = "com.github.marcosalis.kraken.demo.fragments.photos_size";

    public enum PhotosSize {
        SMALL,
        FULL_SCREEN
    }

    private PhotosSize mPhotosSize;
    private ArrayAdapter<Photo> mAdapter;
    private PhotosListTask mPhotosTask;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPhotosSize = (PhotosSize) getArguments().getSerializable(ARGS_PHOTOS_SIZE);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        switch (mPhotosSize) {
            case SMALL:
                mPhotosTask = new PhotosListTask("photos_130x130.json");
                break;
            case FULL_SCREEN:
                final DisplayMetrics metrics = DroidUtils.getDefaultDisplayMetrics(getActivity());
                if (metrics.densityDpi >= DisplayMetrics.DENSITY_HIGH) {
                    mPhotosTask = new PhotosListTask("photos_720x720.json");
                } else {
                    mPhotosTask = new PhotosListTask("photos_480x480.json");
                }
                break;
        }
        mPhotosTask.parallelExec(getActivity().getAssets());
    }

    @Override
    public void onDestroyView() {
        mPhotosTask.cancel(true);
        super.onDestroyView();
    }

    @NonNull
    private ArrayAdapter<Photo> buildAdapter(List<Photo> photos) {
        switch (mPhotosSize) {
            case SMALL:
                mAdapter = new SmallPhotosAdapter(getActivity(), photos);
                break;
            case FULL_SCREEN:
                mAdapter = new FullScreenPhotosAdapter(getActivity(), photos);
                break;
        }
        return mAdapter;
    }

    private class PhotosListTask extends ParallelAsyncTask<AssetManager, Void, PhotosList> {

        private final String mJsonFile;

        public PhotosListTask(String jsonFile) {
            mJsonFile = jsonFile;
        }

        @Override
        protected PhotosList doInBackground(AssetManager... params) {
            try {
                final InputStream stream = params[0].open(mJsonFile);
                final ObjectMapper mapper = new ObjectMapper();
                return mapper.readValue(stream, PhotosList.class);
            } catch (IOException e) {
                LogUtils.logException(e);