Android Open Source - mobilib Mbl Utils






From Project

Back to project page mobilib.

License

The source code is released under:

MIT License

If you think the Android project mobilib listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.datdo.mobilib.util;
/*from  w  ww. j  a  va 2  s. co m*/
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;

import junit.framework.Assert;

import org.json.JSONArray;

import com.datdo.mobilib.base.MblDecorView;
import com.datdo.mobilib.event.MblCommonEvents;
import com.datdo.mobilib.event.MblEventCenter;
import com.datdo.mobilib.event.MblStrongEventListener;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.ExifInterface;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.provider.Settings.Secure;
import android.telephony.TelephonyManager;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Toast;

public class MblUtils {
    private static final String TAG = getTag(MblUtils.class);
    private static float density = 0;
    private static final String EMAIL_TYPE = "message/rfc822";

    private static Handler sMainThread = new Handler(Looper.getMainLooper());
    private static Map<String, Object> sCommonBundle = new ConcurrentHashMap<String, Object>();

    private static SharedPreferences sPrefs;
    private static Context sCurrentContext;
    private static LayoutInflater sLayoutInflater;

    public static void init(Context context) {
        sCurrentContext = context;
    }

    /**
     * <pre>
     * Get {@link Handler} for main thread.
     * </pre>
     */
    public static Handler getMainThreadHandler() {
        return sMainThread;
    }

    /**
     * <pre>
     * Get default {@link SharedPreferences} of the app.
     * </pre>
     */
    public static SharedPreferences getPrefs() {
        if (sPrefs == null) {
            sPrefs = PreferenceManager.getDefaultSharedPreferences(getCurrentContext());
        }
        return sPrefs;
    }

    /**
     * <pre>
     * Get current context of the app. This method resolves the inconvenience of Android which requires context for most of its API.
     * If no activity is resumed, this method returns application context. Otherwise, this method returns last resumed activity.
     * </pre>
     */
    public static Context getCurrentContext() {
        return sCurrentContext;
    }

    public static void setCurrentContext(Context context) {
        sCurrentContext = context;
    }

    /**
     * <pre>
     * Get {@link Locale} from device 's configuration.
     * Return {@link Locale#JAPAN} if configuration is not found.
     * </pre>
     */
    public static Locale getLocale() {
        if (sCurrentContext != null) {
            return sCurrentContext.getResources().getConfiguration().locale;
        } else {
            return Locale.JAPAN;
        }
    }

    /**
     * <pre>
     * Get {@link LayoutInflater} instance which is essential for adapters.
     * </pre>
     */
    public static LayoutInflater getLayoutInflater() {
        if (sLayoutInflater == null) {
            sLayoutInflater = LayoutInflater.from(getCurrentContext());
        }
        return sLayoutInflater;
    }

