package com.datdo.mobilib.util;
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.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Calendar;
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 org.json.JSONObject;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
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.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
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.HandlerThread;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.provider.MediaStore.Images;
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.ViewGroup;
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.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Toast;
import com.datdo.mobilib.base.MblDecorView;
import com.datdo.mobilib.event.MblCommonEvents;
import com.datdo.mobilib.event.MblEventCenter;
import com.datdo.mobilib.event.MblStrongEventListener;
public class MblUtils {
private static final String TAG = getTag(MblUtils.class);
private static final String UTF8 = "UTF-8";
private static float density = 0;
private static float scaledDensity = 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;
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() {
return LayoutInflater.from(getCurrentContext());
}
/**
* <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);
}
}
/**
* Execute the action on a {@link HandlerThread}
* @param handler {@link Handler} object bound with {@link HandlerThread} on which action will be executed
*/
public static void executeOnHandlerThread(Handler handler, Runnable action) {
Assert.assertNotNull(action);
Assert.assertNotNull(handler);
if (Looper.myLooper() == handler.getLooper()) {
action.run();
} else {
handler.post(action);
}
}
/**
* <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) {
executeOnHandlerThread(getMainThreadHandler(), action);
}
/**
* <pre>
* Repeat an action every specified milliseconds.
* </pre>
* @param action action to run
* @param delayMillis delay interval in milliseconds
* @return {@link Runnable} object to stop the repeating. Just call its run() method
*/
public static Runnable repeatDelayed(final Runnable action, final long delayMillis) {
if (action == null || delayMillis <= 0) {
return new Runnable() {
@Override
public void run() {}
};
}
final Runnable hookedAction = new Runnable() {
@Override
public void run() {
getMainThreadHandler().postDelayed(this, delayMillis);
action.run();
}
};
getMainThreadHandler().postDelayed(hookedAction, delayMillis);
return new Runnable() {
@Override
public void run() {
getMainThreadHandler().removeCallbacks(hookedAction);
}
};
}
/**
* <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>
* Convert from SP to Pixel.
* </pre>
*/
public static int pxFromSp(int sp) {
if (scaledDensity == 0) {
scaledDensity = getCurrentContext().getResources().getDisplayMetrics().scaledDensity;
}
return (int) (sp * scaledDensity);
}
/**
* <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();
}
/**
* <pre>
* Determine whether Bluetooth is currently turned on.
* </pre>
*/
public static boolean isBluetoothOn() {
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
}
/**
* <pre>Check if current device has phone feature.</pre>
*/
public static boolean hasPhone() {
Context context = MblUtils.getCurrentContext();
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE) {
return false;
}
if (tm.getLine1Number() == null) {
return false;
}
return true;
}
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;
int photoW = 0;
int photoH = 0;
int[] photoSizes = getBitmapSizes(input);
photoW = photoSizes[0];
photoH = photoSizes[1];
if (targetW > 0 || targetH > 0) {
// figure out which way needs to be reduced less
if (photoW > 0 && photoH > 0) {
if (targetW > 0 && targetH > 0) {
scaleFactor = Math.min(photoW / targetW, photoH / targetH);
} else if (targetW > 0) {
scaleFactor = photoW / targetW;
} else if (targetH > 0) {
scaleFactor = photoH / targetH;
}
}
}
// ensure sizes not exceed 4096
final int MAX_SIZE = 4096;
while (true) {
int resultWidth = scaleFactor <= 1 ? photoW : (photoW / scaleFactor);
int resultHeight = scaleFactor <= 1 ? photoH : (photoH / scaleFactor);
if (resultWidth > MAX_SIZE || resultHeight > MAX_SIZE) {
scaleFactor++;
} else {
break;
}
}
// 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.ARGB_8888;
// decode the bitmap
Bitmap bm = decodeBitmap(input, bmOptions);
// ensure bitmap match exact size
if (bm != null && bm.getWidth() > 0 && bm.getHeight() > 0) {
float s = -1;
if (targetW > 0 && targetH > 0) {
if (bm.getWidth() > targetW || bm.getHeight() > targetH) {
s = Math.min(1.0f * targetW / bm.getWidth(), 1.0f * targetH / bm.getHeight());
}
} else if (targetW > 0) {
if (bm.getWidth() > targetW) {
s = 1.0f * targetW / bm.getWidth();
}
} else if (targetH > 0) {
if (bm.getHeight() > targetH) {
s = 1.0f * targetH / bm.getHeight();
}
}
if (s > 0) {
Matrix matrix = new Matrix();
matrix.postScale(s, s);
Bitmap scaledBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
bm.recycle();
bm = scaledBm;
}
}
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>
* Get width and height of bitmap from InputStream.
* </pre>
* @param is the stream
* @return integer array with 2 elements: width and height
*/
public static int[] getBitmapSizes(InputStream is) throws IOException {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
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>
* Clean up view and its children.
* For ImageView, ImageButton: set image to null.
* For all views: set background to null.
* This method is used when an activity/fragment is no longer used.
* </pre>
*/
public static void cleanupView(View view) {
if (view != null) {
if (view instanceof ImageButton) {
ImageButton ib = (ImageButton) view;
ib.setImageDrawable(null);
} else if (view instanceof ImageView) {
ImageView iv = (ImageView) view;
iv.setImageDrawable(null);
}
MblUtils.setBackgroundDrawable(view, null);
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
int size = vg.getChildCount();
for (int i = 0; i < size; i++) {
cleanupView(vg.getChildAt(i));
}
}
}
}
/**
* <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>
* Clear all files and sub-folder of an directory by traversing.
* Note that the top directory will not be deleted.
* </pre>
*/
public static void clearDir(final File dir) {
traverseFile(dir, new TraverseFileCallback() {
@Override
public void onTraverse(File file) {
if (file != dir) {
file.delete();
}
}
});
}
private static void traverseFile(
final File file,
final TraverseFileCallback callback) {
if (file != null && file.exists()) {
if (file.isDirectory()) {
final File[] children = file.listFiles();
for (File c : children) {
traverseFile(c, callback);
}
}
if (callback != null) {
callback.onTraverse(file);
}
}
}
private static interface TraverseFileCallback {
public void onTraverse(File file);
}
/**
* <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 file stored in "assets" folder.
* </pre>
* @param relativePath relative path to asset file
* @return binary data
*/
public static byte[] readAssetFile(String relativePath) throws IOException {
return readStream(getCurrentContext().getAssets().open(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;
}
return readStream(new FileInputStream(file));
}
private static byte[] readStream(InputStream in) throws IOException {
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()) {
try {
sProgressDialog.dismiss();
} catch (Throwable e) {
e.printStackTrace();
}
}
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>
* Convenient method to show confirmation dialog with message, positive button, negative button.
* </pre>
* @param message
* @param positiveButtonText
* @param negativeButtonText
* @param action action to be executed when user press positive button
*/
public static void showConfirm(
final String message,
final String positiveButtonText,
final String negativeButtonText,
final Runnable action) {
executeOnMainThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(getCurrentContext())
.setMessage(message)
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
action.run();
}
})
.setNegativeButton(negativeButtonText, null)
.show();
}
});
}
/**
* <pre>
* Like {@link #showConfirm(String, String, String, Runnable)
* </pre>
*/
public static void showConfirm(
final int messageResId,
final int positiveButtonResId,
final int negativeButtonResId,
final Runnable action) {
showConfirm(
getCurrentContext().getString(messageResId),
getCurrentContext().getString(positiveButtonResId),
getCurrentContext().getString(negativeButtonResId),
action);
}
/**
* <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("", 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);
}
*/
/**
* <pre>
* Kill app.
* Reference: http://stackoverflow.com/questions/6330200/how-to-quit-android-application-programmatically
* </pre>
* @param mainActivityClass {@link Class} object of app 's main activity
*/
public static void closeApp(final Class<? extends Activity> mainActivityClass) {
closeApp(mainActivityClass, null);
}
/**
* <pre>
* Same like {@link #closeApp(Class)}. Allow to run custom action before app being closed.
* </pre>
*/
public static void closeApp(
final Class<? extends Activity> mainActivityClass,
final Runnable beforeCloseAction) {
// start main activity
Context context = getCurrentContext();
Intent intent = new Intent(context, mainActivityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
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();
activity.finish();
if (beforeCloseAction != null) {
beforeCloseAction.run();
}
System.exit(0);
}
}
}, MblCommonEvents.ACTIVITY_CREATED);
}
/**
* Move app to background without killing it.
*/
public static void moveAppToBackground() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
getCurrentContext().startActivity(intent);
}
/**
* <pre>
* Kill app and restart app after 500ms
* </pre>
* @param mainActivityClass {@link Class} object of app 's main activity
*/
public static void restartApp(final Class<? extends Activity> mainActivityClass) {
closeApp(mainActivityClass, new Runnable() {
@Override
public void run() {
Context context = MblUtils.getCurrentContext();
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
1424287352,
new Intent(context, mainActivityClass),
PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 500, pendingIntent);
}
});
}
/**
* <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 MblLinkRecognizer.isLink(s);
}
/**
* <pre>
* Determine whether a {@link String} object is an email address.
* </pre>
*/
public static boolean isEmail(String s) {
return MblLinkRecognizer.isEmail(s);
}
/**
* <pre>
* Determine whether a {@link String} object is a web url.
* </pre>
*/
public static boolean isWebUrl(String s) {
return MblLinkRecognizer.isWebUrl(s);
}
/**
* <pre>
* Determine whether a {@link String} object is a phone number.
* </pre>
*/
public static boolean isPhone(String s) {
return MblLinkRecognizer.isPhone(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 MblLinkRecognizer.lowerCaseHttpxPrefix(link);
}
/**
* <pre>
* Open other app to view a web url.
* </pre>
*/
public static void openWebUrl(String link) {
if (isEmpty(link) || !isWebUrl(link)) {
return;
}
link = lowerCaseHttpxPrefix(link);
if (!link.startsWith("http")) {
link = "http://" + link;
if (!isWebUrl(link)) {
return;
}
}
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
getCurrentContext().startActivity(browserIntent);
}
private static final String URI_FILE_PREFIX = "file://";
private static final String URI_CONTENT_PREFIX = "content://";
/**
* <pre>
* Extract file path or URL from URI
* </pre>
* @param uri {@link Uri} object to extract
* @return {@link File} object if URI contains file path, {@link URL} object if URI contains URL, otherwise return NULL
*/
public static Object extractUri(Uri uri) {
String uriString = uri.toString();
if (uriString != null && uriString.startsWith(URI_FILE_PREFIX)) {
try {
String filePath = URLDecoder.decode(uriString.substring(URI_FILE_PREFIX.length()), UTF8);
return new File(filePath);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to extract file path from Uri", e);
return null;
}
}
if (uriString != null && uriString.startsWith(URI_CONTENT_PREFIX)) {
Cursor cursor = getCurrentContext().getContentResolver().query(uri, new String[] { Images.Media.DATA }, null, null, null);
String filePath = null;
if (cursor != null && cursor.moveToFirst()) {
int colIndex = cursor.getColumnIndex(Images.Media.DATA);
if (colIndex >= 0) {
filePath = cursor.getString(colIndex);
}
}
// file
if (!MblUtils.isEmpty(filePath)) {
return new File(filePath);
}
// url
String encodedPath = uri.getEncodedPath();
if (!MblUtils.isEmpty(encodedPath)) {
String[] splitted = encodedPath.split("/");
for (String token : splitted) {
if (MblUtils.isEmpty(token)) {
continue;
}
String url;
try {
url = URLDecoder.decode(token, UTF8);
} catch (UnsupportedEncodingException e1) {
continue;
}
if (isWebUrl(url)) {
try {
return new URL(url);
} catch (MalformedURLException e) {}
}
}
}
}
Log.d(TAG, "Invalid Uri: " + uriString);
return null; // invalid URI
}
/**
* <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 {
// load
Bitmap bm = loadBitmapMatchSpecifiedSize(maxSizeLimit, maxSizeLimit, path);
// write bitmap to file
scaledImagePath = MblUtils.getCacheAsbPath(UUID.randomUUID().toString() + ".jpg");
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 byte array in async thread, then set bitmap data to {@link ImageView} object in main thread.
* Also support scaling to specific sizes.
* </pre>
* @param bmData bitmap byte array data
* @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 byte[] bmData,
final ImageView imageView,
final int width,
final int height,
final MblLoadBitmapForImageViewCallback callback) {
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
try {
final Bitmap bm = MblUtils.loadBitmapMatchSpecifiedSize(width, height, bmData);
MblUtils.executeOnMainThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bm);
if (callback != null) {
if (bm != null) {
callback.onSuccess();
} else {
callback.onError();
}
}
}
});
} catch (Throwable e) {
Log.e(TAG, "Error occurred when loading bitmap for image view", e);
if (callback != null) {
MblUtils.executeOnMainThread(new Runnable() {
@Override
public void run() {
callback.onError();
}
});
}
}
}
});
}
/**
* <pre>
* Load bitmap from file in async thread, then set bitmap data to {@link ImageView} object in main thread.
* 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) {
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
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 (Throwable 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();
}
});
}
}
}
});
}
/**
* <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();
}
/**
* <pre>
* Asynchronously create a scaled bitmap file from an existing bitmap file.
* </pre>
* @param path path to existing bitmap file
* @param toWidth scale to width
* @param toHeight scale to height
* @param compressFormat output format
* @param callback callback to received path to output file
*/
public static void createScaledBitmapFile(
final String path,
final int toWidth,
final int toHeight,
final CompressFormat compressFormat,
final MblCreateScaledBitmapFileCallback callback) {
if (callback == null) {
return;
}
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
try {
// if size is already matched, just return current path
int[] sizes = MblUtils.getBitmapSizes(path);
if (sizes[0] == toWidth && sizes[1] == toHeight) {
callback.onSuccess(path);
return;
}
// load bitmap with specific size and save to file
Bitmap bm = MblUtils.loadBitmapMatchSpecifiedSize(toWidth, toHeight, path);
if (bm != null) {
String newPath = MblUtils.getCacheAsbPath(UUID.randomUUID().toString() + ".jpg");
OutputStream os = new FileOutputStream(newPath);
bm.compress(compressFormat, 100, os);
os.flush();
os.close();
bm.recycle();
new File(path).delete();
callback.onSuccess(newPath);
} else {
callback.onError();
}
} catch (Exception e) {
Log.e(TAG, "Failed to create scaled bitmap", e);
callback.onError();
}
}
});
}
public static interface MblCreateScaledBitmapFileCallback {
public void onSuccess(String path);
public void onError();
}
/**
* <pre>Create {@link Calendar} from milliseconds since 1970</pre>
*/
public static Calendar msToCalendar(long ms) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(ms);
return cal;
}
/**
* <pre>Get index of an object in an array.</pre>
*/
public static int indexOf(Object[] arr, Object o) {
if (MblUtils.isEmpty(arr) || o == null) {
return -1;
}
for (int i = 0; i < arr.length; i++) {
if (o.equals(arr[i])) {
return i;
}
}
return -1;
}
private static final Character HANKAKU_SPACE = ' ';
private static final Character ZENKAKU_SPACE = ' ';
private static final Character NEW_LINE = '\n';
private static boolean isTrimmable(Character c) {
return HANKAKU_SPACE.equals(c) || ZENKAKU_SPACE.equals(c) || NEW_LINE.equals(c);
}
/**
* <pre>Method {@link String#trim()} doesn't care Japanese full-width space. Therefore, I added this method to solve that problem.</pre>
*/
public static String trim(String text) {
if (MblUtils.isEmpty(text)) {
return "";
}
int charCount = text.length();
// trim left
int start = 0;
for (int i = 0; i < charCount; i++) {
Character c = text.charAt(i);
if (isTrimmable(c)) {
start++;
} else {
break;
}
}
// trim right
int end = charCount;
for (int i = charCount-1; i >= 0; i--) {
Character c = text.charAt(i);
if (isTrimmable(c)) {
end--;
} else {
break;
}
}
if (start == 0 && end == charCount) {
return text;
} else {
if (start >= end) {
return "";
} else {
return text.substring(start, end);
}
}
}
/**
* <pre>Get {@link String} field from {@link JSONObject} instance, return <code>null</code> instead of "null" when field is null</pre>
* @param jo {@link JSONObject} instance
* @param field field name
* @return {@link String} value
*/
public static String getJSONObjectString(JSONObject jo, String field) {
if (jo.isNull(field)) {
return null;
} else {
return jo.optString(field, null);
}
}
/**
* <pre>
* Crop bitmap from rectangle to square. If bitmap is already a square, just return original bitmap
* </pre>
*/
public static Bitmap createSquareCroppedBitmap(Bitmap bitmap) {
if (bitmap.getWidth() == bitmap.getHeight()) {
return bitmap;
}
int minSize = Math.min(bitmap.getWidth(), bitmap.getHeight());
return Bitmap.createBitmap(
bitmap,
(bitmap.getWidth() - minSize) / 2,
(bitmap.getHeight() - minSize) / 2,
minSize,
minSize);
}
/**
* <pre>
* Crop bitmap from rectangle to circle.
* The cropping is done via 2 croppings: rectangle -> square -> circle.
* </pre>
*/
public static Bitmap createCircleCroppedBitmap(Bitmap bitmap) {
Bitmap squareBitmap = createSquareCroppedBitmap(bitmap);
Bitmap output = Bitmap.createBitmap(
squareBitmap.getWidth(),
squareBitmap.getHeight(),
Bitmap.Config.ARGB_8888);
Paint paint = new Paint();
paint.setAntiAlias(true);
Canvas canvas = new Canvas(output);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(
squareBitmap.getWidth() / 2,
squareBitmap.getHeight() / 2,
squareBitmap.getWidth() / 2,
paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
Rect rect = new Rect(0, 0, squareBitmap.getWidth(), squareBitmap.getHeight());
canvas.drawBitmap(squareBitmap, rect, rect, paint);
if (squareBitmap != bitmap) {
squareBitmap.recycle();
}
return output;
}
}