    /**
     * <pre>
     * Execute the action in a thread which is not main thread.
     * If current thread is not main thread, execute the action immediately.
     * Otherwise, create new {@link AsyncTask} to execute the action. {@link AsyncTask} is created using {@link AsyncTask#THREAD_POOL_EXECUTOR}.
     * If max number of threads exceeds, wait 1000 milliseconds and call this method again to ensure that the action will be executed.
     * </pre>
     */
    @SuppressLint("NewApi")
    public static void executeOnAsyncThread(final Runnable action) {
        Assert.assertNotNull(action);
        if (!MblUtils.isMainThread()) {
            action.run();
            return;
        }
        MblAsyncTask task = new MblAsyncTask() {
            @Override
            protected Void doInBackground(Void... params) {
                action.run();
                return null;
            }
        };
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            } else {
                task.execute();
            }
        } catch (RejectedExecutionException e) {
            Log.e(TAG, "Fail to execute on async thread", e);
            getMainThreadHandler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    executeOnAsyncThread(action);
                }
            }, 1000);
        }
    }

    /**
     * <pre>
     * Execute the action in main thread.
     * If current thread is main thread, action is executed immediately.
     * Otherwise, post the action to main thread 's looper to execute later.
     * </pre>
     */
    public static void executeOnMainThread(Runnable action) {
        Assert.assertNotNull(action);
        if (MblUtils.isMainThread()) {
            action.run();
            return;
        }
        Context context = getCurrentContext();
        if (context != null && context instanceof Activity) {
            ((Activity)context).runOnUiThread(action);
        } else {
            sMainThread.post(action);
        }
    }

    /**
     * <pre>
     * Put an object to temporary bundle to transfer data between objects (typically between activities)
     * This method resolves inconvenience of {@link Intent} which does not allow to put any data into its extra.
     * </pre>
     */
    public static void putToCommonBundle(String key, Object value) {
        if (key != null) {
            sCommonBundle.put(key, value);
        }
    }

    /**
     * <pre>
     * Like {@link #putToCommonBundle(String, Object)} but does not require a key. The key is generated uniquely and returned.
     * </pre>
     */
    public static String putToCommonBundle(Object value) {
        String key = UUID.randomUUID().toString();
        sCommonBundle.put(key, value);
        return key;
    }

    /**
     * <pre>
     * Get data stored in temporary bundle by {@link #putToCommonBundle(Object)} and {@link #putToCommonBundle(String, Object)}.
     * This method is not recommended because it is exposed to potential memory leaks. {@link #removeFromCommonBundle(String)} is recommended.
     * </pre>
     */
    @Deprecated
    public static Object getFromCommonBundle(String key) {
        if (key != null) {
            return sCommonBundle.get(key);
        } else {
            return null;
        }
    }

    /**
     * <pre>
     * Link {@link #getFromCommonBundle(String)} but remove the data from temporary bundle right away.
     * </pre>
     */
    public static Object removeFromCommonBundle(String key) {
        if (key != null) {
            return sCommonBundle.remove(key);
        } else {
            return null;
        }
    }

    /**
     * <pre>
     * Show keyboard, typically in an activity having {@link EditText}.
     * </pre>
     * @param focusedView typically an {@link EditText}
     */
    public static void showKeyboard(View focusedView) {
        focusedView.requestFocus();
        InputMethodManager inputMethodManager = ((InputMethodManager)getCurrentContext().getSystemService(Context.INPUT_METHOD_SERVICE));
        inputMethodManager.showSoftInput(focusedView, InputMethodManager.SHOW_FORCED);
    }

    /**
     * <pre>
     * Hide keyboard.
     * </pre>
     */
    public static void hideKeyboard() {
        Activity activity = (Activity) getCurrentContext();
        View currentFocusedView = activity.getCurrentFocus();
        if (currentFocusedView != null) {
            InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(currentFocusedView.getWindowToken(), 0);
        }
    }

    /**
     * <pre>
     * Get name of a class.
     * </pre>
     */
    @SuppressWarnings("rawtypes")
    public static String getTag(Class c) {
        return c.getSimpleName();
    }

    /**
     * <pre>
     * Convert from DP to Pixel.
     * </pre>
     */
    public static int pxFromDp(int dp) {
        if (density == 0) {
            density = getCurrentContext().getResources().getDisplayMetrics().density;
        }
        return (int) (dp * density);
    }

    /**
     * <pre>
     * Determine whether current thread is main thread.
     * </pre>
     */
    public static boolean isMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }

    /**
     * <pre>
     * Determine whether current orientation is portrait.
     * </pre>
     */
    public static boolean isPortraitDisplay() {
        return getCurrentContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
    }

    /**
     * <pre>
     * Determine whether network is currently connected.
     * </pre>
     */
    public static boolean isNetworkConnected() {
        ConnectivityManager conMan = (ConnectivityManager) getCurrentContext().getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = conMan.getActiveNetworkInfo();
        return activeNetwork != null && activeNetwork.isConnected();
    }

    static {
        MblEventCenter.addListener(new MblStrongEventListener() {
            @Override
            public void onEvent(Object sender, String name, Object... args) {
                if (MblCommonEvents.GO_TO_BACKGROUND.equals(name)) {
                    sAppInForeGround = false;
                } else if (MblCommonEvents.GO_TO_FOREGROUND.equals(name)) {
                    sAppInForeGround = true;
                }
            }
        }, new String[] {
                MblCommonEvents.GO_TO_BACKGROUND,
                MblCommonEvents.GO_TO_FOREGROUND
        });
    }

    private static boolean sAppInForeGround = false;

    /**
     * <pre>
     * Determine whether app is in foreground.
     * </pre>
     */
    public static boolean isAppInForeGround() {
        return sAppInForeGround;
    }

    /**
     * <pre>
     * Determine whether keyboard is shown.
     * </pre>
     */
    public static boolean isKeyboardOn() {
        return MblDecorView.isKeyboardOn();
    }

    /**
     * <pre>
     * Create {@link Bitmap} object from byte array, scaling to size targetW x targetH.
     * </pre>
     * @param targetW width to scale. Pass a value <= 0 to ignored width
     * @param targetH height to scale. Pass a value <= 0 to ignored height
     * @param bmData bitmap byte array data
     */
    public static Bitmap loadBitmapMatchSpecifiedSize(final int targetW, final int targetH, final byte[] bmData) {
        return new LoadBitmapMatchSpecifiedSizeTemplate<byte[]>() {

            @Override
            public int[] getBitmapSizes(byte[] bmData) {
                return MblUtils.getBitmapSizes(bmData);
            }

            @Override
            public Bitmap decodeBitmap(byte[] bmData, BitmapFactory.Options options) {
                return BitmapFactory.decodeByteArray(bmData, 0, bmData.length, options);
            }

        }.load(targetW, targetH, bmData);
    }

    /**
     * <pre>
     * Create {@link Bitmap} object from file, scaling to size targetW x targetH.
     * Automatically correct orientation.
     * </pre>
     * @param targetW width to scale. Pass a value <= 0 to ignored width
     * @param targetH height to scale. Pass a value <= 0 to ignored height
     * @param path path to file
     */
    public static Bitmap loadBitmapMatchSpecifiedSize(int targetW, int targetH, final String path) {

        try {
            int angle = MblUtils.getImageRotateAngle(path);
            if (angle == 90 || angle == 270) {
                int temp = targetW;
                targetW = targetH;
                targetH = temp;
            }

            Bitmap bitmap = new LoadBitmapMatchSpecifiedSizeTemplate<String>() {

                @Override
                public int[] getBitmapSizes(String path) {
                    try {
                        return MblUtils.getBitmapSizes(path);
                    } catch (IOException e) {
                        return new int[] {0, 0};
                    }
                }

                @Override
                public Bitmap decodeBitmap(String path, BitmapFactory.Options options) {
                    return BitmapFactory.decodeFile(path, options);
                }

            }.load(targetW, targetH, path);

            if (angle != 0) {
                bitmap = MblUtils.correctBitmapOrientation(path, bitmap);
            }

            return bitmap;
        } catch (Exception e) {
            Log.e(TAG, "Failed to load bitmap: targetW=" + targetW + ", targetW=" + targetW + ", path=" + path);
            return null;
        }
    }

    private static abstract class LoadBitmapMatchSpecifiedSizeTemplate<T> {

        public abstract int[] getBitmapSizes(T input);
        public abstract Bitmap decodeBitmap(T input, BitmapFactory.Options options);

        public Bitmap load(final int targetW, final int targetH, T input) {
            int scaleFactor = 1;

            // get image view sizes
            int photoW = 0;
            int photoH = 0;
            if (targetW > 0 || targetH > 0) {
                // get bitmap sizes
                int[] photoSizes = getBitmapSizes(input);
                photoW = photoSizes[0];
                photoH = photoSizes[1];

                // figure out which way needs to be reduced less
                if (photoW > 0 && photoH > 0) {
                    if (targetW > 0 && targetH > 0) {
                        if ((targetW > targetH && photoW < photoH) || (targetW < targetH && photoW > photoH)) {
                            scaleFactor = Math.max(photoW / targetW, photoH / targetH);
                        } else {
                            scaleFactor = Math.min(photoW / targetW, photoH / targetH);
                        }
                    } else if (targetW > 0) {
                        scaleFactor = photoW / targetW;
                    } else if (targetH > 0) {
                        scaleFactor = photoH / targetH;
                    }
                }
            }

            // set bitmap options to scale the image decode target
            BitmapFactory.Options bmOptions = new BitmapFactory.Options();
            bmOptions.inSampleSize = scaleFactor;
            bmOptions.inPurgeable = true;
            bmOptions.inPreferredConfig = Bitmap.Config.RGB_565;
            bmOptions.inDither = true;

            // decode the bitmap
            Bitmap bm = decodeBitmap(input, bmOptions);
            return bm;
        }
    }

    /**
     * <pre>
     * Get width and height of bitmap from byte array.
     * </pre>
     * @param bmData bitmap binary data
     * @return integer array with 2 elements: width and height
     */
    public static int[] getBitmapSizes(byte[] bmData) {
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(bmData, 0, bmData.length, bmOptions);
        return new int[]{ bmOptions.outWidth, bmOptions.outHeight };
    }

    /**
     * <pre>
     * Get width and height of bitmap from resource.
     * </pre>
     * @param resId resource id of bitmap
     * @return integer array with 2 elements: width and height
     */
    public static int[] getBitmapSizes(int resId) {
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getCurrentContext().getResources(), resId, bmOptions);
        return new int[]{ bmOptions.outWidth, bmOptions.outHeight };
    }

    /**
     * <pre>
     * Get width and height of bitmap from file.
     * </pre>
     * @param path path to file
     * @return integer array with 2 elements: width and height
     */
    public static int[] getBitmapSizes(String path) throws IOException {
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        FileInputStream is = new FileInputStream(path);
        BitmapFactory.decodeStream(is, null, bmOptions);
        is.close();
        return new int[] { bmOptions.outWidth, bmOptions.outHeight };
    }

    /**
     * <pre>
     * Recycle a {@link Bitmap} object
     * </pre>
     * @return true if bitmap was recycled successfully
     */
    public static boolean recycleBitmap(Bitmap bm) {
        if (bm != null && !bm.isRecycled()) {
            bm.recycle();
            return true;
        }
        return false;
    }

    /**
     * <pre>
     * Recycle bitmap rendered by {@link ImageView}.
     * </pre>
     * @return true if bitmap was recycled successfully
     */
    public static boolean recycleImageView(ImageView imageView) {
        Bitmap bm = extractBitmap(imageView);
        imageView.setImageBitmap(null);
        return recycleBitmap(bm);
    }

    /**
     * <pre>
     * Extract {@link Bitmap} object rendered by {@link ImageView}
     * </pre>
     */
    public static Bitmap extractBitmap(ImageView imageView) {
        if (imageView == null) return null;
        Drawable drawable = imageView.getDrawable();
        if (drawable != null && drawable instanceof BitmapDrawable) {
            Bitmap bm = ((BitmapDrawable)drawable).getBitmap();
            return bm;
        }
        return null;
    }

    /**
     * <pre>
     * Check if android:debuggable is set to true
     * </pre>
     */
    public static boolean getAppFlagDebug() {
        ApplicationInfo appInfo = getCurrentContext().getApplicationInfo();
        int appFlags = appInfo.flags;
        boolean b = (appFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
        return b;
    }

    /**
     * <pre>
     * Determine whether byte array is empty or null.
     * </pre>
     */
    public static boolean isEmpty(byte[] data) {
        return data == null || data.length == 0;
    }

    /**
     * <pre>
     * Determine whether object array is empty or null.
     * </pre>
     */
    public static boolean isEmpty(Object[] a) {
        return a == null || a.length == 0;
    }

    /**
     * <pre>
     * Determine whether a {@link String} is empty or null.
     * </pre>
     */
    public static boolean isEmpty(String s) {
        return TextUtils.isEmpty(s);
    }

    /**
     * <pre>
     * Determine whether a {@link Collection} is empty or null.
     * </pre>
     */
    @SuppressWarnings("rawtypes")
    public static boolean isEmpty(Collection c) {
        return c == null || c.isEmpty();
    }

    /**
     * <pre>
     * Determine whether a {@link Map} is empty or null.
     * </pre>
     */
    @SuppressWarnings("rawtypes")
    public static boolean isEmpty(Map m) {
        return m == null || m.isEmpty();
    }

    /**
     * <pre>
     * Determine whether a {@link JSONArray} is empty or null.
     * </pre>
     */
    public static boolean isEmpty(JSONArray a) {
        return a == null || a.length() == 0;
    }

    /**
     * <pre>
     * Determine whether 2 instances of {@link Collection} contain the same set of objects.
     * </pre>
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static boolean equals(Collection c1, Collection c2) {
        if (isEmpty(c1) && isEmpty(c2)) return true;
        if (isEmpty(c1) || isEmpty(c2)) return false;
        if (c1.size() != c2.size()) return false;
        Set s1 = new HashSet(c1);
        Set s2 = new HashSet(c2);
        return s1.containsAll(s2);
    }

    /**
     * <pre>
     * Print a very long {@link String} to logcat by splitting {@link String} object to smaller {@link String} of 4000 characters.
     * </pre>
     */
    public static void logLongString(final String tag, final String str) {
        if(str.length() > 4000) {
            Log.d(tag, str.substring(0, 4000));
            logLongString(tag, str.substring(4000));
        } else {
            Log.d(tag, str);
        }
    }

    /**
     * <prev>
     * Print current stack trace to logcat.
     * </prev>
     */
    public static void logStackTrace(String tag) {
        Log.d(tag, "====================================================");
        logLongString(tag, TextUtils.join("\n", Thread.currentThread().getStackTrace()));
        Log.d(tag, "====================================================");
    }

    /**
     * <pre>
     * Extract domain part of an email address.
     * </pre>
     * @return domain if email is valid, otherwise return null
     */
    public static String extractEmailDomain(String email) {
        String[] splitted = email.split("@");
        return splitted != null && splitted.length == 2 ? splitted[1] : null;
    }

    /**
     * <prev>
     * Get root view of an activity.
     * </prev>
     */
    public static View getRootView(Activity activity) {
        return activity.getWindow().getDecorView().findViewById(android.R.id.content);
    }

    /**
     * <prev>
     * Remove focus on every child views of an activity.
     * </prev>
     */
    public static void focusNothing(Activity activity) {
        focusNothing(getRootView(activity));
    }

    private static void focusNothing(View rootView) {
        rootView.setFocusableInTouchMode(true);
        rootView.requestFocus();
    }

    /**
     * <pre>
     * Get sizes of screen in pixels.
     * </pre>
     * @return interger array with 2 elements: width and height
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    @SuppressWarnings("deprecation")
    public static int[] getDisplaySizes() {
        Context context = getCurrentContext();
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = windowManager.getDefaultDisplay();
        if (Build.VERSION.SDK_INT < 13) {
            return new int[] {display.getWidth(), display.getHeight() };
        } else {
            Point point = new Point();
            display.getSize(point);
            return new int[] { point.x, point.y };
        }
    }

    /**
     * <pre>
     * Get MD5-hashed code for a {@link String}.
     * </pre>
     */
    public static String md5(final String name) {
        try {
            // create MD5 Hash
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(name.getBytes());
            byte messageDigest[] = digest.digest();

            // create hex string
            StringBuilder hexString = new StringBuilder();
            for (byte aMessageDigest : messageDigest) {
                String h = Integer.toHexString(0xFF & aMessageDigest);
                while (h.length() < 2) {
                    h = "0" + h;
                }
                hexString.append(h);
            }

            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Unable to hash name in md5", e);
            return null;
        }
    }

    /**
    private static String encodeFileName(String name) {
        if (name == null) return "default";
        String s = name;
        s = s.replaceAll("/", "_");
        s = s.replaceAll(":", "_");
        s = s.replaceAll("\\?", "_");
        return s;
    }
     **/

    /**
     * <pre>
     * Get absolute path in app 's cache folder from relative path.
     * </pre>
     */
    public static String getCacheAsbPath(String relativePath) {
        File cacheDir = getCurrentContext().getCacheDir();
        return cacheDir.getAbsolutePath().concat("/").concat(relativePath);
    }

    /**
     * <pre>
     * Save byte array to ap 's cache folder.
     * </pre>
     * @param in byte array
     * @param relativePath relative path to destination file
     */
    public static void saveCacheFile(byte[] in, String relativePath) throws IOException {
        saveFile(in, getCacheAsbPath(relativePath));
    }

    /**
     * <pre>
     * Save byte array to arbitrary file.
     * </pre>
     * @param in byte array
     * @param absolutePath absolute path to destination file
     */
    public static void saveFile(byte[] in, String absolutePath) throws IOException {
        File file = new File(absolutePath);
        if (!file.exists()) {
            file.createNewFile();
        }

        FileOutputStream out = new FileOutputStream(absolutePath);
        out.write(in);
        out.close();
    }

    /**
     * <pre>
     * Read binary data from file stored in app 's cache folder.
     * </pre>
     * @param relativePath relative path to source file
     * @return binary data
     */
    public static byte[] readCacheFile(String relativePath) throws IOException {
        return readFile(getCacheAsbPath(relativePath));
    }

    /**
     * <pre>
     * Read binary data from arbitrary file.
     * </pre>
     * @param absolutePath absolute path to source file
     * @return binary data
     */
    public static byte[] readFile(String absolutePath) throws IOException {
        File file = new File(absolutePath);
        if (!file.exists()) {
            return null;
        }

        FileInputStream in = new FileInputStream(file);
        byte[] b = new byte[in.available()];
        in.read(b);
        in.close();

        return b;
    }

    /**
     * <pre>
     * Save binary data to file in app 's internal memory.
     * </pre>
     * @param in byte array
     * @param absolutePath absolute path to destination file
     * @throws IOException
     */
    public static void saveInternalFile(byte[] in, String absolutePath) throws IOException {
        FileOutputStream out = getCurrentContext().openFileOutput(absolutePath, Context.MODE_PRIVATE);
        out.write(in);
        out.close();
    }

    /**
     * <pre>
     * Read binary data from file stored in app 's internal memory.
     * </pre>
     * @param absolutePath absolute path to source file
     * @return binary data
     */
    public static byte[] readInternalFile(String absolutePath) throws IOException {
        FileInputStream in = getCurrentContext().openFileInput(absolutePath);
        byte[] b = new byte[in.available()];
        in.read(b);
        in.close();

        return b;
    }

    /**
     * <pre>
     * Convenient method to create and show alert in main thread.
     * </pre>
     * @param title alert 's title
     * @param message alert 's message
     * @param postTask action to execute after user presses "OK" button
     */
    public static void showAlert(final String title, final String message, final Runnable postTask) {
        executeOnMainThread(new Runnable() { 
            @Override
            public void run() {
                new AlertDialog.Builder(getCurrentContext())
                .setTitle(title)
                .setMessage(message)
                .setNegativeButton(android.R.string.ok, new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        if (postTask != null) getMainThreadHandler().post(postTask);
                    }
                })
                .show();
            }
        });
    }

    /**
     * <pre>
     * Like {@link #showAlert(String, String, Runnable)}
     * </pre>
     */
    public static void showAlert(final int titleResId, final int messageResId, final Runnable postTask) {
        showAlert(
                getCurrentContext().getString(titleResId),
                getCurrentContext().getString(messageResId),
                postTask);
    }

    private static ProgressDialog sProgressDialog;

    /**
     * <pre>
     * Convenient method to create and show progress dialog in main thread.
     * </pre>
     * @param cancelable whether progress dialog can be canceled by pressing back button
     */
    public static void showProgressDialog(final String message, final boolean cancelable) {
        executeOnMainThread(new Runnable() {
            @Override
            public void run() {
                if (sProgressDialog != null && sProgressDialog.isShowing()) {
                    sProgressDialog.dismiss();
                }
                sProgressDialog = new ProgressDialog(getCurrentContext());
                sProgressDialog.setMessage(message);
                sProgressDialog.setCancelable(cancelable);
                sProgressDialog.show();
            }
        });
    }

    /**
     * <pre>
     * Like {@link #showProgressDialog(String, boolean)}
     * </pre>
     */
    public static void showProgressDialog(final int messageResId, final boolean cancelable) {
        showProgressDialog(getCurrentContext().getString(messageResId), cancelable);
    }

    /**
     * <pre>
     * Hide progress dialog shown by {@link #showProgressDialog(int, boolean)} and {@link #showProgressDialog(String, boolean)}
     * </pre>
     */
    public synchronized static void hideProgressDialog() {
        executeOnMainThread(new Runnable() {
            @Override
            public void run() {
                if (sProgressDialog != null && sProgressDialog.isShowing()) {
                    sProgressDialog.hide();
                }
                sProgressDialog = null;
            }
        });
    }

    /**
     * <pre>
     * Convenient method to show toast in main thread.
     * </pre>
     * @param duration {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}
     */
    public static void showToast(final String text, final int duration) {
        executeOnMainThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getCurrentContext(), text, duration).show();
            }
        });
    }

    /**
     * <pre>
     * Like {@link #showToast(String, int)}
     * </pre>
     */
    public static void showToast(int messageResId, int duration) {
        showToast(getCurrentContext().getString(messageResId), duration);
    }

    /**
     * <pre>
     * Remove {@link OnGlobalLayoutListener} object from view 's {@link ViewTreeObserver}, which is different between API < 16 and API >=16.
     * 
     * Here is sample usage:
     * <code>
     * view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
     *     {@literal @}Override
     *     public void onGlobalLayout() {
     *         MblUtils.removeOnGlobalLayoutListener(view, this);
     *         // ...
     *     }
     * });
     * </code>
     * </pre>
     */
    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    public static void removeOnGlobalLayoutListener(View view, OnGlobalLayoutListener listener) {
        if (Build.VERSION.SDK_INT < 16) {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
        } else {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
        }
    }

    /**
     * <pre>
     * Convenient method to send email.
     * </pre>
     * @param subject email 's subject
     * @param emails target email addresses
     * @param text email 's body text
     * @param title message displayed when user selects app to send email
     * @param attachmentFilenames paths to attachment files
     * @return true if email app is opened successfully
     */
    public static boolean sendEmail(
            String subject,
            String[] emails,
            String[] cc,
            String[] bcc,
            Object text,
            String title,
            List<String> attachmentFilenames) {
        Intent intent;
        if (isEmpty(attachmentFilenames)) {
            intent = new Intent(Intent.ACTION_SEND);
        } else {
            intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
        }
        intent.setType(EMAIL_TYPE);
        intent.putExtra(Intent.EXTRA_SUBJECT, subject);
        intent.putExtra(Intent.EXTRA_EMAIL, emails);
        if (!isEmpty(cc)) {
            intent.putExtra(Intent.EXTRA_CC, cc);
        }
        if (!isEmpty(bcc)) {
            intent.putExtra(Intent.EXTRA_BCC, bcc);
        }
        if (text instanceof String) {
            intent.putExtra(Intent.EXTRA_TEXT, (String)text);
        } else if (text instanceof Spanned) {
            intent.putExtra(Intent.EXTRA_TEXT, (Spanned)text);
        }
        if (!isEmpty(attachmentFilenames)) {
            ArrayList<Uri> uris = new ArrayList<Uri>();
            for (String name : attachmentFilenames) {
                uris.add(Uri.fromFile(new File(name)));
            }
            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
        }
        try {
            getCurrentContext()
            .startActivity(Intent.createChooser(intent, title));
        } catch (ActivityNotFoundException e) {
            Log.e(TAG, "Cannot send email", e);
            return false;
        }
        return true;
    }

    /*
    public static void copyAssetFiles(Pattern pattern) throws IOException {
        AssetManager assetManager = getCurrentContext().getAssets();
        String[] fileList = assetManager.list("");
        for (String path : fileList) {
            if (pattern == null || pattern.matcher(path).matches()) {
                copyAssetFile(path, path);
            }
        }
    }
     */

    /*
    public static void copyAssetFile(String src, String dst) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        AssetManager assets = getCurrentContext().getAssets();
        in = assets.open(src);
        out = getCurrentContext().openFileOutput(dst, Context.MODE_PRIVATE);

        copyFile(in, out);
    }
     */

    /**
     * <pre>
     * Convenient method to copy file.
     * </pre>
     * @param in {@link InputStream} of source file
     * @param out {@link OutputStream} of destination file
     */
    public static void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while((read = in.read(buffer)) != -1){
            out.write(buffer, 0, read);
        }

        in.close();
        out.flush();
        out.close();
    }

    /**
     * <pre>
     * Determine whether a {@link MotionEvent} is on a {@link View}
     * </pre>
     */
    public static boolean motionEventOnView(MotionEvent event, View view) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int x = location[0];
        int y = location[1];
        int w = view.getWidth();
        int h = view.getHeight();
        Rect rect = new Rect(x, y, x+w, y+h);
        return rect.contains((int)event.getRawX(), (int)event.getRawY());
    }

    /*
    public static Bitmap loadBitmapFromInternalStorage(String path) {
        if (isEmpty(path)) return null;
        FileInputStream is = null;
        Bitmap bm = null;
        try {
            is = getCurrentContext().openFileInput(path);
            bm = BitmapFactory.decodeStream(is);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "File not found: " + path, e);
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // ignored
            }
        }
        return bm;
    }
     */

    /**
     * <pre>
     * Determine whether an app is installed on device.
     * </pre>
     * @param packageName app 's package name
     */
    public static boolean isAppInstalled(String packageName) {

        if (MblUtils.isEmpty(packageName)) return false;

        PackageManager pm = getCurrentContext().getPackageManager();
        try {
            pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
            return true;
        } catch (NameNotFoundException e) {
            // do nothing
        }
        return false;
    }

    /**
     * <pre>
     * Copy text to clipboard.
     * Different implementation for API < 11 and API >= 11.
     * </pre>
     */
    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    public static void copyTextToClipboard(String text) {
        if (Build.VERSION.SDK_INT < 11) {
            android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getCurrentContext().getSystemService(Context.CLIPBOARD_SERVICE);
            clipboard.setText(text);
        } else {
            android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getCurrentContext().getSystemService(Context.CLIPBOARD_SERVICE);
            clipboard.setPrimaryClip(ClipData.newPlainText("PoketChat Copy To Clipboard", text));
        }
    }

    /**
     * <pre>
     * Start other app by its package name.
     * </pre>
     * @param packageName app 's package name
     */
    public static void openApp(String packageName) {
        Context context = getCurrentContext();
        PackageManager manager = context.getPackageManager();
        Intent intent = manager.getLaunchIntentForPackage(packageName);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addCategory(Intent.CATEGORY_LAUNCHER);
        context.startActivity(intent);
    }

    /**
     * <pre>
     * Open other app to view URL of an app (typically browser or Google Play)
     * </pre>
     * @param downloadUrl
     */
    public static void openDownloadPage(String downloadUrl) {
        Context context = getCurrentContext();
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setData(Uri.parse(downloadUrl));
        context.startActivity(intent);
    }

    /**
     * <pre>
     * Set {@link Drawable} as background of a view, which is different in API < 16 and API >= 16.
     * </pre>
     * @param view target view
     * @param drawable background drawable
     */
    @SuppressLint("NewApi")
    @SuppressWarnings("deprecation")
    public static void setBackgroundDrawable(View view, Drawable drawable) {
        if (view == null) return;

        if (Build.VERSION.SDK_INT >= 16) {
            view.setBackground(drawable);
        } else {
            view.setBackgroundDrawable(drawable); 
        }
    }

    /**
     * <pre>
     * Delete a file stored in app 's internal memory.
     * </pre>
     * @param path absolute path to file
     */
    public static void deleteInternalFile(String path) {
        Context context = getCurrentContext();
        context.deleteFile(path);
    }

    /**
     * <pre>
     * Add "0" to head of number string so that length >= minLength
     * </pre>
     */
    public static String fillZero(String numberString, int minLength) {
        if (numberString == null) numberString = "";

        int diff = minLength - numberString.length();
        for (int i = 0; i < diff; i++) {
            numberString = "0" + numberString;
        }

        return numberString;
    }

    /**
     * <pre>
     * Get rotation angle of an image.
     * This information is stored in image file. Therefore, this method needs path to file, not a {@link Bitmap} object or byte array.
     * </pre>
     * @param imagePath absolute path to image file
     * @return one of 0, 90, 180, 270
     */
    public static int getImageRotateAngle(String imagePath) throws IOException {
        ExifInterface exif = new ExifInterface(imagePath);
        int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
        int angle = 0;
        if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
            angle = 90;
        } 
        else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
            angle = 180;
        } 
        else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
            angle = 270;
        }

        return angle;
    }

    /**
     * <pre>
     * Rotate bitmap to its correct orientation if needed.
     * WARNING: {@link Bitmap} is immutable. Therefore, when new {@link Bitmap} is created, old {@link Bitmap} object is recycled to prevent {@link OutOfMemoryError}.
     * </pre>
     * @param path absolute path to image file
     * @param bm {@link Bitmap} object
     * @return rotated {@link Bitmap} object if angel != 0, otherwise return original {@link Bitmap} object
     */
    public static Bitmap correctBitmapOrientation(String path, Bitmap bm) {
        if (path != null && bm != null) {
            int angle = 0;
            try {
                angle = getImageRotateAngle(path);
                if (angle != 0) {

                    Matrix matrix = new Matrix();
                    matrix.postRotate(angle);
                    Bitmap rotatedBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, false);

                    bm.recycle();
                    bm = rotatedBm;
                }
            } catch (IOException e) {
                Log.e(TAG, "Can not rotate bitmap path: " + path + ", angle:" + angle, e);
            }
        }
        return bm;
    }

    /**
     * <pre>
     * Scroll {@link ListView} to its bottom item.
     * </pre>
     */
    public static void scrollListViewToBottom(final ListView listView) {
        if (listView == null || listView.getAdapter() == null) {
            return;
        }

        final Runnable action = new Runnable() {
            @Override
            public void run() {
                int count = listView.getAdapter().getCount();
                if (count > 0) {
                    listView.setSelectionFromTop(count, -listView.getHeight());
                }
            }
        };

        if (listView.getHeight() > 0) {
            executeOnMainThread(action);
        } else {
            listView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    removeOnGlobalLayoutListener(listView, this);
                    action.run();
                }
            });
        }
    }

    /**
     * <pre>
     * Generate Unique Device ID.
     * 
     * I did some research on the topic "Unique device Id for android", and found some remarkable articles about it:
     *   http://android-developers.blogspot.com/2011/03/identifying-app-installations.html
     *   http://developer.samsung.com/android/technical-docs/How-to-retrieve-the-Device-Unique-ID-from-android-device
     * 
     * These articles propose many solutions for device id:
     *   1 - Phone Device ID (IMEI, MEID, ESN, IMSI) ==> device must be a phone
     *   2 - Serial Number ==> device must be a non-phone device (although some phone devices also have this value)
     *   3 - Mac Address ==> changed frequently (do not use)
     *   4 - ANDROID_ID ==> duplicated on some devices (some Motorola device, Froyo, or custom ROMs...)
     *   5 - Generate a UUID and store it in external storage ==> this is unsafe because user can copy the login_info file and uuid file to other device, do not use
     * 
     * As you can see, none of them is totally reliable. Therefore, I decided to combine 1,2 and 4 to generate a custom device ID which is totally secured in all cases.
     * The drawback is that we need to require for READ_PHONE_STATE permission.
     * </pre>
     * @return the generated unique device ID
     */
    @SuppressLint("NewApi")
    public static String generateDeviceId() {
        Context context = MblUtils.getCurrentContext();
        StringBuilder builder = new StringBuilder();

        // android id
        String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); 
        if (!TextUtils.isEmpty(androidId)) builder.append(androidId);

        // serial
        if (Build.VERSION.SDK_INT >= 9) {
            String serial = Build.SERIAL;
            if (!TextUtils.isEmpty(serial) && !Build.UNKNOWN.equals(serial)) builder.append(serial);
        }

        // phone device id
        TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
        String deviceId = telephonyManager.getDeviceId();
        if (!TextUtils.isEmpty(deviceId)) builder.append(deviceId);

        // combine & hash
        return md5(builder.toString());
    }

    /*
    public static void loadInternalImage(
            final String path,
            final ImageView target) {

        if (TextUtils.isEmpty(path)) return;

        executeOnAsyncThread(new Runnable() {
            @Override
            public void run() {
                final Bitmap bm = loadInternalImage(path);
                executeOnMainThread(new Runnable() {
                    @Override
                    public void run() {
                        target.setImageBitmap(bm);
                    }
                });
            }
        });
    }
     */

    /*
    public static Bitmap loadInternalImage(String path) {
        if (TextUtils.isEmpty(path)) return null;

        FileInputStream is = null;
        Bitmap bm = null;
        try {
            is = getCurrentContext().openFileInput(path);
            bm = BitmapFactory.decodeStream(is);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Can not load image from internal storage: path=" + path, e);
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // ignored
            }
        }
        return bm;
    }
     */

    /*
    public static void copyAssetFileToExternalMemory(String src, String dst) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        AssetManager assets = getCurrentContext().getAssets();
        in = assets.open(src);
        out = new FileOutputStream(dst);

        copyFile(in, out);
    }
     */

    // ref: http://stackoverflow.com/questions/3105673/android-how-to-kill-an-application-with-all-its-activities
    /**
     * <pre>
     * Kill app.
     * </pre>
     * @param mainActivityClass {@link Class} object of app 's main activity
     */
    public static void closeApp(final Class<? extends Activity> mainActivityClass) {

        final Runnable closeAppAction = new Runnable() {
            @Override
            public void run() {
                android.os.Process.killProcess(android.os.Process.myPid());
            }
        };

        Context context = getCurrentContext();
        if (mainActivityClass.isInstance(context)) {
            closeAppAction.run();
        } else {
            // start main activity
            Intent intent = new Intent(context, mainActivityClass);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            context.startActivity(intent);

            // wait until main activity is resumed
            MblEventCenter.addListener(new MblStrongEventListener() {
                @Override
                public void onEvent(Object sender, String name, Object... args) {
                    Activity activity = (Activity) MblEventCenter.getArgAt(0, args);
                    if (activity == null) {
                        return;
                    }
                    if (mainActivityClass.isInstance(activity)) {
                        terminate();
                        closeAppAction.run();
                    }
                }
            }, MblCommonEvents.ACTIVITY_RESUMED);
        }
    }

    /**
     * <pre>
     * Get app 's PackageInfo.
     * </pre>
     */
    public static PackageInfo getAppPackageInfo() {
        try {
            Context context = MblUtils.getCurrentContext();
            String packageName = context.getPackageName();
            return context.getPackageManager().getPackageInfo(packageName, 0);
        } catch (NameNotFoundException e) {
            Log.i(TAG, "Could not get app name and version", e);
        }
        return null;
    }

    /**
     * <pre>
     * Determine whether a {@link String} object is a link.
     * </pre>
     */
    public static boolean isLink(String s) {
        return MblUrlRecognizer.isLink(s);
    }

    /**
     * <pre>
     * Android do not understand prefixes like "HTtP" or "hTtP".
     * Therefore, we need to make all http/https prefixes lower-case
     * </pre>
     */
    public static String lowerCaseHttpxPrefix(String link) {
        return MblUrlRecognizer.lowerCaseHttpxPrefix(link);
    }

    /**
     * <pre>
     * Open other app to view a link.
     * </pre>
     */
    public static void openLink(String link) {
        if (isEmpty(link) || !isLink(link)) {
            return;
        }
        link = lowerCaseHttpxPrefix(link);
        Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
        getCurrentContext().startActivity(browserIntent);
    }

    /**
     * <pre>
     * Get app 's key hash.
     * </pre>
     * @return key hash
     */
    public static String getKeyHash() {
        try {
            Context context = getCurrentContext();
            PackageInfo info = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 
                    PackageManager.GET_SIGNATURES);
            for (Signature signature : info.signatures) {
                MessageDigest md = MessageDigest.getInstance("SHA");
                md.update(signature.toByteArray());
                String keyHash = Base64.encodeToString(md.digest(), Base64.DEFAULT);
                return new String(keyHash);
            }
            Log.e(TAG, "getKeyHash: no signature found");
            return null;
        } catch (Throwable e) {
            Log.e(TAG, "getKeyHash: error occurred", e);
            return null;
        }
    }

    /**
     * <pre>
     * When uploading a file to server, normally client app must scale image so that its sizes {@literal <}= limited size specified by server.
     * This method help you to do that, without worrying about OutOfMemoryError.
     * If OutOfMemoryError occurs, it will retry 2 times more (each after 2 seconds).
     * Note that in case image size is already {@literal <}= limited size, the original path will be returned in callback method.
     * </pre> 
     * @param path absolute path to original image
     * @param maxSizeLimit limited size specified by server
     * @param callback callback to receive result (path to scaled image)
     */
    public static void createImageFileForUpload(
            final String path,
            final int maxSizeLimit,
            final MblCreateImageFileForUploadCallback callback) {

        final int   N_RETRIES   = 3;
        final int[] nRetries    = new int[] { 0 };
        final long  RETRY_AFTER = 2000l;

        MblUtils.executeOnAsyncThread(new Runnable() {
            @Override
            public void run() {

                nRetries[0]++;
                String scaledImagePath = null;

                try {
                    // check size
                    int[] sizes = MblUtils.getBitmapSizes(path);
                    int w = sizes[0];
                    int h = sizes[1];
                    int maxSize = Math.max(w, h);

                    // check if we need to process the bitmap
                    if (maxSize <= maxSizeLimit) {
                        scaledImagePath = path;
                    } else {

                        String filename = new File(path).getName();
                        scaledImagePath = MblUtils.getCacheAsbPath(UUID.randomUUID().toString()) + "-" + filename;

                        // load bitmap with scale factor so that it is closest to maxSizeLimit
                        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
                        bmOptions.inSampleSize = (int) Math.ceil(1.0f * maxSize / maxSizeLimit);
                        bmOptions.inPreferredConfig = Bitmap.Config.RGB_565;
                        bmOptions.inDither = true;
                        Bitmap bm = BitmapFactory.decodeFile(path, bmOptions);

                        // write bitmap to file
                        FileOutputStream os = new FileOutputStream(scaledImagePath);
                        bm.compress(CompressFormat.JPEG, 100, os);
                        os.flush();
                        os.close();
                        bm.recycle();
                    }

                    // return path to generated file
                    if (callback != null) {
                        final String fScaledPath = scaledImagePath;
                        MblUtils.executeOnMainThread(new Runnable() {
                            @Override
                            public void run() {
                                callback.onSuccess(fScaledPath);
                            }
                        });
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Error when creating image file for upload", e);
                    if (scaledImagePath != null && !TextUtils.equals(path, scaledImagePath)) {
                        new File(scaledImagePath).delete();
                    }

                    if (callback != null) {
                        MblUtils.executeOnMainThread(new Runnable() {
                            @Override
                            public void run() {
                                callback.onError();
                            }
                        });
                    }
                } catch (OutOfMemoryError e) {
                    Log.e(TAG, "Error when creating image file for upload", e);
                    if (nRetries[0] < N_RETRIES) {
                        Log.d(TAG, "Retry after " + RETRY_AFTER + " ms");
                        System.gc();
                        final Runnable fThis = this;
                        MblUtils.getMainThreadHandler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                MblUtils.executeOnAsyncThread(fThis);
                            }
                        }, RETRY_AFTER);
                    } else {
                        if (scaledImagePath != null && !TextUtils.equals(path, scaledImagePath)) {
                            new File(scaledImagePath).delete();
                        }

                        if (callback != null) {
                            MblUtils.executeOnMainThread(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onError();
                                }
                            });
                        }
                    }
                }
            }
        });
    }

    /**
     * <pre>
     * Callback to receive result in {@link MblUtils#createImageFileForUpload(String, int, MblCreateImageFileForUploadCallback)}
     * </pre>
     */
    public static interface MblCreateImageFileForUploadCallback {
        /**
         * @param scaledImagePath absolute path to scaled image.
         */
        public void onSuccess(String scaledImagePath);
        public void onError();
    }

    /**
     * <pre>
     * Load bitmap from file in async thread, then set bitmap data to {@link ImageView} object in main thread.
     * If OutOfMemoryError occurs, it will retry 2 times more (after 2 seconds)
     * Also support scaling to specific sizes.
     * </pre>
     * @param path path to image file
     * @param imageView {@link ImageView} object to display image
     * @param width specific width to scale. -1 to ignore
     * @param height specific height to scale. -1 to ignore
     * @param callback callback to receive result
     */
    public static void loadBitmapForImageView(
            final String path,
            final ImageView imageView,
            final int width,
            final int height,
            final MblLoadBitmapForImageViewCallback callback) {

        final int   N_RETRIES   = 3;
        final int[] nRetries    = new int[] { 0 };
        final long  RETRY_AFTER = 2000l;

        MblUtils.executeOnAsyncThread(new Runnable() {
            @Override
            public void run() {

                nRetries[0]++;

                try {

                    final Bitmap bm = MblUtils.loadBitmapMatchSpecifiedSize(width, height, path);

                    MblUtils.executeOnMainThread(new Runnable() {
                        @Override
                        public void run() {
                            imageView.setImageBitmap(bm);
                            if (callback != null) {
                                if (bm != null) {
                                    callback.onSuccess();
                                } else {
                                    callback.onError();
                                }
                            }
                        }
                    });
                } catch (Exception e) {
                    Log.e(TAG, "Error occurred when loading bitmap for image view: path=" + path, e);
                    if (callback != null) {
                        MblUtils.executeOnMainThread(new Runnable() {
                            @Override
                            public void run() {
                                callback.onError();
                            }
                        });
                    }
                } catch (OutOfMemoryError e) {
                    Log.e(TAG, "Out of memory when loading bitmap for image view: path=" + path, e);
                    if (nRetries[0] < N_RETRIES) {
                        Log.d(TAG, "Retry after " + RETRY_AFTER + " ms");
                        System.gc();
                        final Runnable fThis = this;
                        MblUtils.getMainThreadHandler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                MblUtils.executeOnAsyncThread(fThis);
                            }
                        }, RETRY_AFTER);
                    } else {
                        if (callback != null) {
                            MblUtils.executeOnMainThread(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onError();
                                }
                            });
                        }
                    }
                }
            }
        });
    }

    /**
     * <pre>
     * Callback to receive result in {@link MblUtils#loadBitmapForImageView(String, ImageView, int, int, MblLoadBitmapForImageViewCallback)}
     * </pre>
     */
    public static interface MblLoadBitmapForImageViewCallback {
        public void onSuccess();
        public void onError();
    }
}




Java Source Code List

com.datdo.mobilib.api.DBBase.java
com.datdo.mobilib.api.DBHelper.java
com.datdo.mobilib.api.MblApi.java
com.datdo.mobilib.api.MblCache.java
com.datdo.mobilib.api.MblException.java
com.datdo.mobilib.api.MblSSLCertificateUtils.java
com.datdo.mobilib.base.MblActivityPlugin.java
com.datdo.mobilib.base.MblBaseActionBarActivity.java
com.datdo.mobilib.base.MblBaseActivity.java
com.datdo.mobilib.base.MblBaseAdapter.java
com.datdo.mobilib.base.MblBaseApplication.java
com.datdo.mobilib.base.MblBaseFragmentActivity.java
com.datdo.mobilib.base.MblDecorView.java
com.datdo.mobilib.base.MblNetworkStatusChangedReceiver.java
com.datdo.mobilib.event.MblCommonEvents.java
com.datdo.mobilib.event.MblEventCenter.java
com.datdo.mobilib.event.MblEventListener.java
com.datdo.mobilib.event.MblStrongEventListener.java
com.datdo.mobilib.event.MblWeakArrayList.java
com.datdo.mobilib.imageinput.MblAutoResizeSquareImageView.java
com.datdo.mobilib.imageinput.MblDataInputActivity.java
com.datdo.mobilib.imageinput.MblImageFolderScanner.java
com.datdo.mobilib.imageinput.MblImageInput.java
com.datdo.mobilib.imageinput.MblImagePickingScanEngine.java
com.datdo.mobilib.imageinput.MblPickImageActivity.java
com.datdo.mobilib.imageinput.MblPickImageGridViewAdapter.java
com.datdo.mobilib.imageinput.MblTakeImageActivity.java
com.datdo.mobilib.util.MblAsyncTask.java
com.datdo.mobilib.util.MblImageLoader.java
com.datdo.mobilib.util.MblLinkMovementMethod.java
com.datdo.mobilib.util.MblUrlRecognizer.java
com.datdo.mobilib.util.MblUtils.java
com.datdo.mobilib.widget.MblHorizontalViewPager.java
com.datdo.mobilib.widget.MblListViewWithScrollableItems.java
com.datdo.mobilib.widget.MblSequenceImage.java
com.datdo.mobilib.widget.MblSideMenuEnabledLayout.java
com.datdo.mobilib.widget.MblTouchImageView.java