├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── vcs.xml ├── modules.xml ├── gradle.xml ├── compiler.xml └── misc.xml ├── library ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── me │ │ └── zhanghai │ │ └── android │ │ └── androidutil │ │ └── util │ │ ├── GsonUtils.java │ │ ├── ArrayUtils.java │ │ ├── UriImageLoader.java │ │ ├── ToastUtils.java │ │ ├── ColorUtils.java │ │ ├── FileUtils.java │ │ ├── LoadingEmptyHelper.java │ │ ├── BuildUtils.java │ │ ├── MathUtils.java │ │ ├── ShapeDrawableUtils.java │ │ ├── TouchUtils.java │ │ ├── LogUtils.java │ │ ├── UriUtils.java │ │ ├── LocaleUtils.java │ │ ├── IntentUtils.java │ │ ├── SuperActivityToastUtils.java │ │ ├── SimpleAsyncTaskLoader.java │ │ ├── SharedPrefsUtils.java │ │ ├── CroutonUtils.java │ │ ├── ThemeUtils.java │ │ ├── Showcase.java │ │ ├── LegacyTimeUtils.java │ │ ├── NotificationUtils.java │ │ ├── IoUtils.java │ │ ├── ScreenshotUtils.java │ │ ├── CheatSheetUtils.java │ │ ├── AccountPrefsUtils.java │ │ ├── TimeUtils.java │ │ ├── PickerUtils.java │ │ ├── SecurityUtils.java │ │ ├── DrawableUtils.java │ │ ├── DragUtils.java │ │ ├── AccountPreferences.java │ │ ├── PRNGFixes.java │ │ └── AppUtils.java ├── build.gradle ├── proguard-rules.pro └── library.iml ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md ├── gradle.properties ├── AndroidUtil.iml ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | AndroidUtil -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' 2 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhanghai/AndroidUtil/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/GsonUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import com.google.gson.Gson; 9 | import com.google.gson.GsonBuilder; 10 | 11 | public class GsonUtils { 12 | 13 | private static final Gson GSON = new GsonBuilder().enableComplexMapKeySerialization().create(); 14 | 15 | public static Gson get() { 16 | return GSON; 17 | } 18 | 19 | 20 | private GsonUtils() {} 21 | } 22 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:appcompat-v7:22.2.0' 24 | } 25 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /opt/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Util 2 | 3 | Utilities to make the life of an Android developer easier. 4 | 5 | ## License 6 | 7 | Copyright 2015 Zhang Hai 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/ArrayUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | public class ArrayUtils { 9 | 10 | public static int indexOf(T[] array, T value) { 11 | for (int i = 0; i < array.length; ++i) { 12 | if (array[i].equals(value)) { 13 | return i; 14 | } 15 | } 16 | return -1; 17 | } 18 | 19 | public static int[] stringArrayToIntArray(String[] stringArray) { 20 | int[] intArray = new int[stringArray.length]; 21 | for (int i = 0; i < stringArray.length; ++i) { 22 | intArray[i] = Integer.parseInt(stringArray[i]); 23 | } 24 | return intArray; 25 | } 26 | 27 | 28 | private ArrayUtils() {} 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/UriImageLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | import android.graphics.Bitmap; 10 | import android.net.Uri; 11 | 12 | public class UriImageLoader extends SimpleAsyncTaskLoader { 13 | 14 | private Uri uri; 15 | 16 | public UriImageLoader(Uri uri, Context context) { 17 | super(context); 18 | 19 | this.uri = uri; 20 | } 21 | 22 | @Override 23 | public Bitmap loadInBackground() { 24 | return DrawableUtils.decodeBitmapFromUri(uri, getContext()); 25 | } 26 | 27 | @Override 28 | protected void onReleaseResult(Bitmap result) { 29 | if (result != null && !result.isRecycled()) { 30 | result.recycle(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AndroidUtil.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/ToastUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | import android.widget.Toast; 10 | 11 | public class ToastUtils { 12 | 13 | public static void show(CharSequence text, int duration, Context context) { 14 | Toast.makeText(context, text, duration).show(); 15 | } 16 | public static void show(int resId, int duration, Context context) { 17 | show(context.getText(resId), duration, context); 18 | } 19 | 20 | public static void show(CharSequence text, Context context) { 21 | show(text, Toast.LENGTH_SHORT, context); 22 | } 23 | 24 | public static void show(int resId, Context context) { 25 | show(context.getText(resId), context); 26 | } 27 | 28 | public static void showNotYetImplemented(Context context) { 29 | show(R.string.not_yet_implemented, context); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/ColorUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | public class ColorUtils { 9 | 10 | public enum Material300 { 11 | RED(0xffe57373), 12 | PINK(0xfff06292), 13 | PURPLE(0xffba68c8), 14 | DEEP_PURPLE(0xff9575cd), 15 | INDIGO(0xff7986cb), 16 | BLUE(0xff64b5f6), 17 | CYAN(0xff4dd0e1), 18 | LIGHT_BLUE(0xff4fc3f7), 19 | TEAL(0xff4db6ac), 20 | GREEN(0xff81c784), 21 | LIGHT_GREEN(0xffaed581), 22 | LIME(0xffdce775), 23 | YELLOW(0xfffff176), 24 | AMBER(0xffffd54f), 25 | ORANGE(0xffffb74d), 26 | DEEP_ORANGE(0xffff8a65), 27 | BROWN(0xffa1887f), 28 | GREY(0xffe0e0e0), 29 | BLUE_GREY(0xff90a4ae); 30 | 31 | private int color; 32 | 33 | Material300(int color) { 34 | this.color = color; 35 | } 36 | 37 | public int getColor() { 38 | return color; 39 | } 40 | } 41 | 42 | private ColorUtils() {} 43 | 44 | public static int fromText(String text) { 45 | return Material300.values()[text.hashCode() % Material300.values().length].getColor(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.os.Environment; 9 | 10 | import java.io.File; 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | import java.util.Locale; 14 | 15 | public class FileUtils { 16 | 17 | private static final String MOBILE_DIRECTORY_NAME = "QSCMobile"; 18 | 19 | private static final SimpleDateFormat DATE_TIME_FORMAT = 20 | new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); 21 | 22 | public static String makeDateTimeFileName(String prefix, String suffix) { 23 | return String.format("%s_%s.%s", prefix, DATE_TIME_FORMAT.format(new Date()), suffix); 24 | } 25 | 26 | public static File makePictureDirectory() { 27 | return new File( 28 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), 29 | MOBILE_DIRECTORY_NAME); 30 | } 31 | 32 | public static void ensureParentDirectory(File file) { 33 | File parent = file.getParentFile(); 34 | if (parent != null && !parent.isDirectory()) { 35 | parent.mkdirs(); 36 | } 37 | } 38 | 39 | 40 | private FileUtils() {} 41 | } 42 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/LoadingEmptyHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.database.Cursor; 9 | import android.view.View; 10 | 11 | public class LoadingEmptyHelper { 12 | 13 | private View loadingView; 14 | private View emptyView; 15 | 16 | public LoadingEmptyHelper(View loadingView, View emptyView) { 17 | this.loadingView = loadingView; 18 | this.emptyView = emptyView; 19 | } 20 | 21 | public LoadingEmptyHelper(View parent, int loadingViewId, int emptyViewId) { 22 | this(parent.findViewById(loadingViewId), parent.findViewById(emptyViewId)); 23 | } 24 | 25 | public View getLoadingView() { 26 | return loadingView; 27 | } 28 | 29 | public View getEmptyView() { 30 | return emptyView; 31 | } 32 | 33 | public void setLoading() { 34 | emptyView.setVisibility(View.GONE); 35 | loadingView.setVisibility(View.VISIBLE); 36 | } 37 | 38 | public void setFinished(boolean empty) { 39 | loadingView.setVisibility(View.GONE); 40 | emptyView.setVisibility(empty ? View.VISIBLE : View.GONE); 41 | } 42 | 43 | public void setFinished(Cursor cursor) { 44 | setFinished(cursor == null || cursor.getCount() == 0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/BuildUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | 10 | import com.myqsc.mobile3.umeng.util.UmengUtils; 11 | 12 | public class BuildUtils { 13 | 14 | public static boolean isDebug() { 15 | return BuildConfig.DEBUG; 16 | } 17 | 18 | public static void throwOrPrint(Throwable throwable) { 19 | if (BuildConfig.DEBUG) { 20 | throwRuntimeException(throwable); 21 | } else { 22 | throwable.printStackTrace(); 23 | } 24 | } 25 | 26 | private static void throwRuntimeException(Throwable throwable) { 27 | if (throwable instanceof RuntimeException) { 28 | throw (RuntimeException) throwable; 29 | } else { 30 | throw new RuntimeException(throwable); 31 | } 32 | } 33 | 34 | // NOTE: If in productive build, this method prints stack trace before reporting. 35 | public static void throwOrPrintAndReport(Throwable throwable, Context context) { 36 | if (BuildConfig.DEBUG) { 37 | throwRuntimeException(throwable); 38 | } else { 39 | throwable.printStackTrace(); 40 | UmengUtils.reportError(throwable, context); 41 | } 42 | } 43 | 44 | 45 | private BuildUtils() {} 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/MathUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | public class MathUtils { 9 | 10 | private MathUtils() {} 11 | 12 | public static float constrain(float min, float max, float v) { 13 | return Math.max(min, Math.min(max, v)); 14 | } 15 | 16 | public static float interpolate(float x1, float x2, float f) { 17 | return x1 + (x2 - x1) * f; 18 | } 19 | 20 | public static float uninterpolate(float x1, float x2, float v) { 21 | if (x2 - x1 == 0) { 22 | throw new IllegalArgumentException("Can't reverse interpolate with domain size of 0"); 23 | } 24 | return (v - x1) / (x2 - x1); 25 | } 26 | 27 | public static float dist(float x, float y) { 28 | return (float) Math.sqrt(x * x + y * y); 29 | } 30 | 31 | public static int floorEven(int num) { 32 | return num & ~0x01; 33 | } 34 | 35 | public static int roundMult4(int num) { 36 | return (num + 2) & ~0x03; 37 | } 38 | 39 | public static boolean isEven(int num) { 40 | return num % 2 == 0; 41 | } 42 | 43 | // divide two integers but round up 44 | // see http://stackoverflow.com/a/7446742/102703 45 | public static int intDivideRoundUp(int num, int divisor) { 46 | int sign = (num > 0 ? 1 : -1) * (divisor > 0 ? 1 : -1); 47 | return sign * (Math.abs(num) + Math.abs(divisor) - 1) / Math.abs(divisor); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/ShapeDrawableUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | import android.graphics.drawable.Drawable; 10 | import android.graphics.drawable.StateListDrawable; 11 | import android.support.v4.content.ContextCompat; 12 | 13 | import com.myqsc.mobile3.ui.DrawableShapeDrawable; 14 | import com.myqsc.mobile3.ui.TextShapeDrawable; 15 | 16 | public class ShapeDrawableUtils { 17 | 18 | private ShapeDrawableUtils() {} 19 | 20 | public static TextShapeDrawable makeText(String text) { 21 | return new TextShapeDrawable.Builder() 22 | .setColor(ColorUtils.fromText(text)) 23 | .setText(text.substring(0, 1).toUpperCase()) 24 | .asOval() 25 | .build(); 26 | } 27 | 28 | public static DrawableShapeDrawable makeChecked(Context context) { 29 | return new DrawableShapeDrawable.Builder() 30 | .setDrawable(ContextCompat.getDrawable(context, R.drawable.ic_check_white_24dp)) 31 | .asOval() 32 | .build(); 33 | } 34 | 35 | public static StateListDrawable makeSelectable(String text, Drawable checkedDrawable) { 36 | StateListDrawable drawable = new StateListDrawable(); 37 | drawable.addState(new int[] {android.R.attr.state_activated}, checkedDrawable); 38 | drawable.addState(new int[] {}, makeText(text)); 39 | return drawable; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/TouchUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.os.SystemClock; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | import android.view.ViewConfiguration; 12 | 13 | public class TouchUtils { 14 | 15 | private static final int TOUCH_TIME_MILLIS = 500; 16 | 17 | private TouchUtils() {} 18 | 19 | // Adapted from android.test.TouchUtils 20 | public static void touchAndCancel(final View view) { 21 | 22 | int[] xy = new int[2]; 23 | view.getLocationOnScreen(xy); 24 | final float x = xy[0] + (view.getWidth() / 2.0f); 25 | final float y = xy[1] + (view.getHeight() / 2.0f); 26 | 27 | final long downTime = SystemClock.uptimeMillis(); 28 | long eventTime = SystemClock.uptimeMillis(); 29 | 30 | MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 31 | 0); 32 | view.dispatchTouchEvent(event); 33 | event.recycle(); 34 | 35 | AppUtils.postDelayed(new Runnable() { 36 | @Override 37 | public void run() { 38 | long eventTime = SystemClock.uptimeMillis(); 39 | int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop(); 40 | MotionEvent event = MotionEvent.obtain(downTime, eventTime, 41 | MotionEvent.ACTION_CANCEL, 42 | x + (touchSlop / 2.0f), y + (touchSlop / 2.0f), 0); 43 | view.dispatchTouchEvent(event); 44 | event.recycle(); 45 | } 46 | }, TOUCH_TIME_MILLIS); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/LogUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.util.Log; 9 | 10 | /** 11 | * Utility class for one-line logging. 12 | * 13 | *

Be sure to change {@link #TAG} before using the class. You can simply make a copy of this 14 | * class and then modify it.

15 | * 16 | * @see Log 17 | */ 18 | public class LogUtils { 19 | 20 | private static final String TAG = "app_name"; 21 | 22 | private LogUtils() {} 23 | 24 | /** 25 | * @see Log#d(String, String) 26 | */ 27 | public static void d(String message) { 28 | Log.d(TAG, buildMessage(message)); 29 | } 30 | 31 | /** 32 | * @see Log#e(String, String) 33 | */ 34 | public static void e(String message) { 35 | Log.e(TAG, buildMessage(message)); 36 | } 37 | 38 | /** 39 | * @see Log#i(String, String) 40 | */ 41 | public static void i(String message) { 42 | Log.i(TAG, buildMessage(message)); 43 | } 44 | 45 | /** 46 | * @see Log#v(String, String) 47 | */ 48 | public static void v(String message) { 49 | Log.v(TAG, buildMessage(message)); 50 | } 51 | 52 | /** 53 | * @see Log#w(String, String) 54 | */ 55 | public static void w(String message) { 56 | Log.w(TAG, buildMessage(message)); 57 | } 58 | 59 | /** 60 | * @see Log#wtf(String, String) 61 | */ 62 | public static void wtf(String message) { 63 | Log.wtf(TAG, buildMessage(message)); 64 | } 65 | 66 | /** 67 | * @see Log#println(int, String, String) 68 | */ 69 | public static void println(String message) { 70 | Log.println(Log.INFO, TAG, message); 71 | } 72 | 73 | private static String buildMessage(String rawMessage) { 74 | StackTraceElement caller = new Throwable().getStackTrace()[2]; 75 | String fullClassName = caller.getClassName(); 76 | String className = fullClassName.substring(fullClassName.lastIndexOf(".") + 1); 77 | return className + "." + caller.getMethodName() + "(): " + rawMessage; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/UriUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.ContentResolver; 9 | import android.content.Context; 10 | import android.database.Cursor; 11 | import android.net.Uri; 12 | import android.provider.OpenableColumns; 13 | 14 | import java.io.File; 15 | import java.io.FileNotFoundException; 16 | import java.io.InputStream; 17 | 18 | public class UriUtils { 19 | 20 | public static InputStream openInputStream(Uri uri, Context context) { 21 | try { 22 | return context.getContentResolver().openInputStream(uri); 23 | } catch (FileNotFoundException e) { 24 | e.printStackTrace(); 25 | return null; 26 | } 27 | } 28 | 29 | // See https://developer.android.com/training/secure-file-sharing/retrieve-info.html#RetrieveFileInfo 30 | public static OpenableInfo queryOpenableInfo(Uri uri, Context context) { 31 | 32 | Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); 33 | 34 | if (cursor == null || !cursor.moveToFirst()) { 35 | // Fallback: Check if it is a file Uri. 36 | if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { 37 | File file = new File(uri.getPath()); 38 | return new OpenableInfo(file.getName(), file.length()); 39 | } 40 | return null; 41 | } 42 | 43 | OpenableInfo info = new OpenableInfo( 44 | cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)), 45 | cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE))); 46 | cursor.close(); 47 | return info; 48 | } 49 | 50 | public static class OpenableInfo { 51 | 52 | private String displayName; 53 | private long size; 54 | 55 | public OpenableInfo(String displayName, long size) { 56 | this.displayName = displayName; 57 | this.size = size; 58 | } 59 | 60 | public String getDisplayName() { 61 | return displayName; 62 | } 63 | 64 | public long getSize() { 65 | return size; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/LocaleUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.annotation.TargetApi; 9 | import android.content.Context; 10 | import android.content.res.Configuration; 11 | import android.content.res.Resources; 12 | import android.os.Build; 13 | 14 | import com.myqsc.mobile3.settings.info.SharedPrefsContract; 15 | 16 | import java.util.Locale; 17 | 18 | public class LocaleUtils { 19 | 20 | // NOTICE: 21 | // Framework language will fallback to en-US directly if the locale combination is not found, so 22 | // we have to localize it ourselves. 23 | 24 | // Android resource system have no support for language variant code, so we have to do this 25 | // dirty hack. (https://groups.google.com/forum/#!topic/android-contrib/AU9DMpt5Ewo) 26 | // We are using country code "AD" (Andorra) (the first element in officially assigned code 27 | // elements of ISO 3166-1 alpha-2) here, sorry if this will hurt their feelings. 28 | public static final Locale LOCALE_ZH_CN_MOE = new Locale("zh", "AD"); 29 | 30 | public static final Locale[] LOCALES = new Locale[] { 31 | null, // For system default. 32 | Locale.CHINA, 33 | LOCALE_ZH_CN_MOE, 34 | Locale.ENGLISH 35 | }; 36 | 37 | private LocaleUtils() {} 38 | 39 | public static Locale getLocale(Context context) { 40 | 41 | int index = Integer.valueOf(SharedPrefsUtils.getString(SharedPrefsContract.KEY_LANGUAGE, 42 | SharedPrefsContract.DEFAULT_LANGUAGE, context)); 43 | 44 | if (index == 0) { 45 | return Locale.getDefault(); 46 | } else { 47 | return LOCALES[index]; 48 | } 49 | } 50 | 51 | public static void applyLocaleToConfiguration(Context context, Configuration configuration) { 52 | applyLocaleToConfiguration(configuration, getLocale(context)); 53 | } 54 | 55 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 56 | public static void applyLocaleToConfiguration(Configuration configuration, Locale locale) { 57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 58 | // For layout direction (Framework preferred way). 59 | configuration.setLocale(locale); 60 | } else { 61 | configuration.locale = locale; 62 | } 63 | } 64 | 65 | public static void applyLocale(Context context) { 66 | 67 | Locale locale = getLocale(context); 68 | 69 | Resources resources = context.getResources(); 70 | Configuration configuration = resources.getConfiguration(); 71 | applyLocaleToConfiguration(configuration, locale); 72 | resources.updateConfiguration(configuration, resources.getDisplayMetrics()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/IntentUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.annotation.TargetApi; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.net.Uri; 12 | import android.os.Build; 13 | 14 | public class IntentUtils { 15 | 16 | private static final String ACTION_INSTALL_SHORTCUT = 17 | "com.android.launcher.action.INSTALL_SHORTCUT"; 18 | 19 | private static final String MIME_TYPE_TEXT_PLAIN = "text/plain"; 20 | private static final String MIME_TYPE_IMAGE_ANY = "image/*"; 21 | private static final String MIME_TYPE_ANY = "*/*"; 22 | 23 | private IntentUtils() {} 24 | 25 | public static Intent makeInstallShortcut(int iconRes, int nameRes, Class intentClass, 26 | Context context) { 27 | return new Intent() 28 | .putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(context.getApplicationContext(), 29 | intentClass)) 30 | .putExtra(Intent.EXTRA_SHORTCUT_NAME, context.getString(nameRes)) 31 | .putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 32 | Intent.ShortcutIconResource.fromContext(context, iconRes)); 33 | } 34 | 35 | public static Intent makeInstallShortcutWithAction(int iconRes, int nameRes, 36 | Class intentClass, Context context) { 37 | return makeInstallShortcut(iconRes, nameRes, intentClass, context) 38 | .setAction(ACTION_INSTALL_SHORTCUT); 39 | } 40 | 41 | public static Intent makePickFile() { 42 | return new Intent(Intent.ACTION_GET_CONTENT) 43 | .setType(MIME_TYPE_ANY) 44 | .addCategory(Intent.CATEGORY_OPENABLE); 45 | } 46 | 47 | // NOTE: Before Build.VERSION_CODES.JELLY_BEAN htmlText will be no-op. 48 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 49 | public static Intent makeSendText(CharSequence text, String htmlText) { 50 | Intent intent = new Intent() 51 | .setAction(Intent.ACTION_SEND) 52 | .putExtra(Intent.EXTRA_TEXT, text); 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && htmlText != null) { 54 | intent.putExtra(Intent.EXTRA_HTML_TEXT, htmlText); 55 | } 56 | return intent.setType(MIME_TYPE_TEXT_PLAIN); 57 | } 58 | 59 | public static Intent makeSendText(CharSequence text) { 60 | return makeSendText(text, null); 61 | } 62 | 63 | public static Intent makeSendImage(Uri imageUri, CharSequence text) { 64 | return new Intent() 65 | .setAction(Intent.ACTION_SEND) 66 | // For maximum compatibility. 67 | .putExtra(Intent.EXTRA_TEXT, text) 68 | .putExtra(Intent.EXTRA_TITLE, text) 69 | .putExtra(Intent.EXTRA_SUBJECT, text) 70 | // HACK: WeChat moments respects this extra only. 71 | .putExtra("Kdescription", text) 72 | .putExtra(Intent.EXTRA_STREAM, imageUri) 73 | .setType(MIME_TYPE_IMAGE_ANY); 74 | } 75 | 76 | public static Intent makeView(Uri uri) { 77 | return new Intent(Intent.ACTION_VIEW, uri); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/SuperActivityToastUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Activity; 9 | import android.os.Parcelable; 10 | import android.view.View; 11 | import android.widget.Button; 12 | 13 | import com.github.johnpersano.supertoasts.SuperActivityToast; 14 | import com.github.johnpersano.supertoasts.SuperToast; 15 | import com.github.johnpersano.supertoasts.util.OnClickWrapper; 16 | 17 | public class SuperActivityToastUtils { 18 | 19 | public static void showWithButton(CharSequence message, int buttonIconRes, 20 | CharSequence buttonText, View.OnClickListener onClickListener, 21 | int duration, Activity activity) { 22 | SuperActivityToast toast = new SuperActivityToast(activity, SuperToast.Type.BUTTON); 23 | toast.setText(message); 24 | toast.setButtonIcon(buttonIconRes); 25 | toast.setButtonText(buttonText); 26 | ((Button)toast.getView().findViewById(com.github.johnpersano.supertoasts.R.id.button)) 27 | .setAllCaps(true); 28 | toast.setOnClickWrapper(new OnClickWrapper(null, new OnClickListenerWrapper( 29 | onClickListener))); 30 | toast.setDuration(duration); 31 | toast.show(); 32 | } 33 | 34 | public static void showWithButton(CharSequence message, int buttonIconRes, 35 | CharSequence buttonText, View.OnClickListener onClickListener, 36 | Activity activity) { 37 | showWithButton(message, buttonIconRes, buttonText, onClickListener, 38 | SuperToast.Duration.LONG, activity); 39 | } 40 | 41 | public static void showWithButton(int messageRes, int buttonIconRes, int buttonTextRes, 42 | View.OnClickListener onClickListener, int duration, 43 | Activity activity) { 44 | showWithButton(activity.getText(messageRes), buttonIconRes, activity.getText(buttonTextRes), 45 | onClickListener, duration, activity); 46 | } 47 | 48 | public static void showWithButton(int messageRes, int buttonIconRes, int buttonTextRes, 49 | View.OnClickListener onClickListener, Activity activity) { 50 | showWithButton(activity.getText(messageRes), buttonIconRes, activity.getText(buttonTextRes), 51 | onClickListener, activity); 52 | } 53 | 54 | public static void clear(Activity activity) { 55 | SuperActivityToast.clearSuperActivityToastsForActivity(activity); 56 | } 57 | 58 | public static void clear() { 59 | SuperActivityToast.cancelAllSuperActivityToasts(); 60 | } 61 | 62 | private SuperActivityToastUtils() {} 63 | 64 | private static class OnClickListenerWrapper implements SuperToast.OnClickListener { 65 | 66 | private View.OnClickListener listener; 67 | 68 | private OnClickListenerWrapper(View.OnClickListener listener) { 69 | this.listener = listener; 70 | } 71 | 72 | @Override 73 | public void onClick(View view, Parcelable parcelable) { 74 | listener.onClick(view); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/SimpleAsyncTaskLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | import android.support.v4.content.AsyncTaskLoader; 10 | 11 | public abstract class SimpleAsyncTaskLoader extends AsyncTaskLoader { 12 | 13 | private D result; 14 | 15 | public SimpleAsyncTaskLoader(Context context) { 16 | super(context); 17 | } 18 | 19 | /** 20 | * Called when there is new data to deliver to the client. The 21 | * super class will take care of delivering it; the implementation 22 | * here just adds a little more logic. 23 | */ 24 | @Override 25 | public void deliverResult(D newResult) { 26 | 27 | if (isReset()) { 28 | // An async query came in while the loader is stopped. We 29 | // don't need the result. 30 | if (newResult != null) { 31 | onReleaseResult(newResult); 32 | } 33 | } 34 | D oldResult = result; 35 | result = newResult; 36 | 37 | if (isStarted()) { 38 | // If the Loader is currently started, we can immediately 39 | // deliver its results. 40 | super.deliverResult(newResult); 41 | } 42 | 43 | // At this point we can release the resources associated with 44 | // old result if needed; now that the new result is delivered we 45 | // know that it is no longer in use. 46 | if (oldResult != result && oldResult != null) { 47 | onReleaseResult(oldResult); 48 | } 49 | } 50 | 51 | /** 52 | * Handles a request to start the Loader. 53 | */ 54 | @Override 55 | protected void onStartLoading() { 56 | if (result != null) { 57 | // If we currently have a result available, deliver it 58 | // immediately. 59 | deliverResult(result); 60 | } 61 | 62 | if (takeContentChanged() || result == null) { 63 | // If the data has changed since the last time it was loaded 64 | // or is not currently available, start a load. 65 | forceLoad(); 66 | } 67 | } 68 | 69 | /** 70 | * Handles a request to stop the Loader. 71 | */ 72 | @Override 73 | protected void onStopLoading() { 74 | // Attempt to cancel the current load task if possible. 75 | cancelLoad(); 76 | } 77 | 78 | /** 79 | * Handles a request to cancel a load. 80 | */ 81 | @Override 82 | public void onCanceled(D result) { 83 | super.onCanceled(result); 84 | 85 | // At this point we can release the resources associated with result 86 | // if needed. 87 | if (result != null) { 88 | onReleaseResult(result); 89 | } 90 | } 91 | 92 | /** 93 | * Handles a request to completely reset the Loader. 94 | */ 95 | @Override 96 | protected void onReset() { 97 | super.onReset(); 98 | 99 | // Ensure the loader is stopped 100 | onStopLoading(); 101 | 102 | // At this point we can release the resources associated with 'result' 103 | // if needed. 104 | if (result != null) { 105 | onReleaseResult(result); 106 | result = null; 107 | } 108 | } 109 | 110 | /** 111 | * Helper function to take care of releasing resources associated 112 | * with an actively loaded data set. 113 | */ 114 | protected void onReleaseResult(D result) {} 115 | } 116 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/SharedPrefsUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | import android.content.SharedPreferences; 10 | import android.preference.PreferenceManager; 11 | 12 | import java.security.GeneralSecurityException; 13 | import java.util.Set; 14 | 15 | public class SharedPrefsUtils { 16 | 17 | public static SharedPreferences getSharedPrefs(Context context) { 18 | return PreferenceManager.getDefaultSharedPreferences(context); 19 | } 20 | 21 | public static String getString(String key, String defaultValue, Context context) { 22 | return getSharedPrefs(context).getString(key, defaultValue); 23 | } 24 | 25 | public static Set getStringSet(String key, Set defaultValue, Context context) { 26 | return getSharedPrefs(context).getStringSet(key, defaultValue); 27 | } 28 | 29 | public static int getInt(String key, int defaultValue, Context context) { 30 | return getSharedPrefs(context).getInt(key, defaultValue); 31 | } 32 | 33 | public static long getLong(String key, long defaultValue, Context context) { 34 | return getSharedPrefs(context).getLong(key, defaultValue); 35 | } 36 | 37 | public static float getFloat(String key, float defaultValue, Context context) { 38 | return getSharedPrefs(context).getFloat(key, defaultValue); 39 | } 40 | 41 | public static boolean getBoolean(String key, boolean defaultValue, Context context) { 42 | return getSharedPrefs(context).getBoolean(key, defaultValue); 43 | } 44 | 45 | public static SharedPreferences.Editor getEditor(Context context) { 46 | return getSharedPrefs(context).edit(); 47 | } 48 | 49 | public static void putString(String key, String value, Context context) { 50 | getEditor(context).putString(key, value).apply(); 51 | } 52 | 53 | public static void putStringSet(String key, Set value, Context context) { 54 | getEditor(context).putStringSet(key, value).apply(); 55 | } 56 | 57 | public static void putInt(String key, int value, Context context) { 58 | getEditor(context).putInt(key, value).apply(); 59 | } 60 | 61 | public static void putLong(String key, long value, Context context) { 62 | getEditor(context).putLong(key, value).apply(); 63 | } 64 | 65 | public static void putFloat(String key, float value, Context context) { 66 | getEditor(context).putFloat(key, value).apply(); 67 | } 68 | 69 | public static void putBoolean(String key, boolean value, Context context) { 70 | getEditor(context).putBoolean(key, value).apply(); 71 | } 72 | 73 | public static void remove(String key, Context context) { 74 | getEditor(context).remove(key).apply(); 75 | } 76 | 77 | public static void clear(Context context) { 78 | getEditor(context).clear().apply(); 79 | } 80 | 81 | public static String getStringObfuscated(String key, String defaultValue, Context context) { 82 | String value = getString(key, null, context); 83 | if (value != null) { 84 | try { 85 | return SecurityUtils.deobfuscate(value, context); 86 | } catch (GeneralSecurityException e) { 87 | BuildUtils.throwOrPrintAndReport(e, context); 88 | } 89 | } 90 | return defaultValue; 91 | } 92 | 93 | public static void putStringObfuscated(String key, String value, Context context) { 94 | putString(key, SecurityUtils.obfuscateIfNotNull(value, context), context); 95 | } 96 | 97 | private SharedPrefsUtils() {} 98 | } 99 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/CroutonUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Activity; 9 | import android.content.Context; 10 | import android.view.ViewGroup; 11 | 12 | import de.keyboardsurfer.android.widget.crouton.Crouton; 13 | import de.keyboardsurfer.android.widget.crouton.Style; 14 | 15 | public class CroutonUtils { 16 | 17 | private enum Method { 18 | INFO, 19 | OK, 20 | ERROR 21 | } 22 | 23 | 24 | public static void info(Activity activity, int textRes){ 25 | Crouton.showText(activity, textRes, getStyle(Method.INFO, activity)); 26 | } 27 | 28 | public static void info(Activity activity, int textRes, int viewGroupRes){ 29 | Crouton.showText(activity, textRes, getStyle(Method.INFO, activity), viewGroupRes); 30 | } 31 | 32 | public static void info(Activity activity, CharSequence text) { 33 | Crouton.showText(activity, text, getStyle(Method.INFO, activity)); 34 | } 35 | 36 | public static void info(Activity activity, CharSequence text, ViewGroup viewGroup) { 37 | Crouton.showText(activity, text, getStyle(Method.INFO, activity), viewGroup); 38 | } 39 | 40 | public static void ok(Activity activity, int textRes){ 41 | Crouton.showText(activity, textRes, getStyle(Method.OK, activity)); 42 | } 43 | 44 | public static void ok(Activity activity, int textRes, int viewGroupRes){ 45 | Crouton.showText(activity, textRes, getStyle(Method.OK, activity), viewGroupRes); 46 | } 47 | 48 | public static void ok(Activity activity, CharSequence text) { 49 | Crouton.showText(activity, text, getStyle(Method.OK, activity)); 50 | } 51 | 52 | public static void ok(Activity activity, CharSequence text, ViewGroup viewGroup) { 53 | Crouton.showText(activity, text, getStyle(Method.OK, activity), viewGroup); 54 | } 55 | 56 | public static void error(Activity activity, int textRes){ 57 | Crouton.showText(activity, textRes, getStyle(Method.ERROR, activity)); 58 | } 59 | 60 | public static void error(Activity activity, int textRes, int viewGroupRes){ 61 | Crouton.showText(activity, textRes, getStyle(Method.ERROR, activity), viewGroupRes); 62 | } 63 | 64 | public static void error(Activity activity, CharSequence text) { 65 | Crouton.showText(activity, text, getStyle(Method.ERROR, activity)); 66 | } 67 | 68 | public static void error(Activity activity, CharSequence text, ViewGroup viewGroup) { 69 | Crouton.showText(activity, text, getStyle(Method.ERROR, activity), viewGroup); 70 | } 71 | 72 | // NOTICE: Every Activity using Crouton should call this function in its onDestroy(). 73 | public static void clear(Activity activity) { 74 | Crouton.clearCroutonsForActivity(activity); 75 | } 76 | 77 | public static void clearAll() { 78 | Crouton.cancelAllCroutons(); 79 | } 80 | 81 | private static Style getStyle(Method method, Context themedContext) { 82 | 83 | int textColorAttrResId = -1, backgroundColorAttrResId = -1; 84 | switch (method) { 85 | case INFO: 86 | textColorAttrResId = R.attr.croutonInfoTextColor; 87 | backgroundColorAttrResId = R.attr.croutonInfoBackgroundColor; 88 | break; 89 | case OK: 90 | textColorAttrResId = R.attr.croutonOkTextColor; 91 | backgroundColorAttrResId = R.attr.croutonOkBackgroundColor; 92 | break; 93 | case ERROR: 94 | textColorAttrResId = R.attr.croutonErrorTextColor; 95 | backgroundColorAttrResId = R.attr.croutonErrorBackgroundColor; 96 | break; 97 | } 98 | 99 | return buildStyle(ThemeUtils.getResId(themedContext, textColorAttrResId), 100 | ThemeUtils.getResId(themedContext, backgroundColorAttrResId)); 101 | } 102 | 103 | private static Style buildStyle(int textColorResId, int backgroundColorResId) { 104 | return new Style.Builder() 105 | .setTextColor(textColorResId) 106 | .setBackgroundColor(backgroundColorResId) 107 | .build(); 108 | } 109 | 110 | 111 | private CroutonUtils() {} 112 | } 113 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/ThemeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Activity; 9 | import android.content.Context; 10 | import android.content.res.TypedArray; 11 | import android.graphics.Color; 12 | import android.graphics.drawable.ColorDrawable; 13 | import android.graphics.drawable.Drawable; 14 | import android.view.ContextThemeWrapper; 15 | 16 | import com.myqsc.mobile3.settings.info.SharedPrefsContract; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | public class ThemeUtils { 22 | 23 | private static final int[] THEME_IDS = new int[] { 24 | R.style.Theme_Default, 25 | R.style.Theme_Holo_Dark, 26 | R.style.Theme_Holo_Wallpaper 27 | }; 28 | 29 | private static final Map NO_ACTION_BAR_THEME_MAP; 30 | static { 31 | NO_ACTION_BAR_THEME_MAP = new HashMap<>(); 32 | NO_ACTION_BAR_THEME_MAP.put(R.style.Theme_Default, R.style.Theme_Default_NoActionBar); 33 | NO_ACTION_BAR_THEME_MAP.put(R.style.Theme_Holo_Dark, R.style.Theme_Holo_Dark_NoActionBar); 34 | NO_ACTION_BAR_THEME_MAP.put(R.style.Theme_Holo_Wallpaper, 35 | R.style.Theme_Holo_Wallpaper_NoActionBar); 36 | } 37 | 38 | 39 | public static int getThemeId(Context context) { 40 | 41 | int index = Integer.valueOf(SharedPrefsUtils.getString(SharedPrefsContract.KEY_THEME, 42 | SharedPrefsContract.DEFAULT_THEME, context)); 43 | 44 | return THEME_IDS[index]; 45 | } 46 | 47 | public static int getNoActionBarThemeId(int themeId) { 48 | return NO_ACTION_BAR_THEME_MAP.get(themeId); 49 | } 50 | 51 | public static boolean isWallpaperTheme(int themeId) { 52 | return themeId == R.style.Theme_Holo_Wallpaper 53 | || themeId == R.style.Theme_Holo_Wallpaper_NoActionBar; 54 | } 55 | 56 | public static boolean isWallpaperTheme(Context context) { 57 | return isWallpaperTheme(getThemeId(context)); 58 | } 59 | 60 | public static void applyTheme(Activity activity) { 61 | int themeId = getThemeId(activity); 62 | activity.setTheme(themeId); 63 | if (isWallpaperTheme(themeId)) { 64 | applyWallpaperDim(activity); 65 | } 66 | } 67 | 68 | public static void applyNoActionBarTheme(Activity activity) { 69 | int themeId = getNoActionBarThemeId(getThemeId(activity)); 70 | activity.setTheme(themeId); 71 | if (isWallpaperTheme(themeId)) { 72 | applyWallpaperDim(activity); 73 | } 74 | } 75 | 76 | public static void applyWallpaperDim(Activity activity) { 77 | Drawable background = new ColorDrawable(Color.BLACK); 78 | int dim = SharedPrefsUtils.getInt(SharedPrefsContract.KEY_WALLPAPER_DIM, 79 | SharedPrefsContract.DEFAULT_WALLPAPER_DIM, activity); 80 | int alpha = Math.round((float)dim / SharedPrefsContract.MAX_WALLPAPER_DIM * 255); 81 | background.setAlpha(alpha); 82 | activity.getWindow().setBackgroundDrawable(background); 83 | } 84 | 85 | public static Context wrapContextWithTheme(Context context, int themeRes) { 86 | return new ContextThemeWrapper(context, themeRes); 87 | } 88 | 89 | public static Context wrapContextWithTheme(Context context) { 90 | return wrapContextWithTheme(context, getThemeId(context)); 91 | } 92 | 93 | public static int getResId(Context themedContext, int attrRes) { 94 | // NOTE: Theme.resolveAttribute() and TypedValue.resourceId doesn't seem to work, with 95 | // resolveRefs set to either true or false. 96 | TypedArray attributes = themedContext.obtainStyledAttributes(new int[] {attrRes}); 97 | int resId = attributes.getResourceId(0, -1); 98 | attributes.recycle(); 99 | return resId; 100 | } 101 | 102 | public static boolean getBoolean(Context themedContext, int attrRes, boolean defaultValue) { 103 | TypedArray attributes = themedContext.obtainStyledAttributes(new int[] {attrRes}); 104 | boolean result = attributes.getBoolean(0, defaultValue); 105 | attributes.recycle(); 106 | return result; 107 | } 108 | 109 | public static int getColor(Context themedContext, int attrRes, int defaultValue) { 110 | TypedArray attributes = themedContext.obtainStyledAttributes(new int[] {attrRes}); 111 | int result = attributes.getColor(0, defaultValue); 112 | attributes.recycle(); 113 | return result; 114 | } 115 | 116 | 117 | private ThemeUtils() {} 118 | } 119 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/Showcase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Activity; 9 | import android.view.View; 10 | 11 | import com.espian.showcaseview.ShowcaseView; 12 | import com.espian.showcaseview.ShowcaseViewBuilder; 13 | import com.espian.showcaseview.ShowcaseViews; 14 | 15 | public class Showcase { 16 | 17 | public enum ShowcaseId { 18 | Calendar_DayView_Add, 19 | Calendar_DayView_Goto, 20 | Courses, 21 | AccountManagement, 22 | Home_Edit 23 | } 24 | 25 | private static final float SCALE_DEFAULT = 1; 26 | 27 | private static final float SCALE_ACTION_ITEM = 0.5f; 28 | 29 | private Activity activity; 30 | 31 | private ShowcaseViews showcaseViews; 32 | 33 | public Showcase(Activity activity) { 34 | this.activity = activity; 35 | showcaseViews = new ShowcaseViews(activity); 36 | } 37 | 38 | public Showcase addActionItem(ShowcaseId showcaseId, int actionItemId, int titleResId, 39 | int detailResId, float scale) { 40 | showcaseViews.addView(new ShowcaseViewBuilder(activity) 41 | .setShowcaseItem(ShowcaseView.ITEM_ACTION_ITEM, actionItemId, activity) 42 | .setText(titleResId, detailResId) 43 | .setShowcaseIndicatorScale(scale) 44 | .setConfigOptions(makeConfigOptions(showcaseId)) 45 | .build()); 46 | return this; 47 | } 48 | 49 | public Showcase addActionItem(ShowcaseId showcaseId, int actionItemId, int titleResId, 50 | int detailResId) { 51 | return addActionItem(showcaseId, actionItemId, titleResId, detailResId, SCALE_ACTION_ITEM); 52 | } 53 | 54 | public Showcase addView(ShowcaseId showcaseId, View view, int titleResId, int detailResId, 55 | float scale) { 56 | if (view == null) { 57 | LogUtils.e("addView called with view == null, adding no view"); 58 | return addNoView(showcaseId, titleResId, detailResId); 59 | } else { 60 | showcaseViews.addView(new ShowcaseViewBuilder(activity) 61 | .setShowcaseView(view) 62 | .setText(titleResId, detailResId) 63 | .setShowcaseIndicatorScale(scale) 64 | .setConfigOptions(makeConfigOptions(showcaseId)) 65 | .build()); 66 | return this; 67 | } 68 | } 69 | 70 | public Showcase addView(ShowcaseId showcaseId, View view, int titleResId, int detailResId) { 71 | return addView(showcaseId, view, titleResId, detailResId, SCALE_DEFAULT); 72 | } 73 | 74 | public Showcase addView(ShowcaseId showcaseId, int viewId, int titleResId, int detailResId, 75 | float scale) { 76 | return addView(showcaseId, activity.findViewById(viewId), titleResId, detailResId, scale); 77 | } 78 | 79 | public Showcase addView(ShowcaseId showcaseId, int viewId, int titleResId, int detailResId) { 80 | return addView(showcaseId, viewId, titleResId, detailResId, SCALE_DEFAULT); 81 | } 82 | 83 | public Showcase addPosition(ShowcaseId showcaseId, int x, int y, int titleResId, 84 | int detailResId, float scale) { 85 | showcaseViews.addView(new ShowcaseViewBuilder(activity) 86 | .setShowcasePosition(x, y) 87 | .setText(titleResId, detailResId) 88 | .setShowcaseIndicatorScale(scale) 89 | .setConfigOptions(makeConfigOptions(showcaseId)) 90 | .build()); 91 | return this; 92 | } 93 | 94 | public Showcase addPosition(ShowcaseId showcaseId, int x, int y, int titleResId, 95 | int detailResId) { 96 | return addPosition(showcaseId, x, y, titleResId, detailResId, SCALE_DEFAULT); 97 | } 98 | 99 | public Showcase addNoView(ShowcaseId showcaseId, int titleResId, int detailResId) { 100 | showcaseViews.addView(new ShowcaseViewBuilder(activity) 101 | .setShowcaseNoView() 102 | .setText(titleResId, detailResId) 103 | .setShowcaseIndicatorScale(SCALE_DEFAULT) 104 | .setConfigOptions(makeConfigOptions(showcaseId)) 105 | .build()); 106 | return this; 107 | } 108 | 109 | public void show() { 110 | showcaseViews.show(); 111 | } 112 | 113 | public void postShow() { 114 | AppUtils.post(new Runnable() { 115 | @Override 116 | public void run() { 117 | show(); 118 | } 119 | }); 120 | } 121 | 122 | private ShowcaseView.ConfigOptions makeConfigOptions(ShowcaseId showcaseId) { 123 | ShowcaseView.ConfigOptions configOptions = new ShowcaseView.ConfigOptions(); 124 | configOptions.block = true; 125 | configOptions.shotType = ShowcaseView.TYPE_ONE_SHOT; 126 | configOptions.showcaseId = showcaseId.ordinal(); 127 | return configOptions; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/LegacyTimeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.annotation.SuppressLint; 9 | 10 | import java.text.DateFormat; 11 | import java.text.SimpleDateFormat; 12 | import java.util.Calendar; 13 | 14 | public class LegacyTimeUtils { 15 | 16 | // General functions 17 | 18 | public static Calendar getNow() { 19 | Calendar now = Calendar.getInstance(); 20 | now.setLenient(false); 21 | return now; 22 | } 23 | 24 | public static int compare(Calendar time1, Calendar time2) { 25 | return time1.compareTo(time2); 26 | } 27 | 28 | public static boolean isSame(Calendar time1, Calendar time2) { 29 | return compare(time1, time2) == 0; 30 | } 31 | 32 | // Date related functions 33 | 34 | public static Calendar getDate() { 35 | Calendar date = getNow(); 36 | clearTime(date); 37 | return date; 38 | } 39 | 40 | public static void setDate(Calendar date, int month, int day) { 41 | date.set(Calendar.MONTH, month); 42 | date.set(Calendar.DAY_OF_MONTH, day); 43 | } 44 | 45 | public static void setDate(Calendar date, int year, int month, int day) { 46 | date.set(Calendar.YEAR, year); 47 | setDate(date, month, day); 48 | } 49 | 50 | public static void setDate(Calendar date, Calendar newDate) { 51 | date.set(Calendar.ERA, newDate.get(Calendar.ERA)); 52 | date.set(Calendar.YEAR, newDate.get(Calendar.YEAR)); 53 | date.set(Calendar.DAY_OF_YEAR, newDate.get(Calendar.DAY_OF_YEAR)); 54 | } 55 | 56 | public static void clearDate(Calendar time) { 57 | time.clear(Calendar.ERA); 58 | time.clear(Calendar.YEAR); 59 | time.clear(Calendar.MONTH); 60 | time.clear(Calendar.DAY_OF_MONTH); 61 | } 62 | 63 | /* 64 | public static boolean isSameDate(Calendar date1, Calendar date2) { 65 | return date1.get(Calendar.ERA) == date2.get(Calendar.ERA) && 66 | date1.get(Calendar.YEAR) == date2.get(Calendar.YEAR) && 67 | date1.get(Calendar.DAY_OF_YEAR) == date2.get(Calendar.YEAR); 68 | 69 | date1 = (Calendar) date1.clone(); 70 | date2 = (Calendar) date2.clone(); 71 | clearTime(date1); 72 | clearTime(date2); 73 | 74 | return date1.compareTo(date2) == 0; 75 | } 76 | */ 77 | 78 | public static boolean isToday(Calendar date) { 79 | date = (Calendar) date.clone(); 80 | clearTime(date); 81 | return isSame(date, getDate()); 82 | } 83 | 84 | // Time related functions 85 | 86 | public static Calendar getTime() { 87 | Calendar time = getNow(); 88 | clearTime(time); 89 | return time; 90 | } 91 | 92 | public static void setTime(Calendar time, int hour, int minute, int second, int millisecond) { 93 | time.set(Calendar.HOUR_OF_DAY, hour); 94 | time.set(Calendar.MINUTE, minute); 95 | time.set(Calendar.SECOND, second); 96 | time.set(Calendar.MILLISECOND, millisecond); 97 | } 98 | 99 | public static void setTime(Calendar time, int hour, int minute, int second) { 100 | setTime(time, hour, minute, second, 0); 101 | } 102 | 103 | public static void setTime(Calendar time, int hour, int minute) { 104 | setTime(time, hour, minute, 0, 0); 105 | } 106 | 107 | public static void setTime(Calendar time, Calendar newTime) { 108 | setTime(time, newTime.get(Calendar.HOUR_OF_DAY), 109 | newTime.get(Calendar.MINUTE), 110 | newTime.get(Calendar.SECOND), 111 | newTime.get(Calendar.MILLISECOND)); 112 | } 113 | 114 | // NOTE: Calendar.clear() on HOUR_OF_DAY, HOUR or AM_PM doesn't reset the hour of day value of 115 | // this Calendar. 116 | public static void clearTime(Calendar date) { 117 | date.set(Calendar.HOUR_OF_DAY, 0); 118 | date.clear(Calendar.MINUTE); 119 | date.clear(Calendar.SECOND); 120 | date.clear(Calendar.MILLISECOND); 121 | } 122 | 123 | // String related functions 124 | 125 | @SuppressLint("SimpleDateFormat") 126 | public static String getString(Calendar time, String format) { 127 | DateFormat dateFormat = new SimpleDateFormat(format); 128 | return dateFormat.format(time.getTime()); 129 | } 130 | 131 | //private static final String[] WEEKDAY_NAMES = 132 | // new String[] {"周日", "周一", "周二", "周三", "周四", "周五", "周六"}; 133 | 134 | public static String getWeekdayString(Calendar date) { 135 | return getString(date, "EE"); 136 | //return WEEKDAY_NAMES[date.get(Calendar.DAY_OF_WEEK) - 1]; 137 | } 138 | 139 | public static String getHourMinuteString(Calendar time) { 140 | return getString(time, "HH:mm"); 141 | } 142 | 143 | public static String getNormalString(Calendar time) { 144 | return getString(time, "yyyy-M-d HH:mm:ss:SSSS"); 145 | } 146 | 147 | /* 148 | // This function includes the left boundary while excludes the right boundary of the date range. 149 | public static Boolean inRange(Calendar time, Calendar start, Calendar end) { 150 | return !time.before(start) && time.before(end); 151 | } 152 | */ 153 | 154 | 155 | private LegacyTimeUtils() {} 156 | } 157 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/NotificationUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Notification; 9 | import android.app.PendingIntent; 10 | import android.content.Context; 11 | import android.content.Intent; 12 | import android.support.v4.app.NotificationCompat; 13 | import android.support.v4.app.NotificationManagerCompat; 14 | import android.support.v4.app.TaskStackBuilder; 15 | 16 | import java.util.Collection; 17 | 18 | public class NotificationUtils { 19 | 20 | public static NotificationCompat.Builder makeBaseBuilder(int iconRes, CharSequence title, 21 | CharSequence text, Context context) { 22 | return new NotificationCompat.Builder(context) 23 | .setSmallIcon(iconRes) 24 | .setContentTitle(title) 25 | .setContentText(text); 26 | } 27 | 28 | public static NotificationCompat.Builder makeBaseBuilder(int iconRes, int titleRes, 29 | CharSequence text, Context context) { 30 | return makeBaseBuilder(iconRes, context.getString(titleRes), text, context); 31 | } 32 | 33 | public static NotificationCompat.Builder makeBaseBuilder(int iconRes, int titleRes, int textRes, 34 | Context context) { 35 | return makeBaseBuilder(iconRes, context.getString(titleRes), context.getString(textRes), 36 | context); 37 | } 38 | 39 | public static NotificationCompat.Builder makeActivityBuilder(int iconRes, CharSequence title, 40 | CharSequence text, Intent intent, 41 | Context context) { 42 | PendingIntent pendingIntent = TaskStackBuilder.create(context) 43 | .addNextIntentWithParentStack(intent) 44 | .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); 45 | return makeBaseBuilder(iconRes, title, text, context).setContentIntent(pendingIntent); 46 | } 47 | 48 | public static NotificationCompat.Builder makeActivityBuilder(int iconRes, CharSequence title, 49 | CharSequence text, 50 | Class activityClass, 51 | Context context) { 52 | return makeActivityBuilder(iconRes, title, text, new Intent(context, activityClass), 53 | context); 54 | } 55 | 56 | public static NotificationCompat.Builder makeActivityBuilder(int iconRes, int titleRes, 57 | CharSequence text, 58 | Class activityClass, 59 | Context context) { 60 | return makeActivityBuilder(iconRes, context.getString(titleRes), text, activityClass, 61 | context); 62 | } 63 | 64 | public static NotificationCompat.Builder makeActivityBuilder(int iconRes, int titleRes, 65 | int textRes, 66 | Class activityClass, 67 | Context context) { 68 | return makeActivityBuilder(iconRes, context.getString(titleRes), context.getString(textRes), 69 | activityClass, context); 70 | } 71 | 72 | public static NotificationCompat.BigTextStyle makeBigTextStyle(CharSequence title, 73 | CharSequence text) { 74 | NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle() 75 | .setBigContentTitle(title); 76 | bigTextStyle.bigText(text); 77 | return bigTextStyle; 78 | } 79 | 80 | public static NotificationCompat.BigTextStyle makeBigTextStyle(int titleRes, CharSequence text, 81 | Context context) { 82 | return makeBigTextStyle(context.getString(titleRes), text); 83 | } 84 | 85 | public static NotificationCompat.InboxStyle makeInboxStyle(CharSequence title, 86 | Collection lines) { 87 | NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle() 88 | .setBigContentTitle(title); 89 | for (CharSequence line : lines) { 90 | inboxStyle.addLine(line); 91 | } 92 | return inboxStyle; 93 | } 94 | 95 | public static NotificationCompat.InboxStyle makeInboxStyle(int titleRes, 96 | Collection lines, 97 | Context context) { 98 | return makeInboxStyle(context.getString(titleRes), lines); 99 | } 100 | 101 | public static void show(int id, Notification notification, Context context) { 102 | NotificationManagerCompat.from(context).notify(id, notification); 103 | } 104 | 105 | public static void cancel(int id, Context context) { 106 | NotificationManagerCompat.from(context).cancel(id); 107 | } 108 | 109 | 110 | private NotificationUtils() {} 111 | } 112 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/IoUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.util.Base64; 9 | 10 | import org.json.JSONArray; 11 | import org.json.JSONException; 12 | 13 | import java.io.Closeable; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.InputStreamReader; 17 | import java.util.Collection; 18 | 19 | public class IoUtils { 20 | 21 | private static final int BUFFER_SIZE = 4 * 1024; 22 | 23 | private static final String STRING_DELIMITER = "|"; 24 | 25 | private static final String STRING_DELIMITER_REGEX = "\\|"; 26 | 27 | private IoUtils() {} 28 | 29 | public static void close(Closeable closeable) { 30 | try { 31 | closeable.close(); 32 | } catch (IOException e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | 37 | public static String inputStreamToString(InputStream inputStream, String charsetName) 38 | throws IOException { 39 | StringBuilder builder = new StringBuilder(); 40 | InputStreamReader reader = new InputStreamReader(inputStream, charsetName); 41 | char[] buffer = new char[BUFFER_SIZE]; 42 | int length; 43 | while ((length = reader.read(buffer)) != -1) { 44 | builder.append(buffer, 0, length); 45 | } 46 | return builder.toString(); 47 | } 48 | 49 | public static String byteArrayToBase64(byte[] byteArray) { 50 | // We are using Base64 in Json so we don't want newlines here. 51 | return Base64.encodeToString(byteArray, Base64.NO_WRAP); 52 | } 53 | 54 | public static byte[] base64ToByteArray(String base64) { 55 | 56 | return Base64.decode(base64, Base64.DEFAULT); 57 | } 58 | 59 | // NOTE: This function is null-tolerant, nulls will be printed as "null" (so it will stay "null" 60 | // instead of null) 61 | public static String stringArrayToString(String[] strings, String delimiter) { 62 | StringBuilder builder = new StringBuilder(); 63 | boolean first = true; 64 | for (String string : strings) { 65 | if (first) { 66 | first = false; 67 | } else { 68 | builder.append(delimiter); 69 | } 70 | builder.append(string); 71 | } 72 | return builder.toString(); 73 | } 74 | 75 | public static String stringArrayToString(String[] strings) { 76 | 77 | return stringArrayToString(strings, STRING_DELIMITER); 78 | } 79 | 80 | public static String jsonStringArrayToString(JSONArray jsonArray, String delimiter) 81 | throws JSONException { 82 | StringBuilder builder = new StringBuilder(); 83 | int numStrings = jsonArray.length(); 84 | for (int i = 0; i < numStrings; ++i) { 85 | if (i != 0) { 86 | builder.append(delimiter); 87 | } 88 | builder.append(jsonArray.getString(i)); 89 | } 90 | return builder.toString(); 91 | } 92 | 93 | public static String jsonStringArrayToString(JSONArray jsonArray) throws JSONException { 94 | return jsonStringArrayToString(jsonArray, STRING_DELIMITER); 95 | } 96 | 97 | public static String arrayToString(T[] array, Stringifier stringifier, 98 | String delimiter) { 99 | StringBuilder builder = new StringBuilder(); 100 | boolean first = true; 101 | for (T object : array) { 102 | if (first) { 103 | first = false; 104 | } else { 105 | builder.append(delimiter); 106 | } 107 | builder.append(stringifier.stringify(object)); 108 | } 109 | return builder.toString(); 110 | } 111 | 112 | public static String arrayToString(T[] array, Stringifier stringifier) { 113 | 114 | return arrayToString(array, stringifier, STRING_DELIMITER); 115 | } 116 | 117 | public static String collectionToString(Collection collection, 118 | Stringifier stringifier, String delimiter) { 119 | StringBuilder builder = new StringBuilder(); 120 | boolean first = true; 121 | for (T object : collection) { 122 | if (first) { 123 | first = false; 124 | } else { 125 | builder.append(delimiter); 126 | } 127 | builder.append(stringifier.stringify(object)); 128 | } 129 | return builder.toString(); 130 | } 131 | 132 | public static String collectionToString(Collection collection, 133 | Stringifier stringifier) { 134 | return collectionToString(collection, stringifier, STRING_DELIMITER); 135 | } 136 | 137 | public static String collectionToString(Collection collection) { 138 | 139 | return collectionToString(collection, new Stringifier() { 140 | @Override 141 | public String stringify(T object) { 142 | 143 | return object.toString(); 144 | } 145 | }, STRING_DELIMITER); 146 | } 147 | 148 | // Consecutive tokens make an empty string; if you want to avoid this, use regex like "\\|+". 149 | public static String[] stringToStringArray(String string) { 150 | return stringToStringArray(string, STRING_DELIMITER_REGEX); 151 | } 152 | 153 | public static String[] stringToStringArray(String string, String delimiterRegex) { 154 | // String.split() returns the original String if pattern is not found, but we need to return 155 | // an empty array when the string is empty, instead an array containing the original empty 156 | // string. 157 | if (string.isEmpty()) { 158 | return new String[0]; 159 | } else { 160 | return string.split(delimiterRegex); 161 | } 162 | } 163 | 164 | public interface Stringifier { 165 | String stringify(T object); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/ScreenshotUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Activity; 9 | import android.app.WallpaperManager; 10 | import android.content.Context; 11 | import android.graphics.Bitmap; 12 | import android.graphics.Canvas; 13 | import android.graphics.Rect; 14 | import android.graphics.drawable.Drawable; 15 | import android.util.DisplayMetrics; 16 | import android.view.View; 17 | 18 | import java.io.File; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | 22 | // NOTE: Using JPEG against PNG gives a performance of about 1 second when wallpaper is to be 23 | // attached. 24 | public class ScreenshotUtils { 25 | 26 | public static Bitmap screenshot(View view, Rect clipRect) { 27 | Bitmap bitmap = Bitmap.createBitmap(clipRect.width(), clipRect.height(), 28 | Bitmap.Config.ARGB_8888); 29 | Canvas canvas = new Canvas(bitmap); 30 | canvas.translate(-clipRect.left, -clipRect.top); 31 | view.draw(canvas); 32 | return bitmap; 33 | } 34 | 35 | public static Bitmap screenshot(View view) { 36 | return screenshot(view, new Rect(0, 0, view.getWidth(), view.getHeight())); 37 | } 38 | 39 | public static Bitmap screenshot(Activity activity) { 40 | //View activityView = activity.findViewById(android.R.id.content).getRootView(); 41 | View activityView = activity.getWindow().getDecorView(); 42 | Rect frame = new Rect(); 43 | activityView.getWindowVisibleDisplayFrame(frame); 44 | return screenshot(activityView, frame); 45 | } 46 | 47 | public static boolean saveBitmap(Bitmap snapshot, File file, 48 | Bitmap.CompressFormat compressFormat, int quality) { 49 | try { 50 | FileOutputStream snapshotOutput = new FileOutputStream(file); 51 | if (!snapshot.compress(compressFormat, quality, snapshotOutput)) { 52 | return false; 53 | } 54 | snapshotOutput.close(); 55 | return true; 56 | } catch (IOException e) { 57 | e.printStackTrace(); 58 | return false; 59 | } 60 | } 61 | 62 | public static boolean screenshotToFile(View view, File file, 63 | Bitmap.CompressFormat compressFormat, int quality) { 64 | return saveBitmap(screenshot(view), file, compressFormat, quality); 65 | } 66 | 67 | public static boolean screenshotToFile(Activity activity, File file, 68 | Bitmap.CompressFormat compressFormat, int quality) { 69 | return saveBitmap(screenshot(activity), file, compressFormat, quality); 70 | } 71 | 72 | public static boolean screenshotToFile(View view, File file) { 73 | return screenshotToFile(view, file, Bitmap.CompressFormat.JPEG, 95); 74 | } 75 | 76 | public static boolean screenshotToFile(Activity activity, File file) { 77 | return screenshotToFile(activity, file, Bitmap.CompressFormat.JPEG, 95); 78 | } 79 | 80 | // It seems impossible to attach wallpaper to screenshot; live wallpapers seems impossible be 81 | // drawn, while static wallpapers lose its clip bounds. 82 | 83 | public static Bitmap screenshotWithWallpaper(Context context, View view, Rect clipRect) { 84 | 85 | Bitmap bitmap = Bitmap.createBitmap(clipRect.width(), clipRect.height(), 86 | Bitmap.Config.ARGB_8888); 87 | Canvas canvas = new Canvas(bitmap); 88 | 89 | // Translate for clipRect. 90 | canvas.translate(-clipRect.left, -clipRect.top); 91 | 92 | Drawable wallpaper = WallpaperManager.getInstance(context).getFastDrawable(); 93 | // Center wallpaper on screen, as in launcher. 94 | DisplayMetrics displayMetrics = view.getResources().getDisplayMetrics(); 95 | wallpaper.setBounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); 96 | // Translate the canvas to draw wallpaper on the correct location. 97 | int[] location = new int[2]; 98 | view.getLocationOnScreen(location); 99 | canvas.save(); 100 | canvas.translate(-location[0], -location[1]); 101 | wallpaper.draw(canvas); 102 | canvas.restore(); 103 | 104 | view.draw(canvas); 105 | 106 | return bitmap; 107 | } 108 | 109 | public static Bitmap screenshotWithWallpaper(Activity activity) { 110 | //View activityView = activity.findViewById(android.R.id.content).getRootView(); 111 | View activityView = activity.getWindow().getDecorView(); 112 | Rect frame = new Rect(); 113 | activityView.getWindowVisibleDisplayFrame(frame); 114 | return screenshotWithWallpaper(activity, activityView, frame); 115 | } 116 | 117 | public static boolean screenshotToFileWithWallpaper(Activity activity, File file, 118 | Bitmap.CompressFormat compressFormat, int quality) { 119 | return saveBitmap(screenshotWithWallpaper(activity), file, compressFormat, quality); 120 | } 121 | 122 | public static boolean screenshotToFileWithWallpaper(Activity activity, File file) { 123 | return screenshotToFileWithWallpaper(activity, file, Bitmap.CompressFormat.JPEG, 95); 124 | } 125 | 126 | public static boolean screenshotToFileWithWallpaperIf(Activity activity, File file, 127 | Bitmap.CompressFormat compressFormat, 128 | int quality) { 129 | if (ThemeUtils.isWallpaperTheme(activity)) { 130 | return screenshotToFileWithWallpaper(activity, file, compressFormat, quality); 131 | } else { 132 | return screenshotToFile(activity, file, compressFormat, quality); 133 | } 134 | } 135 | 136 | public static boolean screenshotToFileWithWallpaperIf(Activity activity, File file) { 137 | return screenshotToFileWithWallpaperIf(activity, file, Bitmap.CompressFormat.JPEG, 95); 138 | } 139 | 140 | 141 | private ScreenshotUtils() {} 142 | } 143 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/CheatSheetUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | /* 7 | * From https://gist.github.com/romannurik/3982005 8 | */ 9 | 10 | /* 11 | * Copyright 2012 Google Inc. 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | */ 25 | 26 | package me.zhanghai.android.androidutil.util; 27 | 28 | import android.content.Context; 29 | import android.graphics.Rect; 30 | import android.text.TextUtils; 31 | import android.view.Gravity; 32 | import android.view.View; 33 | import android.widget.Toast; 34 | 35 | /** 36 | * Helper class for showing cheat sheets (tooltips) for icon-only UI elements on long-press. This is 37 | * already default platform behavior for icon-only {@link android.app.ActionBar} items and tabs. 38 | * This class provides this behavior for any other such UI element. 39 | * 40 | *

Based on the original action bar implementation in 41 | * ActionMenuItemView.java. 42 | */ 43 | public class CheatSheetUtils { 44 | 45 | /** 46 | * The estimated height of a toast, in dips (density-independent pixels). This is used to 47 | * determine whether or not the toast should appear above or below the UI element. 48 | */ 49 | private static final int ESTIMATED_TOAST_HEIGHT_DIPS = 48; 50 | 51 | /** 52 | * Sets up a cheat sheet (tooltip) for the given view by setting its {@link 53 | * View.OnLongClickListener}. When the view is long-pressed, a {@link Toast} with 54 | * the view's {@link View#getContentDescription() content description} will be 55 | * shown either above (default) or below the view (if there isn't room above it). 56 | * 57 | * @param view The view to add a cheat sheet for. 58 | */ 59 | public static void setup(View view) { 60 | view.setOnLongClickListener(new View.OnLongClickListener() { 61 | @Override 62 | public boolean onLongClick(View view) { 63 | return show(view, view.getContentDescription()); 64 | } 65 | }); 66 | } 67 | 68 | /** 69 | * Sets up a cheat sheet (tooltip) for the given view by setting its {@link 70 | * View.OnLongClickListener}. When the view is long-pressed, a {@link Toast} with 71 | * the given text will be shown either above (default) or below the view (if there isn't room 72 | * above it). 73 | * 74 | * @param view The view to add a cheat sheet for. 75 | * @param textResId The string resource containing the text to show on long-press. 76 | */ 77 | public static void setup(View view, final int textResId) { 78 | view.setOnLongClickListener(new View.OnLongClickListener() { 79 | @Override 80 | public boolean onLongClick(View view) { 81 | return show(view, view.getContext().getString(textResId)); 82 | } 83 | }); 84 | } 85 | 86 | /** 87 | * Sets up a cheat sheet (tooltip) for the given view by setting its {@link 88 | * View.OnLongClickListener}. When the view is long-pressed, a {@link Toast} with 89 | * the given text will be shown either above (default) or below the view (if there isn't room 90 | * above it). 91 | * 92 | * @param view The view to add a cheat sheet for. 93 | * @param text The text to show on long-press. 94 | */ 95 | public static void setup(View view, final CharSequence text) { 96 | view.setOnLongClickListener(new View.OnLongClickListener() { 97 | @Override 98 | public boolean onLongClick(View view) { 99 | return show(view, text); 100 | } 101 | }); 102 | } 103 | 104 | /** 105 | * Removes the cheat sheet for the given view by removing the view's {@link 106 | * View.OnLongClickListener}. 107 | * 108 | * @param view The view whose cheat sheet should be removed. 109 | */ 110 | public static void remove(final View view) { 111 | view.setOnLongClickListener(null); 112 | } 113 | 114 | /** 115 | * Internal helper method to show the cheat sheet toast. 116 | */ 117 | private static boolean show(View view, CharSequence text) { 118 | if (TextUtils.isEmpty(text)) { 119 | return false; 120 | } 121 | 122 | final int[] screenPos = new int[2]; // origin is device display 123 | final Rect displayFrame = new Rect(); // includes decorations (e.g. status bar) 124 | view.getLocationOnScreen(screenPos); 125 | view.getWindowVisibleDisplayFrame(displayFrame); 126 | 127 | final Context context = view.getContext(); 128 | final int viewWidth = view.getWidth(); 129 | final int viewHeight = view.getHeight(); 130 | final int viewCenterX = screenPos[0] + viewWidth / 2; 131 | final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 132 | final int estimatedToastHeight = (int) (ESTIMATED_TOAST_HEIGHT_DIPS 133 | * context.getResources().getDisplayMetrics().density); 134 | 135 | Toast cheatSheet = Toast.makeText(context, text, Toast.LENGTH_SHORT); 136 | boolean showBelow = screenPos[1] < estimatedToastHeight; 137 | if (showBelow) { 138 | // Show below 139 | // Offsets are after decorations (e.g. status bar) are factored in 140 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 141 | viewCenterX - screenWidth / 2, 142 | screenPos[1] - displayFrame.top + viewHeight); 143 | } else { 144 | // Show above 145 | // Offsets are after decorations (e.g. status bar) are factored in 146 | // NOTE: We can't use Gravity.BOTTOM because when the keyboard is up 147 | // its height isn't factored in. 148 | cheatSheet.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 149 | viewCenterX - screenWidth / 2, 150 | screenPos[1] - displayFrame.top - estimatedToastHeight); 151 | } 152 | 153 | cheatSheet.show(); 154 | return true; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/AccountPrefsUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.accounts.Account; 9 | import android.content.Context; 10 | import android.content.SharedPreferences; 11 | 12 | import java.security.GeneralSecurityException; 13 | import java.util.Set; 14 | 15 | /** 16 | * Utility class providing one-line access to {@link AccountPreferences} functionality. 17 | */ 18 | public class AccountPrefsUtils { 19 | 20 | private AccountPrefsUtils() {} 21 | 22 | /** 23 | * @see AccountPreferences#getString(String, String) 24 | */ 25 | public static String getString(String key, String defaultValue, Account account, 26 | Context context) { 27 | return AccountPreferences.from(account, context).getString(key, defaultValue); 28 | } 29 | 30 | /** 31 | * @see AccountPreferences#getStringSet(String, Set) 32 | */ 33 | public static Set getStringSet(String key, Set defaultValue, Account account, 34 | Context context) { 35 | return AccountPreferences.from(account, context).getStringSet(key, defaultValue); 36 | } 37 | 38 | /** 39 | * @see AccountPreferences#getInt(String, int) 40 | */ 41 | public static int getInt(String key, int defaultValue, Account account, Context context) { 42 | return AccountPreferences.from(account, context).getInt(key, defaultValue); 43 | } 44 | 45 | /** 46 | * @see AccountPreferences#getLong(String, long) 47 | */ 48 | public static long getLong(String key, long defaultValue, Account account, Context context) { 49 | return AccountPreferences.from(account, context).getLong(key, defaultValue); 50 | } 51 | 52 | /** 53 | * @see AccountPreferences#getFloat(String, float) 54 | */ 55 | public static float getFloat(String key, float defaultValue, Account account, Context context) { 56 | return AccountPreferences.from(account, context).getFloat(key, defaultValue); 57 | } 58 | 59 | /** 60 | * @see AccountPreferences#getBoolean(String, boolean) 61 | */ 62 | public static boolean getBoolean(String key, boolean defaultValue, Account account, 63 | Context context) { 64 | return AccountPreferences.from(account, context).getBoolean(key, defaultValue); 65 | } 66 | 67 | /** 68 | * @see AccountPreferences#contains(String) 69 | */ 70 | public static boolean contains(String key, Account account, Context context) { 71 | return AccountPreferences.from(account, context).contains(key); 72 | } 73 | 74 | /** 75 | * @see AccountPreferences#putString(String, String) 76 | */ 77 | public static void putString(String key, String value, Account account, Context context) { 78 | AccountPreferences.from(account, context).putString(key, value); 79 | } 80 | 81 | /** 82 | * @see AccountPreferences#putStringSet(String, Set) 83 | */ 84 | public static void putStringSet(String key, Set value, Account account, 85 | Context context) { 86 | AccountPreferences.from(account, context).putStringSet(key, value); 87 | } 88 | 89 | /** 90 | * @see AccountPreferences#putInt(String, int) 91 | */ 92 | public static void putInt(String key, int value, Account account, Context context) { 93 | AccountPreferences.from(account, context).putInt(key, value); 94 | } 95 | 96 | /** 97 | * @see AccountPreferences#putLong(String, long) 98 | */ 99 | public static void putLong(String key, long value, Account account, Context context) { 100 | AccountPreferences.from(account, context).putLong(key, value); 101 | } 102 | 103 | /** 104 | * @see AccountPreferences#putFloat(String, float) 105 | */ 106 | public static void putFloat(String key, float value, Account account, Context context) { 107 | AccountPreferences.from(account, context).putFloat(key, value); 108 | } 109 | 110 | /** 111 | * @see AccountPreferences#putBoolean(String, boolean) 112 | */ 113 | public static void putBoolean(String key, boolean value, Account account, Context context) { 114 | AccountPreferences.from(account, context).putBoolean(key, value); 115 | } 116 | 117 | /** 118 | * Get an {@link String} that will be deobfuscated by 119 | * {@link SecurityUtils#deobfuscate(String, Context)}. 120 | * 121 | * @see AccountPreferences#getString(String, String) 122 | * @see SecurityUtils#deobfuscate(String, Context) 123 | */ 124 | public static String getStringObfuscated(String key, String defaultValue, Account account, 125 | Context context) { 126 | String value = getString(key, null, account, context); 127 | if (value != null) { 128 | try { 129 | return SecurityUtils.deobfuscate(value, context); 130 | } catch (GeneralSecurityException e) { 131 | BuildUtils.throwOrPrint(e); 132 | } 133 | } 134 | return defaultValue; 135 | } 136 | 137 | /** 138 | * Put an {@link String} that will be obfuscated by 139 | * {@link SecurityUtils#obfuscateIfNotNull(String, Context)}. 140 | * 141 | * @see AccountPreferences#putString(String, String) 142 | * @see SecurityUtils#obfuscateIfNotNull(String, Context) 143 | */ 144 | public static void putStringObfuscated(String key, String value, Account account, 145 | Context context) { 146 | putString(key, SecurityUtils.obfuscateIfNotNull(value, context), account, context); 147 | } 148 | 149 | /** 150 | * @see AccountPreferences#remove(String) 151 | */ 152 | public static void remove(String key, Account account, Context context) { 153 | AccountPreferences.from(account, context).remove(key); 154 | } 155 | 156 | /** 157 | * @see AccountPreferences#registerOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener) 158 | */ 159 | public static void registerOnAccountMetadataChangedListener( 160 | AccountPreferences.OnSharedPreferenceChangeListener listener, Account account, 161 | Context context) { 162 | AccountPreferences.from(account, context) 163 | .registerOnSharedPreferenceChangeListener(listener); 164 | } 165 | 166 | /** 167 | * @see AccountPreferences#unregisterOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener) 168 | */ 169 | public static void unregisterOnAccountMetadataChangedListener( 170 | AccountPreferences.OnSharedPreferenceChangeListener listener, Account account, 171 | Context context) { 172 | AccountPreferences.from(account, context) 173 | .unregisterOnSharedPreferenceChangeListener(listener); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/TimeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | 10 | import org.threeten.bp.DayOfWeek; 11 | import org.threeten.bp.Instant; 12 | import org.threeten.bp.LocalDate; 13 | import org.threeten.bp.LocalDateTime; 14 | import org.threeten.bp.LocalTime; 15 | import org.threeten.bp.ZoneId; 16 | import org.threeten.bp.ZoneOffset; 17 | import org.threeten.bp.ZonedDateTime; 18 | import org.threeten.bp.format.DateTimeFormatter; 19 | import org.threeten.bp.temporal.ChronoUnit; 20 | 21 | import java.util.concurrent.TimeUnit; 22 | 23 | public class TimeUtils { 24 | 25 | public static final ZoneOffset ZJU_ZONE_OFFSET = ZoneOffset.of("+08:00"); 26 | 27 | public static final ZoneId ZJU_ZONE_ID = ZoneId.ofOffset("GMT", ZJU_ZONE_OFFSET); 28 | 29 | public static long toEpochMilli(ZonedDateTime zonedDateTime) { 30 | return zonedDateTime.toInstant().toEpochMilli(); 31 | } 32 | 33 | public static long toEpochSecondInZju(LocalDateTime dateTime) { 34 | return dateTime.toEpochSecond(ZJU_ZONE_OFFSET); 35 | } 36 | 37 | public static long toEpochSecondInZju(LocalDate date, LocalTime time) { 38 | return toEpochSecondInZju(LocalDateTime.of(date, time)); 39 | } 40 | 41 | public static ZonedDateTime toZonedDateTime(long epochSecond, ZoneId zoneId) { 42 | return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), zoneId); 43 | } 44 | 45 | public static ZonedDateTime toZonedDateTimeInSystem(long epochSecond) { 46 | return toZonedDateTime(epochSecond, ZoneId.systemDefault()); 47 | } 48 | 49 | public static ZonedDateTime toZonedDateTimeInZju(long epochSecond) { 50 | return toZonedDateTime(epochSecond, ZJU_ZONE_ID); 51 | } 52 | 53 | public static ZonedDateTime toZonedDateTimeInSystem(LocalDate date, LocalTime time) { 54 | return ZonedDateTime.of(date, time, ZoneId.systemDefault()); 55 | } 56 | 57 | public static ZonedDateTime toZonedDateTimeInZju(LocalDate date, LocalTime time) { 58 | return ZonedDateTime.of(date, time, ZJU_ZONE_ID); 59 | } 60 | 61 | public static ZonedDateTime toZonedDateTimeInSystem(Instant instant) { 62 | return ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); 63 | } 64 | 65 | public static ZonedDateTime toZonedDateTimeInZju(Instant instant) { 66 | return ZonedDateTime.ofInstant(instant, ZJU_ZONE_ID); 67 | } 68 | 69 | public static ZonedDateTime withDayOfWeek(ZonedDateTime zonedDateTime, DayOfWeek dayOfWeek) { 70 | return zonedDateTime.plusDays( 71 | dayOfWeek.getValue() - zonedDateTime.getDayOfWeek().getValue()); 72 | } 73 | 74 | public static ZonedDateTime withWeekStart(ZonedDateTime zonedDateTime) { 75 | return withDayOfWeek(zonedDateTime, DayOfWeek.MONDAY); 76 | } 77 | 78 | public static ZonedDateTime withDayStart(ZonedDateTime zonedDateTime) { 79 | return zonedDateTime.truncatedTo(ChronoUnit.DAYS); 80 | } 81 | 82 | public static ZonedDateTime getTodayStartInSystem() { 83 | return withDayStart(ZonedDateTime.now()); 84 | } 85 | 86 | public static ZonedDateTime getTomorrowStartInSystem() { 87 | return getTodayStartInSystem().plusDays(1); 88 | } 89 | 90 | public static long getNowEpochSecond() { 91 | return Instant.now().getEpochSecond(); 92 | } 93 | 94 | public static long getNowEpochMilli() { 95 | return Instant.now().toEpochMilli(); 96 | } 97 | 98 | public static ZonedDateTime getNowInZju() { 99 | return ZonedDateTime.now(ZJU_ZONE_ID); 100 | } 101 | 102 | public static long secondToMilli(long second) { 103 | return TimeUnit.SECONDS.toMillis(second); 104 | } 105 | 106 | public static long milliToSecond(long milli) { 107 | return TimeUnit.MILLISECONDS.toSeconds(milli); 108 | } 109 | 110 | public static boolean isInSameDate(ZonedDateTime first, ZonedDateTime second) { 111 | return first.toLocalDate().equals(second.toLocalDate()); 112 | } 113 | 114 | public static String formatTimestamp(ZonedDateTime zonedDateTime, Context context) { 115 | String pattern = context.getString(R.string.timestamp_pattern); 116 | DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); 117 | return zonedDateTime.format(dateTimeFormatter); 118 | } 119 | 120 | public static String formatTimestamp(Instant instant, Context context) { 121 | return formatTimestamp(toZonedDateTimeInSystem(instant), context); 122 | } 123 | 124 | public String formatTimestamp(long epochSecond, Context context) { 125 | return formatTimestamp(toZonedDateTimeInSystem(epochSecond), context); 126 | } 127 | 128 | public static String formatDuration(long seconds, Context context) { 129 | long days = TimeUnit.SECONDS.toDays(seconds); 130 | seconds -= TimeUnit.DAYS.toSeconds(days); 131 | long hours = TimeUnit.SECONDS.toHours(seconds); 132 | seconds -= TimeUnit.HOURS.toSeconds(hours); 133 | long minutes = TimeUnit.SECONDS.toMinutes(seconds); 134 | seconds -= TimeUnit.MINUTES.toSeconds(minutes); 135 | if (days > 0) { 136 | return context.getString(R.string.duration_with_day_format, days, hours, minutes, 137 | seconds); 138 | } else { 139 | return context.getString(R.string.duration_format, hours, minutes, seconds); 140 | } 141 | } 142 | 143 | private static String getDateTimeFormat(ZonedDateTime dateTime, LocalDate defaultDate, 144 | Context context) { 145 | if (dateTime.toLocalDate().equals(defaultDate)) { 146 | return context.getString(R.string.time_pattern); 147 | } else if (dateTime.getYear() == defaultDate.getYear()) { 148 | return context.getString(R.string.month_day_time_pattern); 149 | } else { 150 | return context.getString(R.string.date_time_pattern); 151 | } 152 | } 153 | 154 | public static String formatPeriod(ZonedDateTime start, ZonedDateTime end, LocalDate defaultDate, 155 | Context context) { 156 | ZonedDateTime startDayStart = TimeUtils.withDayStart(start); 157 | if (start.equals(startDayStart) 158 | && end.equals(startDayStart.plusDays(1))) { 159 | // The period is a whole day, just return the date. 160 | String datePattern = context.getString(R.string.month_day_pattern); 161 | DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(datePattern); 162 | return start.format(dateFormatter); 163 | } else { 164 | String startFormat = getDateTimeFormat(start, defaultDate, context); 165 | DateTimeFormatter startFormatter = DateTimeFormatter.ofPattern(startFormat); 166 | // NOTE: We should use the date part of start time as default date for end time. 167 | String endFormat = getDateTimeFormat(end, start.toLocalDate(), context); 168 | DateTimeFormatter endFormatter = DateTimeFormatter.ofPattern(endFormat); 169 | return context.getString(R.string.period_format, start.format(startFormatter), 170 | end.format(endFormatter)); 171 | } 172 | } 173 | 174 | 175 | private TimeUtils() {} 176 | } 177 | -------------------------------------------------------------------------------- /library/library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/PickerUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.app.Activity; 9 | import android.content.Context; 10 | import android.os.Bundle; 11 | import android.text.TextUtils; 12 | 13 | import com.doomonafireball.betterpickers.calendardatepicker.CalendarDatePickerDialog; 14 | import com.doomonafireball.betterpickers.radialtimepicker.RadialTimePickerDialog; 15 | import com.doomonafireball.betterpickers.recurrencepicker.zh.RecurrencePickerDialog; 16 | import com.doomonafireball.betterpickers.timezonepicker.TimeZoneInfo; 17 | import com.doomonafireball.betterpickers.timezonepicker.TimeZonePickerDialog; 18 | import com.myqsc.mobile3.calendar.info.RecurrenceRule; 19 | import com.myqsc.mobile3.calendar.ui.DayFragmentAdapter; 20 | 21 | import org.threeten.bp.LocalDate; 22 | import org.threeten.bp.LocalTime; 23 | import org.threeten.bp.ZoneId; 24 | import org.threeten.bp.ZonedDateTime; 25 | 26 | public class PickerUtils { 27 | 28 | private static boolean getUseDarkTheme(Context themedContext) { 29 | return ThemeUtils.getBoolean(themedContext, R.attr.betterPickersUseDarkTheme, false); 30 | } 31 | 32 | public static void pickDate(LocalDate presetDate, OnDateSetListener listener, 33 | Activity activity) { 34 | 35 | CalendarDatePickerDialog datePickerDialog = CalendarDatePickerDialog.newInstance( 36 | new OnDateSetListenerWrapper(listener), presetDate.getYear(), 37 | presetDate.getMonthValue() - 1, presetDate.getDayOfMonth()); 38 | 39 | int currentYear = LocalDate.now().getYear(); 40 | int startYear = currentYear - DayFragmentAdapter.YEAR_OFFSET_LIMIT; 41 | int endYear = currentYear + DayFragmentAdapter.YEAR_OFFSET_LIMIT; 42 | datePickerDialog.setYearRange(startYear, endYear); 43 | 44 | datePickerDialog.setThemeDark(getUseDarkTheme(activity)); 45 | 46 | datePickerDialog.show(activity.getFragmentManager(), null); 47 | } 48 | 49 | public static void pickTime(LocalTime presetTime, OnTimeSetListener listener, 50 | Activity activity) { 51 | 52 | RadialTimePickerDialog timePickerDialog = RadialTimePickerDialog.newInstance( 53 | new OnTimeSetListenerWrapper(listener), presetTime.getHour(), 54 | presetTime.getMinute(), true); 55 | 56 | timePickerDialog.setThemeDark(getUseDarkTheme(activity)); 57 | 58 | timePickerDialog.show(activity.getFragmentManager(), null); 59 | } 60 | 61 | public static void pickTimeZone(ZoneId presetTimeZone, OnTimeZoneSetListener listener, 62 | Activity activity) { 63 | 64 | TimeZonePickerDialog timeZonePickerDialog = new TimeZonePickerDialog(); 65 | 66 | Bundle arguments = new Bundle(); 67 | // Set start time to now so users see what the time now will be in other timezones. 68 | arguments.putLong(TimeZonePickerDialog.BUNDLE_START_TIME_MILLIS, 69 | TimeUtils.getNowEpochMilli()); 70 | arguments.putString(TimeZonePickerDialog.BUNDLE_TIME_ZONE, presetTimeZone.getId()); 71 | timeZonePickerDialog.setArguments(arguments); 72 | 73 | timeZonePickerDialog.setOnTimeZoneSetListener(new OnTimeZoneSetListenerWrapper(listener)); 74 | 75 | timeZonePickerDialog.show(activity.getFragmentManager(), null); 76 | } 77 | 78 | // Supply start for appropriate defaults. 79 | public static void pickRecurrenceRule(RecurrenceRule presetRecurrenceRule, ZonedDateTime start, 80 | OnRecurrenceRuleSetListener listener, Activity activity) { 81 | 82 | RecurrencePickerDialog recurrencePickerDialog = new RecurrencePickerDialog(); 83 | 84 | Bundle arguments = new Bundle(); 85 | arguments.putLong(RecurrencePickerDialog.BUNDLE_START_TIME_MILLIS, 86 | TimeUtils.toEpochMilli(start)); 87 | arguments.putString(RecurrencePickerDialog.BUNDLE_TIME_ZONE, start.getZone().getId()); 88 | if (presetRecurrenceRule != null) { 89 | arguments.putString(RecurrencePickerDialog.BUNDLE_RRULE, presetRecurrenceRule.toString()); 90 | } 91 | recurrencePickerDialog.setArguments(arguments); 92 | 93 | recurrencePickerDialog.setOnRecurrenceSetListener(new OnRecurrenceRuleSetListenerWrapper( 94 | listener)); 95 | 96 | recurrencePickerDialog.show(activity.getFragmentManager(), null); 97 | } 98 | 99 | 100 | public interface OnDateSetListener { 101 | public void onDateSet(LocalDate date); 102 | } 103 | 104 | public interface OnTimeSetListener { 105 | public void onTimeSet(LocalTime time); 106 | } 107 | 108 | public interface OnTimeZoneSetListener { 109 | public void onTimeZoneSet(ZoneId timeZone); 110 | } 111 | 112 | public interface OnRecurrenceRuleSetListener { 113 | public void onRecurrenceRuleSet(RecurrenceRule recurrenceRule); 114 | } 115 | 116 | private static class OnDateSetListenerWrapper implements 117 | CalendarDatePickerDialog.OnDateSetListener { 118 | 119 | private OnDateSetListener onDateSetListener; 120 | 121 | public OnDateSetListenerWrapper(OnDateSetListener onDateSetListener) { 122 | this.onDateSetListener = onDateSetListener; 123 | } 124 | 125 | @Override 126 | public void onDateSet(CalendarDatePickerDialog dialog, int year, int month, 127 | int dayOfMonth) { 128 | onDateSetListener.onDateSet(LocalDate.of(year, month + 1, dayOfMonth)); 129 | } 130 | } 131 | 132 | private static class OnTimeSetListenerWrapper implements 133 | RadialTimePickerDialog.OnTimeSetListener { 134 | 135 | private OnTimeSetListener onTimeSetListener; 136 | 137 | public OnTimeSetListenerWrapper(OnTimeSetListener onTimeSetListener) { 138 | this.onTimeSetListener = onTimeSetListener; 139 | } 140 | 141 | @Override 142 | public void onTimeSet(RadialTimePickerDialog radialTimePickerDialog, int hour, int minute) { 143 | onTimeSetListener.onTimeSet(LocalTime.of(hour, minute)); 144 | } 145 | } 146 | 147 | private static class OnTimeZoneSetListenerWrapper 148 | implements TimeZonePickerDialog.OnTimeZoneSetListener { 149 | 150 | private OnTimeZoneSetListener onTimeZoneSetListener; 151 | 152 | public OnTimeZoneSetListenerWrapper(OnTimeZoneSetListener onTimeZoneSetListener) { 153 | this.onTimeZoneSetListener = onTimeZoneSetListener; 154 | } 155 | 156 | @Override 157 | public void onTimeZoneSet(TimeZoneInfo timeZoneInfo) { 158 | onTimeZoneSetListener.onTimeZoneSet(ZoneId.of(timeZoneInfo.mTzId)); 159 | } 160 | } 161 | 162 | private static class OnRecurrenceRuleSetListenerWrapper 163 | implements RecurrencePickerDialog.OnRecurrenceSetListener { 164 | 165 | private OnRecurrenceRuleSetListener onRecurrenceRuleSetListener; 166 | 167 | public OnRecurrenceRuleSetListenerWrapper( 168 | OnRecurrenceRuleSetListener onRecurrenceRuleSetListener) { 169 | this.onRecurrenceRuleSetListener = onRecurrenceRuleSetListener; 170 | } 171 | 172 | @Override 173 | public void onRecurrenceSet(String recurrenceRule) { 174 | LogUtils.i(recurrenceRule); 175 | onRecurrenceRuleSetListener.onRecurrenceRuleSet(TextUtils.isEmpty(recurrenceRule) ? null 176 | : RecurrenceRule.parse(recurrenceRule)); 177 | } 178 | } 179 | 180 | 181 | private PickerUtils() {} 182 | } 183 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/SecurityUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.content.Context; 9 | import android.provider.Settings; 10 | 11 | import java.io.UnsupportedEncodingException; 12 | import java.security.GeneralSecurityException; 13 | import java.security.InvalidAlgorithmParameterException; 14 | import java.security.InvalidKeyException; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.security.SecureRandom; 17 | import java.security.spec.InvalidKeySpecException; 18 | import java.security.spec.KeySpec; 19 | 20 | import javax.crypto.Cipher; 21 | import javax.crypto.Mac; 22 | import javax.crypto.NoSuchPaddingException; 23 | import javax.crypto.SecretKey; 24 | import javax.crypto.SecretKeyFactory; 25 | import javax.crypto.spec.PBEKeySpec; 26 | import javax.crypto.spec.PBEParameterSpec; 27 | import javax.crypto.spec.SecretKeySpec; 28 | 29 | public class SecurityUtils { 30 | 31 | // NOTE: 32 | // See https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/java/security/StandardNames.java 33 | // for supported algorithms of Android. 34 | 35 | private static final String ALGORITHM_NAME_PBKDF2_WITH_HMAC_SHA1 = "PBKDF2WithHmacSHA1"; 36 | private static final String ALGORITHM_NAME_HMAC_SHA1 = "HmacSHA1"; 37 | 38 | private static final String CHARSET_NAME = "UTF-8"; 39 | 40 | private static final String OBFUSCATION_ALGORITHM = "PBEWithMD5AndDES"; 41 | private static final String OBFUSCATION_PASSWORD = "this_is_insecure_enough"; 42 | private static final int OBFUSCATION_ITERATION_COUNT = 32; 43 | 44 | 45 | public static byte[] random(int numberOfBytes) { 46 | SecureRandom secureRandom = new SecureRandom(); 47 | byte[] bytes = new byte[numberOfBytes]; 48 | secureRandom.nextBytes(bytes); 49 | return bytes; 50 | } 51 | 52 | // Implement PBKDF2. 53 | // NOTE: 54 | // There is a bug in Android about this, see: 55 | // https://code.google.com/p/android/issues/detail?id=40578 56 | // One can choose to use SpongyCastle, see: 57 | // https://github.com/rtyley/spongycastle 58 | // But in this case we simply need to use the bug-free Ascii version. 59 | public static byte[] pbkdf2WithHmacSha1(String password, byte[] salt, int iterationCount, 60 | int keyLength) { 61 | 62 | SecretKeyFactory secretKeyFactory; 63 | try { 64 | secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM_NAME_PBKDF2_WITH_HMAC_SHA1); 65 | } catch (NoSuchAlgorithmException e) { 66 | LogUtils.wtf("No such algorithm: " + ALGORITHM_NAME_PBKDF2_WITH_HMAC_SHA1); 67 | throw new RuntimeException(e); 68 | } 69 | KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength); 70 | SecretKey secretKey; 71 | try { 72 | secretKey = secretKeyFactory.generateSecret(keySpec); 73 | } catch (InvalidKeySpecException e) { 74 | LogUtils.wtf("Invalid key specification: " + keySpec); 75 | throw new RuntimeException(e); 76 | } 77 | 78 | return secretKey.getEncoded(); 79 | } 80 | 81 | public static byte[] pbkdf2WithHmacSha1(String password, String salt, int iterationCount, 82 | int keyLength) { 83 | try { 84 | return pbkdf2WithHmacSha1(password, salt.getBytes(CHARSET_NAME), iterationCount, 85 | keyLength); 86 | } catch (UnsupportedEncodingException e) { 87 | LogUtils.wtf("Unsupported encoding: " + CHARSET_NAME); 88 | throw new RuntimeException(e); 89 | } 90 | } 91 | 92 | public static byte[] hmacSha1(byte[] key, byte[] data) { 93 | Mac mac; 94 | try { 95 | mac = Mac.getInstance(ALGORITHM_NAME_HMAC_SHA1); 96 | } catch (NoSuchAlgorithmException e) { 97 | LogUtils.wtf("No such algorithm: " + ALGORITHM_NAME_HMAC_SHA1); 98 | throw new RuntimeException(e); 99 | } 100 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM_NAME_HMAC_SHA1); 101 | try { 102 | mac.init(secretKeySpec); 103 | } catch (InvalidKeyException e) { 104 | LogUtils.wtf("Invalid key: " + secretKeySpec); 105 | throw new RuntimeException(e); 106 | } 107 | return mac.doFinal(data); 108 | } 109 | 110 | public static byte[] hmacSha1(String key, String data) { 111 | try { 112 | return hmacSha1(key.getBytes(CHARSET_NAME), data.getBytes(CHARSET_NAME)); 113 | } catch (UnsupportedEncodingException e) { 114 | LogUtils.wtf("Unsupported encoding: " + CHARSET_NAME); 115 | throw new RuntimeException(e); 116 | } 117 | } 118 | 119 | // NOTICE: This function is null-intolerant. 120 | public static String obfuscate(String value, Context context) { 121 | try { 122 | return doObfuscation(value, Cipher.ENCRYPT_MODE, context); 123 | } catch (GeneralSecurityException e) { 124 | // Should never happen. 125 | throw new RuntimeException(e); 126 | } 127 | } 128 | 129 | public static String obfuscateIfNotNull(String value, Context context) { 130 | return value == null ? null : obfuscate(value, context); 131 | } 132 | 133 | // NOTICE: This function is null-intolerant. 134 | public static String deobfuscate(String value, Context context) 135 | throws GeneralSecurityException { 136 | return doObfuscation(value, Cipher.DECRYPT_MODE, context); 137 | } 138 | 139 | private static String doObfuscation(String value, int cipherMode, Context context) 140 | throws GeneralSecurityException { 141 | 142 | try { 143 | 144 | byte[] bytes; 145 | switch (cipherMode) { 146 | case Cipher.ENCRYPT_MODE: 147 | bytes = value.getBytes(CHARSET_NAME); 148 | break; 149 | case Cipher.DECRYPT_MODE: 150 | bytes = IoUtils.base64ToByteArray(value); 151 | break; 152 | default: 153 | throw new IllegalArgumentException("Unexpected cipherMode: " + cipherMode); 154 | } 155 | 156 | SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(OBFUSCATION_ALGORITHM); 157 | SecretKey key = keyFactory.generateSecret( 158 | new PBEKeySpec(OBFUSCATION_PASSWORD.toCharArray())); 159 | 160 | Cipher cipher = Cipher.getInstance(OBFUSCATION_ALGORITHM); 161 | byte[] salt = Settings.Secure.getString(context.getContentResolver(), 162 | Settings.Secure.ANDROID_ID).getBytes(CHARSET_NAME); 163 | cipher.init(cipherMode, key, 164 | new PBEParameterSpec(salt, OBFUSCATION_ITERATION_COUNT)); 165 | 166 | byte[] finalBytes = cipher.doFinal(bytes); 167 | switch (cipherMode) { 168 | case Cipher.ENCRYPT_MODE: 169 | return IoUtils.byteArrayToBase64(finalBytes); 170 | case Cipher.DECRYPT_MODE: 171 | return new String(finalBytes, CHARSET_NAME); 172 | default: 173 | throw new IllegalArgumentException("Unexpected cipherMode: " + cipherMode); 174 | } 175 | 176 | } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException 177 | | InvalidAlgorithmParameterException | InvalidKeyException 178 | | UnsupportedEncodingException e) { 179 | 180 | // Should never happen. 181 | throw new RuntimeException(e); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/DrawableUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.animation.ObjectAnimator; 9 | import android.animation.TypeEvaluator; 10 | import android.animation.ValueAnimator; 11 | import android.content.Context; 12 | import android.graphics.Bitmap; 13 | import android.graphics.BitmapFactory; 14 | import android.graphics.Color; 15 | import android.graphics.ColorMatrix; 16 | import android.graphics.ColorMatrixColorFilter; 17 | import android.graphics.LinearGradient; 18 | import android.graphics.Shader; 19 | import android.graphics.drawable.Drawable; 20 | import android.graphics.drawable.PaintDrawable; 21 | import android.graphics.drawable.ShapeDrawable; 22 | import android.graphics.drawable.shapes.RectShape; 23 | import android.net.Uri; 24 | import android.support.annotation.Keep; 25 | import android.view.Gravity; 26 | 27 | import java.io.InputStream; 28 | 29 | public class DrawableUtils { 30 | 31 | private DrawableUtils() {} 32 | 33 | public static Bitmap decodeBitmapFromUri(Uri uri, Context context) { 34 | 35 | InputStream inputStream = UriUtils.openInputStream(uri, context); 36 | if (inputStream == null) { 37 | return null; 38 | } 39 | BitmapFactory.Options options = new BitmapFactory.Options(); 40 | options.inJustDecodeBounds = true; 41 | BitmapFactory.decodeStream(inputStream, null, options); 42 | IoUtils.close(inputStream); 43 | if (!(options.outWidth > 0 && options.outHeight > 0)) { 44 | return null; 45 | } 46 | 47 | inputStream = UriUtils.openInputStream(uri, context); 48 | if (inputStream == null) { 49 | return null; 50 | } 51 | // Canvas.getMaximumBitmapWidth() needs a hardware accelerated canvas to produce the right 52 | // result, so we simply use 2048x2048 instead. 53 | options.inSampleSize = computeInSampleSize(options, 2048, 2048); 54 | options.inJustDecodeBounds = false; 55 | Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); 56 | IoUtils.close(inputStream); 57 | 58 | return bitmap; 59 | } 60 | 61 | private static int computeInSampleSize(BitmapFactory.Options options, int maxWidth, 62 | int maxHeight) { 63 | 64 | int width = options.outWidth; 65 | int height = options.outHeight; 66 | int inSampleSize = 1; 67 | 68 | while (width > maxWidth || height > maxHeight) { 69 | inSampleSize *= 2; 70 | width /= 2; 71 | height /= 2; 72 | } 73 | 74 | return inSampleSize; 75 | } 76 | 77 | // From Muzei, Copyright 2014 Google Inc. 78 | public static Drawable makeScrimDrawable(int baseColor, int numStops, int gravity) { 79 | 80 | numStops = Math.max(numStops, 2); 81 | 82 | PaintDrawable paintDrawable = new PaintDrawable(); 83 | paintDrawable.setShape(new RectShape()); 84 | 85 | final int[] stopColors = new int[numStops]; 86 | 87 | int red = Color.red(baseColor); 88 | int green = Color.green(baseColor); 89 | int blue = Color.blue(baseColor); 90 | int alpha = Color.alpha(baseColor); 91 | 92 | for (int i = 0; i < numStops; i++) { 93 | float x = i * 1f / (numStops - 1); 94 | float opacity = MathUtils.constrain(0, 1, (float) Math.pow(x, 3)); 95 | stopColors[i] = Color.argb((int) (alpha * opacity), red, green, blue); 96 | } 97 | 98 | final float x0, x1, y0, y1; 99 | switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 100 | case Gravity.LEFT: 101 | x0 = 1; 102 | x1 = 0; 103 | break; 104 | case Gravity.RIGHT: 105 | x0 = 0; 106 | x1 = 1; 107 | break; 108 | default: 109 | x0 = 0; 110 | x1 = 0; 111 | break; 112 | } 113 | switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { 114 | case Gravity.TOP: 115 | y0 = 1; 116 | y1 = 0; 117 | break; 118 | case Gravity.BOTTOM: 119 | y0 = 0; 120 | y1 = 1; 121 | break; 122 | default: 123 | y0 = 0; 124 | y1 = 0; 125 | break; 126 | } 127 | 128 | paintDrawable.setShaderFactory(new ShapeDrawable.ShaderFactory() { 129 | @Override 130 | public Shader resize(int width, int height) { 131 | return new LinearGradient( 132 | width * x0, 133 | height * y0, 134 | width * x1, 135 | height * y1, 136 | stopColors, null, 137 | Shader.TileMode.CLAMP); 138 | } 139 | }); 140 | 141 | paintDrawable.setAlpha(Math.round(0.4f * 255)); 142 | 143 | return paintDrawable; 144 | } 145 | 146 | public static Drawable makeScrimDrawable(int gravity) { 147 | return makeScrimDrawable(Color.BLACK, 9, gravity); 148 | } 149 | 150 | public static Drawable makeScrimDrawable() { 151 | return makeScrimDrawable(Gravity.BOTTOM); 152 | } 153 | 154 | public static void animateLoading(final Drawable drawable, int duration) { 155 | 156 | ImageLoadingEvaluator evaluator = new ImageLoadingEvaluator(); 157 | evaluator.evaluate(0, null, null); 158 | final AnimateColorMatrixColorFilter filter = new AnimateColorMatrixColorFilter( 159 | evaluator.getColorMatrix()); 160 | 161 | drawable.setColorFilter(filter.getColorFilter()); 162 | ObjectAnimator animator = ObjectAnimator.ofObject(filter, "colorMatrix", evaluator, 163 | evaluator.getColorMatrix() /* Dummy */); 164 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 165 | @Override 166 | public void onAnimationUpdate(ValueAnimator animation) { 167 | drawable.setColorFilter(filter.getColorFilter()); 168 | } 169 | }); 170 | animator.setDuration(duration); 171 | animator.start(); 172 | } 173 | 174 | public static void animateLoading(Drawable drawable, Context context) { 175 | animateLoading(drawable, 176 | context.getResources().getInteger(android.R.integer.config_longAnimTime)); 177 | } 178 | 179 | private static class ImageLoadingEvaluator implements TypeEvaluator { 180 | 181 | private ColorMatrix colorMatrix = new ColorMatrix(); 182 | private float[] elements = new float[20]; 183 | 184 | public ColorMatrix getColorMatrix() { 185 | return colorMatrix; 186 | } 187 | 188 | @Override 189 | public ColorMatrix evaluate(float fraction, ColorMatrix startValue, ColorMatrix endValue) { 190 | 191 | // There are 3 phases so we multiply fraction by that amount 192 | float phase = fraction * 3; 193 | 194 | // Compute the alpha change over period [0, 2] 195 | float alpha = Math.min(phase, 2f) / 2f; 196 | elements [19] = (float) Math.round(alpha * 255); 197 | 198 | // We subtract to make the picture look darker, it will automatically clamp 199 | // This is spread over period [0, 2.5] 200 | final int MaxBlacker = 100; 201 | float blackening = (float) Math.round((1 - Math.min(phase, 2.5f) / 2.5f) * MaxBlacker); 202 | elements [4] = elements [9] = elements [14] = -blackening; 203 | 204 | // Finally we desaturate over [0, 3], taken from ColorMatrix.setSaturation 205 | float invSat = 1 - Math.max(0.2f, fraction); 206 | float R = 0.213f * invSat; 207 | float G = 0.715f * invSat; 208 | float B = 0.072f * invSat; 209 | elements[0] = R + fraction; elements[1] = G; elements[2] = B; 210 | elements[5] = R; elements[6] = G + fraction; elements[7] = B; 211 | elements[10] = R; elements[11] = G; elements[12] = B + fraction; 212 | 213 | colorMatrix.set(elements); 214 | return colorMatrix; 215 | } 216 | } 217 | 218 | private static class AnimateColorMatrixColorFilter { 219 | 220 | private ColorMatrixColorFilter filter; 221 | private ColorMatrix matrix; 222 | 223 | public AnimateColorMatrixColorFilter(ColorMatrix matrix) { 224 | setColorMatrix(matrix); 225 | } 226 | 227 | public ColorMatrixColorFilter getColorFilter() { 228 | return filter; 229 | } 230 | 231 | @Keep 232 | public ColorMatrix getColorMatrix() { 233 | return matrix; 234 | } 235 | 236 | @Keep 237 | public void setColorMatrix(ColorMatrix matrix) { 238 | this.matrix = matrix; 239 | filter = new ColorMatrixColorFilter(matrix); 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/DragUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.animation.Animator; 9 | import android.animation.AnimatorListenerAdapter; 10 | import android.animation.ObjectAnimator; 11 | import android.animation.ValueAnimator; 12 | import android.view.DragEvent; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.view.animation.AccelerateDecelerateInterpolator; 16 | 17 | // TODO: Provide scrolling at edge, see 18 | // https://github.com/justasm/DragLinearLayout/blob/master/library/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java 19 | // and 20 | // https://github.com/nhaarman/ListViewAnimations/blob/master/lib-manipulation/src/main/java/com/nhaarman/listviewanimations/itemmanipulation/dragdrop/DragAndDropHandler.java 21 | public class DragUtils { 22 | 23 | private DragUtils() {} 24 | 25 | public static void setupDragSort(View view, final DragListener listener) { 26 | view.setOnDragListener(new View.OnDragListener() { 27 | @Override 28 | public boolean onDrag(final View view, DragEvent event) { 29 | ViewGroup viewGroup = (ViewGroup)view.getParent(); 30 | DragState dragState = (DragState)event.getLocalState(); 31 | switch (event.getAction()) { 32 | case DragEvent.ACTION_DRAG_STARTED: 33 | if (view == dragState.view) { 34 | view.setVisibility(View.INVISIBLE); 35 | listener.onDragStarted(); 36 | } 37 | return true; 38 | case DragEvent.ACTION_DRAG_LOCATION: { 39 | if (view == dragState.view){ 40 | break; 41 | } 42 | int index = viewGroup.indexOfChild(view); 43 | if ((index > dragState.index && event.getY() > view.getHeight() / 2) 44 | || (index < dragState.index && event.getY() < view.getHeight() / 2)) { 45 | swapViews(viewGroup, view, index, dragState); 46 | } else { 47 | swapViewsBetweenIfNeeded(viewGroup, index, dragState); 48 | } 49 | break; 50 | } 51 | case DragEvent.ACTION_DRAG_ENDED: 52 | if (view == dragState.view) { 53 | view.setVisibility(View.VISIBLE); 54 | listener.onDragEnded(); 55 | } 56 | break; 57 | } 58 | return true; 59 | } 60 | }); 61 | view.setOnLongClickListener(new View.OnLongClickListener() { 62 | @Override 63 | public boolean onLongClick(View view) { 64 | view.startDrag(null, new View.DragShadowBuilder(view), new DragState(view), 0); 65 | return true; 66 | } 67 | }); 68 | } 69 | 70 | public static void setupDragDelete(View view, final ViewGroup viewGroup, 71 | final OnDragDeletedListener listener) { 72 | view.setOnDragListener(new View.OnDragListener() { 73 | @Override 74 | public boolean onDrag(View view, DragEvent event) { 75 | switch (event.getAction()) { 76 | case DragEvent.ACTION_DRAG_ENTERED: 77 | view.setActivated(true); 78 | break; 79 | case DragEvent.ACTION_DRAG_EXITED: 80 | view.setActivated(false); 81 | break; 82 | case DragEvent.ACTION_DROP: 83 | DragState dragState = (DragState)event.getLocalState(); 84 | removeView(viewGroup, dragState); 85 | listener.onDragDeleted(); 86 | break; 87 | case DragEvent.ACTION_DRAG_ENDED: 88 | // NOTE: Needed because ACTION_DRAG_EXITED may not be sent when the drag 89 | // ends within the view. 90 | view.setActivated(false); 91 | break; 92 | } 93 | return true; 94 | } 95 | }); 96 | } 97 | 98 | private static void swapViewsBetweenIfNeeded(ViewGroup viewGroup, int index, 99 | DragState dragState) { 100 | if (index - dragState.index > 1) { 101 | int indexAbove = index - 1; 102 | swapViews(viewGroup, viewGroup.getChildAt(indexAbove), indexAbove, dragState); 103 | } else if (dragState.index - index > 1) { 104 | int indexBelow = index + 1; 105 | swapViews(viewGroup, viewGroup.getChildAt(indexBelow), indexBelow, dragState); 106 | } 107 | } 108 | 109 | private static void swapViews(ViewGroup viewGroup, final View view, int index, 110 | DragState dragState) { 111 | swapViewsBetweenIfNeeded(viewGroup, index, dragState); 112 | final float viewY = view.getY(); 113 | AppUtils.swapViewGroupChildren(viewGroup, view, dragState.view); 114 | dragState.index = index; 115 | AppUtils.postOnPreDraw(view, new Runnable() { 116 | @Override 117 | public void run() { 118 | ObjectAnimator 119 | .ofFloat(view, View.Y, viewY, view.getTop()) 120 | .setDuration(getDuration(view)) 121 | .start(); 122 | } 123 | }); 124 | } 125 | 126 | private static void removeView(final ViewGroup viewGroup, DragState dragState) { 127 | 128 | final int oldViewGroupLayoutParamsHeight = viewGroup.getLayoutParams().height; 129 | final int oldViewGroupHeight = viewGroup.getHeight(); 130 | viewGroup.removeView(dragState.view); 131 | 132 | int childCount = viewGroup.getChildCount(); 133 | for (int i = dragState.index; i < childCount; ++i) { 134 | final View view = viewGroup.getChildAt(i); 135 | final float viewY = view.getY(); 136 | AppUtils.postOnPreDraw(view, new Runnable() { 137 | @Override 138 | public void run() { 139 | ObjectAnimator 140 | .ofFloat(view, View.Y, viewY, view.getTop()) 141 | .setDuration(getDuration(view)) 142 | .start(); 143 | } 144 | }); 145 | } 146 | 147 | final int newViewGroupHeight = measureViewGroupHeight(viewGroup); 148 | if (viewGroup.getChildCount() > 0) { 149 | // Prevent the flash of the new height before the start of our animation. 150 | AppUtils.setViewLayoutParamsHeight(viewGroup, oldViewGroupHeight); 151 | // Wait until the OnPreDraw of the last child is called for syncing the two animations on 152 | // View and ViewGroup. 153 | AppUtils.postOnPreDraw(viewGroup.getChildAt(viewGroup.getChildCount() - 1), new Runnable() { 154 | @Override 155 | public void run() { 156 | animateViewGroupHeight(viewGroup, oldViewGroupLayoutParamsHeight, 157 | oldViewGroupHeight, newViewGroupHeight); 158 | } 159 | }); 160 | } else { 161 | // Animate now since there is no children. 162 | animateViewGroupHeight(viewGroup, oldViewGroupLayoutParamsHeight, oldViewGroupHeight, 163 | newViewGroupHeight); 164 | } 165 | } 166 | 167 | private static int measureViewGroupHeight(ViewGroup viewGroup) { 168 | View parent = (View)viewGroup.getParent(); 169 | int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( 170 | parent.getMeasuredWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), 171 | View.MeasureSpec.AT_MOST); 172 | int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 173 | viewGroup.measure(widthMeasureSpec, heightMeasureSpec); 174 | return viewGroup.getMeasuredHeight(); 175 | } 176 | 177 | private static void animateViewGroupHeight(final ViewGroup viewGroup, 178 | final int oldLayoutParamsHeight, int oldHeight, 179 | int newHeight) { 180 | ValueAnimator viewGroupAnimator = ValueAnimator 181 | .ofInt(oldHeight, newHeight) 182 | .setDuration(getDuration(viewGroup)); 183 | viewGroupAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 184 | viewGroupAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 185 | @Override 186 | public void onAnimationUpdate(ValueAnimator animation) { 187 | int animatedValue = (int) animation.getAnimatedValue(); 188 | AppUtils.setViewLayoutParamsHeight(viewGroup, animatedValue); 189 | } 190 | }); 191 | viewGroupAnimator.addListener(new AnimatorListenerAdapter() { 192 | @Override 193 | public void onAnimationEnd(Animator animation) { 194 | AppUtils.setViewLayoutParamsHeight(viewGroup, oldLayoutParamsHeight); 195 | } 196 | }); 197 | viewGroupAnimator.start(); 198 | } 199 | 200 | private static int getDuration(View view) { 201 | return view.getResources().getInteger(android.R.integer.config_shortAnimTime); 202 | } 203 | 204 | public interface DragListener { 205 | void onDragStarted(); 206 | void onDragEnded(); 207 | } 208 | 209 | public interface OnDragDeletedListener { 210 | void onDragDeleted(); 211 | } 212 | 213 | private static class DragState { 214 | 215 | public View view; 216 | public int index; 217 | 218 | private DragState(View view) { 219 | this.view = view; 220 | index = ((ViewGroup)view.getParent()).indexOfChild(view); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/AccountPreferences.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.accounts.Account; 9 | import android.accounts.AccountManager; 10 | import android.content.Context; 11 | import android.content.SharedPreferences; 12 | import android.os.Handler; 13 | import android.os.Looper; 14 | 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.HashSet; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.WeakHashMap; 21 | 22 | /** 23 | * An {@link Account} based {@link SharedPreferences} implementation using 24 | * {@link AccountManager#getUserData(Account, String)} and 25 | * {@link AccountManager#setUserData(Account, String, String)}. 26 | * 27 | *

Due to the limitation of {@link AccountManager}, we can only access a value by its key, but we 28 | * cannot access the key set or the entire map. So {@link #getAll()} and {@link #clear()} are 29 | * unsupported, {@link #edit()}, {@link #commit()} and {@link #apply()} are stub, and changes are 30 | * committed immediately.

31 | * 32 | *

Also due to the limitation of {@link AccountManager}, all values are stored as {@link String}, 33 | * so {@link #putStringSet(String, Set)} needs to flatten the {@link String} array into one 34 | * {@link String}, in this case the character bar ('|') is used as the delimiter.

35 | * 36 | * @see SharedPreferences 37 | * @see AccountManager 38 | */ 39 | public class AccountPreferences implements SharedPreferences, SharedPreferences.Editor { 40 | 41 | private static final String TRUE_STRING = "true"; 42 | private static final String FALSE_STRING = "false"; 43 | 44 | private static final Object INSTANCES_LOCK = new Object(); 45 | // NOTE: Not using WeakHashMap and WeakReference for instance map because we don't won't to 46 | // lose registered listeners. 47 | private static final Map INSTANCES = new HashMap<>(); 48 | 49 | private Account account; 50 | private AccountManager accountManager; 51 | private Handler mainHandler = new Handler(Looper.getMainLooper()); 52 | private Set listeners = Collections.newSetFromMap( 53 | new WeakHashMap()); 54 | 55 | private AccountPreferences(Account account, AccountManager accountManager) { 56 | this.account = account; 57 | this.accountManager = accountManager; 58 | } 59 | 60 | public static AccountPreferences from(AccountManager accountManager, Account account) { 61 | if (account == null) { 62 | throw new IllegalArgumentException("account is null"); 63 | } 64 | synchronized (INSTANCES_LOCK) { 65 | AccountPreferences accountPreferences = INSTANCES.get(account); 66 | if (accountPreferences == null) { 67 | accountPreferences = new AccountPreferences(account, accountManager); 68 | INSTANCES.put(account, accountPreferences); 69 | } 70 | return accountPreferences; 71 | } 72 | } 73 | 74 | public static AccountPreferences from(Account account, Context context) { 75 | return from(AccountManager.get(context), account); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | @Override 82 | public String getString(String key, String defaultValue) { 83 | String value = accountManager.getUserData(account, key); 84 | return value != null ? value : defaultValue; 85 | } 86 | 87 | /** 88 | * {@inheritDoc} 89 | */ 90 | @Override 91 | public Set getStringSet(String key, Set defaultValue) { 92 | String stringValue = getString(key, null); 93 | if (stringValue == null) { 94 | return defaultValue; 95 | } else { 96 | Set value = new HashSet<>(); 97 | Collections.addAll(value, IoUtils.stringToStringArray(stringValue)); 98 | return value; 99 | } 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | @Override 106 | public int getInt(String key, int defaultValue) { 107 | String stringValue = getString(key, null); 108 | if (stringValue == null) { 109 | return defaultValue; 110 | } else { 111 | try { 112 | return Integer.parseInt(stringValue); 113 | } catch (NumberFormatException e) { 114 | BuildUtils.throwOrPrint(e); 115 | return defaultValue; 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * {@inheritDoc} 122 | */ 123 | @Override 124 | public long getLong(String key, long defaultValue) { 125 | String stringValue = getString(key, null); 126 | if (stringValue == null) { 127 | return defaultValue; 128 | } else { 129 | try { 130 | return Long.parseLong(stringValue); 131 | } catch (NumberFormatException e) { 132 | BuildUtils.throwOrPrint(e); 133 | return defaultValue; 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * {@inheritDoc} 140 | */ 141 | @Override 142 | public float getFloat(String key, float defaultValue) { 143 | String stringValue = getString(key, null); 144 | if (stringValue == null) { 145 | return defaultValue; 146 | } else { 147 | try { 148 | return Float.parseFloat(stringValue); 149 | } catch (NumberFormatException e) { 150 | BuildUtils.throwOrPrint(e); 151 | return defaultValue; 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * {@inheritDoc} 158 | */ 159 | @Override 160 | public boolean getBoolean(String key, boolean defaultValue) { 161 | String stringValue = getString(key, null); 162 | if (stringValue == null) { 163 | return defaultValue; 164 | } else { 165 | switch (stringValue) { 166 | case TRUE_STRING: 167 | return true; 168 | case FALSE_STRING: 169 | return false; 170 | default: 171 | return defaultValue; 172 | } 173 | } 174 | } 175 | 176 | 177 | /** 178 | * Unsupported operation. 179 | * 180 | * Due to the limitation of {@link AccountManager}, we can only access a value by its key, but we 181 | * cannot access the key set or the entire map.. Calling this method will throw an 182 | * {@link UnsupportedOperationException}. 183 | * 184 | * @throws UnsupportedOperationException 185 | */ 186 | @Override 187 | public Map getAll() { 188 | throw new UnsupportedOperationException("getAll() is not supported by AccountManager"); 189 | } 190 | 191 | /** 192 | * {@inheritDoc} 193 | */ 194 | @Override 195 | public boolean contains(String key) { 196 | return getString(key, null) != null; 197 | } 198 | 199 | 200 | /** 201 | * Stub method. 202 | * 203 | * Due to the limitation of {@link AccountManager}, we cannot batch commit changes, so this 204 | * instance itself is returned. 205 | * 206 | * @return Returns this instance. 207 | */ 208 | @Override 209 | public Editor edit() { 210 | return this; 211 | } 212 | 213 | /** 214 | * {@inheritDoc} 215 | */ 216 | @Override 217 | public AccountPreferences putString(String key, String value) { 218 | accountManager.setUserData(account, key, value); 219 | notifyChanged(key); 220 | return this; 221 | } 222 | 223 | /** 224 | * {@inheritDoc} 225 | * 226 | *

Due to the limitation of {@link AccountManager}, all values are stored as {@link String}, 227 | * so {@link #putStringSet(String, Set)} needs to flatten the {@link String} array into one 228 | * {@link String}, in this case the character bar ('|') is used as the delimiter.

229 | */ 230 | @Override 231 | public AccountPreferences putStringSet(String key, Set value) { 232 | return putString(key, value != null ? IoUtils.collectionToString(value) : null); 233 | } 234 | 235 | /** 236 | * {@inheritDoc} 237 | */ 238 | @Override 239 | public AccountPreferences putInt(String key, int value) { 240 | return putString(key, Integer.toString(value)); 241 | } 242 | 243 | /** 244 | * {@inheritDoc} 245 | */ 246 | @Override 247 | public AccountPreferences putLong(String key, long value) { 248 | return putString(key, Long.toString(value)); 249 | } 250 | 251 | /** 252 | * {@inheritDoc} 253 | */ 254 | @Override 255 | public AccountPreferences putFloat(String key, float value) { 256 | return putString(key, Float.toString(value)); 257 | } 258 | 259 | /** 260 | * {@inheritDoc} 261 | */ 262 | @Override 263 | public AccountPreferences putBoolean(String key, boolean value) { 264 | return putString(key, value ? TRUE_STRING : FALSE_STRING); 265 | } 266 | 267 | /** 268 | * {@inheritDoc} 269 | */ 270 | @Override 271 | public AccountPreferences remove(String key) { 272 | return putString(key, null); 273 | } 274 | 275 | /** 276 | * Unsupported operation. 277 | * 278 | * Due to the limitation of {@link AccountManager}, we can only access a value by its key, but we 279 | * cannot access the key set or the entire map. Calling this method will throw an 280 | * {@link UnsupportedOperationException}. 281 | * 282 | * @throws UnsupportedOperationException 283 | */ 284 | @Override 285 | public Editor clear() { 286 | throw new UnsupportedOperationException("clear() is not supported by AccountManager"); 287 | } 288 | 289 | /** 290 | * Stub method. 291 | * 292 | * Due to the limitation of {@link AccountManager}, we cannot batch commit changes, so nothing 293 | * is done and it will always success. 294 | * 295 | * @return Always returns true. 296 | */ 297 | @Override 298 | public boolean commit() { 299 | return true; 300 | } 301 | 302 | /** 303 | * Stub method. 304 | * 305 | * Due to the limitation of {@link AccountManager}, we cannot batch commit changes, so nothing 306 | * is done. 307 | */ 308 | @Override 309 | public void apply() {} 310 | 311 | /** 312 | * {@inheritDoc} 313 | */ 314 | @Override 315 | public void registerOnSharedPreferenceChangeListener( 316 | OnSharedPreferenceChangeListener listener) { 317 | listeners.add(listener); 318 | } 319 | 320 | /** 321 | * {@inheritDoc} 322 | */ 323 | @Override 324 | public void unregisterOnSharedPreferenceChangeListener( 325 | OnSharedPreferenceChangeListener listener) { 326 | listeners.remove(listener); 327 | } 328 | 329 | private void notifyChanged(final String key) { 330 | if (Looper.myLooper() == Looper.getMainLooper()) { 331 | for (OnSharedPreferenceChangeListener listener : listeners) { 332 | if (listener != null) { 333 | listener.onSharedPreferenceChanged(AccountPreferences.this, key); 334 | } 335 | } 336 | } else { 337 | mainHandler.post(new Runnable() { 338 | @Override 339 | public void run() { 340 | notifyChanged(key); 341 | } 342 | }); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/PRNGFixes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | // NOTE: The code below is copied from 9 | // http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html 10 | 11 | /* 12 | * This software is provided 'as-is', without any express or implied 13 | * warranty. In no event will Google be held liable for any damages 14 | * arising from the use of this software. 15 | * 16 | * Permission is granted to anyone to use this software for any purpose, 17 | * including commercial applications, and to alter it and redistribute it 18 | * freely, as long as the origin is not misrepresented. 19 | */ 20 | 21 | import android.os.Build; 22 | import android.os.Process; 23 | import android.util.Log; 24 | 25 | import java.io.ByteArrayOutputStream; 26 | import java.io.DataInputStream; 27 | import java.io.DataOutputStream; 28 | import java.io.File; 29 | import java.io.FileInputStream; 30 | import java.io.FileOutputStream; 31 | import java.io.IOException; 32 | import java.io.OutputStream; 33 | import java.io.UnsupportedEncodingException; 34 | import java.security.NoSuchAlgorithmException; 35 | import java.security.Provider; 36 | import java.security.SecureRandom; 37 | import java.security.SecureRandomSpi; 38 | import java.security.Security; 39 | 40 | /** 41 | * Fixes for the output of the default PRNG having low entropy. 42 | * 43 | *

The fixes need to be applied via {@link #apply()} before any use of Java Cryptography 44 | * Architecture primitives. A good place to invoke them is in the 45 | * {@link android.app.Application#onCreate}.

46 | */ 47 | public final class PRNGFixes { 48 | 49 | private static final int VERSION_CODE_JELLY_BEAN = 16; 50 | private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; 51 | private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = 52 | getBuildFingerprintAndDeviceSerial(); 53 | 54 | private PRNGFixes() {} 55 | 56 | /** 57 | * Applies all fixes. 58 | * 59 | * @throws SecurityException if a fix is needed but could not be applied. 60 | */ 61 | public static void apply() { 62 | applyOpenSSLFix(); 63 | installLinuxPRNGSecureRandom(); 64 | } 65 | 66 | /** 67 | * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the fix is not needed. 68 | * 69 | * @throws SecurityException if the fix is needed but could not be applied. 70 | */ 71 | private static void applyOpenSSLFix() throws SecurityException { 72 | 73 | if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) 74 | || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { 75 | // No need to apply the fix 76 | return; 77 | } 78 | 79 | try { 80 | // Mix in the device- and invocation-specific seed. 81 | Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") 82 | .getMethod("RAND_seed", byte[].class) 83 | .invoke(null, generateSeed()); 84 | 85 | // Mix output of Linux PRNG into OpenSSL's PRNG 86 | int bytesRead = (Integer) Class.forName( 87 | "org.apache.harmony.xnet.provider.jsse.NativeCrypto") 88 | .getMethod("RAND_load_file", String.class, long.class) 89 | .invoke(null, "/dev/urandom", 1024); 90 | if (bytesRead != 1024) { 91 | throw new IOException( 92 | "Unexpected number of bytes read from Linux PRNG: " + bytesRead); 93 | } 94 | } catch (Exception e) { 95 | throw new SecurityException("Failed to seed OpenSSL PRNG", e); 96 | } 97 | } 98 | 99 | /** 100 | * Installs a Linux PRNG-backed {@link SecureRandom} implementation as the default. Does nothing 101 | * if the implementation is already the default or if there is not need to install the 102 | * implementation. 103 | * 104 | * @throws SecurityException if the fix is needed but could not be applied. 105 | */ 106 | private static void installLinuxPRNGSecureRandom() 107 | throws SecurityException { 108 | 109 | if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { 110 | // No need to apply the fix 111 | return; 112 | } 113 | 114 | // Install a Linux PRNG-based SecureRandom implementation as the 115 | // default, if not yet installed. 116 | Provider[] secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG"); 117 | if ((secureRandomProviders == null) 118 | || (secureRandomProviders.length < 1) 119 | || (!LinuxPRNGSecureRandomProvider.class.equals( 120 | secureRandomProviders[0].getClass()))) { 121 | Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); 122 | } 123 | 124 | // Assert that new SecureRandom() and 125 | // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed 126 | // by the Linux PRNG-based SecureRandom implementation. 127 | SecureRandom rng1 = new SecureRandom(); 128 | if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider().getClass())) { 129 | throw new SecurityException("new SecureRandom() backed by wrong Provider: " 130 | + rng1.getProvider().getClass()); 131 | } 132 | 133 | SecureRandom rng2; 134 | try { 135 | rng2 = SecureRandom.getInstance("SHA1PRNG"); 136 | } catch (NoSuchAlgorithmException e) { 137 | throw new SecurityException("SHA1PRNG not available", e); 138 | } 139 | if (!LinuxPRNGSecureRandomProvider.class.equals( 140 | rng2.getProvider().getClass())) { 141 | throw new SecurityException( 142 | "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong Provider: " 143 | + rng2.getProvider().getClass()); 144 | } 145 | } 146 | 147 | /** 148 | * {@code Provider} of {@link SecureRandom} engines which pass through all requests to the Linux 149 | * PRNG. 150 | */ 151 | private static class LinuxPRNGSecureRandomProvider extends Provider { 152 | 153 | public LinuxPRNGSecureRandomProvider() { 154 | super("LinuxPRNG", 1.0, 155 | "A Linux-specific random number provider that uses /dev/urandom"); 156 | // Although /dev/urandom is not a SHA-1 PRNG, some apps explicitly request a SHA1PRNG 157 | // SecureRandom and we thus need to prevent them from getting the default implementation 158 | // whose output may have low entropy. 159 | put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); 160 | put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); 161 | } 162 | } 163 | 164 | /** 165 | * {@link SecureRandomSpi} which passes all requests to the Linux PRNG ({@code /dev/urandom}). 166 | */ 167 | public static class LinuxPRNGSecureRandom extends SecureRandomSpi { 168 | 169 | /* 170 | * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed are passed through 171 | * to the Linux PRNG (/dev/urandom). Instances of this class seed themselves by mixing in 172 | * the current time, PID, UID, build fingerprint, and hardware serial number (where 173 | * available) into Linux PRNG. 174 | * 175 | * Concurrency: Read requests to the underlying Linux PRNG are serialized (on sLock) to 176 | * ensure that multiple threads do not get duplicated PRNG output. 177 | */ 178 | 179 | private static final File URANDOM_FILE = new File("/dev/urandom"); 180 | 181 | private static final Object sLock = new Object(); 182 | 183 | /** 184 | * Input stream for reading from Linux PRNG or {@code null} if not yet opened. 185 | * 186 | * @GuardedBy("sLock") 187 | */ 188 | private static DataInputStream sUrandomIn; 189 | 190 | /** 191 | * Output stream for writing to Linux PRNG or {@code null} if not yet opened. 192 | * 193 | * @GuardedBy("sLock") 194 | */ 195 | private static OutputStream sUrandomOut; 196 | 197 | /** 198 | * Whether this engine instance has been seeded. This is needed because each instance needs 199 | * to seed itself if the client does not explicitly seed it. 200 | */ 201 | private boolean mSeeded; 202 | 203 | @Override 204 | protected void engineSetSeed(byte[] bytes) { 205 | try { 206 | OutputStream out; 207 | synchronized (sLock) { 208 | out = getUrandomOutputStream(); 209 | } 210 | out.write(bytes); 211 | out.flush(); 212 | } catch (IOException e) { 213 | // On a small fraction of devices /dev/urandom is not writable. Log and ignore. 214 | Log.w(PRNGFixes.class.getSimpleName(), "Failed to mix seed into " + URANDOM_FILE); 215 | } finally { 216 | mSeeded = true; 217 | } 218 | } 219 | 220 | @Override 221 | protected void engineNextBytes(byte[] bytes) { 222 | 223 | if (!mSeeded) { 224 | // Mix in the device- and invocation-specific seed. 225 | engineSetSeed(generateSeed()); 226 | } 227 | 228 | try { 229 | DataInputStream in; 230 | synchronized (sLock) { 231 | in = getUrandomInputStream(); 232 | } 233 | synchronized (in) { 234 | in.readFully(bytes); 235 | } 236 | } catch (IOException e) { 237 | throw new SecurityException("Failed to read from " + URANDOM_FILE, e); 238 | } 239 | } 240 | 241 | @Override 242 | protected byte[] engineGenerateSeed(int size) { 243 | byte[] seed = new byte[size]; 244 | engineNextBytes(seed); 245 | return seed; 246 | } 247 | 248 | private DataInputStream getUrandomInputStream() { 249 | synchronized (sLock) { 250 | if (sUrandomIn == null) { 251 | // NOTE: Consider inserting a BufferedInputStream between DataInputStream and 252 | // FileInputStream if you need higher PRNG output performance and can live with 253 | // future PRNG output being pulled into this process prematurely. 254 | try { 255 | sUrandomIn = new DataInputStream( 256 | new FileInputStream(URANDOM_FILE)); 257 | } catch (IOException e) { 258 | throw new SecurityException("Failed to open " 259 | + URANDOM_FILE + " for reading", e); 260 | } 261 | } 262 | return sUrandomIn; 263 | } 264 | } 265 | 266 | private OutputStream getUrandomOutputStream() throws IOException { 267 | synchronized (sLock) { 268 | if (sUrandomOut == null) { 269 | sUrandomOut = new FileOutputStream(URANDOM_FILE); 270 | } 271 | return sUrandomOut; 272 | } 273 | } 274 | } 275 | 276 | /** 277 | * Generates a device- and invocation-specific seed to be mixed into the Linux PRNG. 278 | */ 279 | private static byte[] generateSeed() { 280 | try { 281 | ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); 282 | DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer); 283 | seedBufferOut.writeLong(System.currentTimeMillis()); 284 | seedBufferOut.writeLong(System.nanoTime()); 285 | seedBufferOut.writeInt(Process.myPid()); 286 | seedBufferOut.writeInt(Process.myUid()); 287 | seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); 288 | seedBufferOut.close(); 289 | return seedBuffer.toByteArray(); 290 | } catch (IOException e) { 291 | throw new SecurityException("Failed to generate seed", e); 292 | } 293 | } 294 | 295 | /** 296 | * Gets the hardware serial number of this device. 297 | * 298 | * @return serial number or {@code null} if not available. 299 | */ 300 | private static String getDeviceSerialNumber() { 301 | // We're using the Reflection API because Build.SERIAL is only available since API Level 9 302 | // (Gingerbread, Android 2.3). 303 | try { 304 | return (String) Build.class.getField("SERIAL").get(null); 305 | } catch (Exception ignored) { 306 | return null; 307 | } 308 | } 309 | 310 | private static byte[] getBuildFingerprintAndDeviceSerial() { 311 | StringBuilder result = new StringBuilder(); 312 | String fingerprint = Build.FINGERPRINT; 313 | if (fingerprint != null) { 314 | result.append(fingerprint); 315 | } 316 | String serial = getDeviceSerialNumber(); 317 | if (serial != null) { 318 | result.append(serial); 319 | } 320 | try { 321 | return result.toString().getBytes("UTF-8"); 322 | } catch (UnsupportedEncodingException e) { 323 | throw new RuntimeException("UTF-8 encoding not supported"); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /library/src/main/java/me/zhanghai/android/androidutil/util/AppUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Zhang Hai 3 | * All Rights Reserved. 4 | */ 5 | 6 | package me.zhanghai.android.androidutil.util; 7 | 8 | import android.animation.Animator; 9 | import android.animation.AnimatorListenerAdapter; 10 | import android.annotation.SuppressLint; 11 | import android.annotation.TargetApi; 12 | import android.app.ActionBar; 13 | import android.app.Activity; 14 | import android.app.AlarmManager; 15 | import android.app.Fragment; 16 | import android.app.FragmentManager; 17 | import android.app.PendingIntent; 18 | import android.content.ActivityNotFoundException; 19 | import android.content.ComponentName; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.pm.PackageInfo; 23 | import android.content.pm.PackageManager; 24 | import android.content.res.TypedArray; 25 | import android.graphics.drawable.Drawable; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import android.os.Handler; 29 | import android.support.v4.app.NavUtils; 30 | import android.support.v4.app.TaskStackBuilder; 31 | import android.transition.Fade; 32 | import android.transition.TransitionManager; 33 | import android.transition.TransitionSet; 34 | import android.util.SparseBooleanArray; 35 | import android.util.TypedValue; 36 | import android.view.LayoutInflater; 37 | import android.view.View; 38 | import android.view.ViewGroup; 39 | import android.view.ViewTreeObserver; 40 | import android.view.Window; 41 | import android.webkit.CookieManager; 42 | import android.widget.AbsListView; 43 | import android.widget.LinearLayout; 44 | import android.widget.Spinner; 45 | 46 | import com.myqsc.mobile3.main.info.DrawerManager; 47 | import com.myqsc.mobile3.main.ui.MainActivity; 48 | import com.myqsc.mobile3.ui.RestartApplicationActivity; 49 | 50 | import java.util.ArrayList; 51 | import java.util.List; 52 | 53 | public class AppUtils { 54 | 55 | public enum ActivityTransitionType { 56 | ACTIVITY_OPEN, 57 | ACTIVITY_CLOSE, 58 | SLIDE_IN_UP, 59 | SLIDE_OUT_DOWN, 60 | FADE 61 | } 62 | 63 | public static final int ACTIONBAR_DISPLAY_OPTIONS = ActionBar.DISPLAY_SHOW_HOME 64 | | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE; 65 | 66 | public static final int ACTIONBAR_DISPLAY_OPTIONS_NO_UP = ACTIONBAR_DISPLAY_OPTIONS 67 | & ~ActionBar.DISPLAY_HOME_AS_UP; 68 | 69 | private static final int RESTART_APPLICATION_DELAY = 250; 70 | 71 | public static void crossfadeViews(final View fromView, View toView) { 72 | 73 | int duration = fromView.getResources() 74 | .getInteger(android.R.integer.config_shortAnimTime); 75 | 76 | fromView.animate() 77 | .setDuration(duration) 78 | .alpha(0) 79 | .setListener(new AnimatorListenerAdapter() { 80 | private boolean mCanceled = false; 81 | @Override 82 | public void onAnimationCancel(Animator animation) { 83 | mCanceled = true; 84 | } 85 | @Override 86 | public void onAnimationEnd(Animator animator) { 87 | if (!mCanceled) { 88 | fromView.setVisibility(View.INVISIBLE); 89 | } 90 | } 91 | }) 92 | .start(); 93 | 94 | toView.setAlpha(0); 95 | toView.setVisibility(View.VISIBLE); 96 | toView.animate() 97 | .setDuration(NOTE) 98 | // NOTE: We need to remove any previously set listener or Android will reuse it. 99 | .setListener(null) 100 | .alpha(1) 101 | .start(); 102 | } 103 | 104 | public static List getAbsListViewCheckedItemPositions(AbsListView absListView) { 105 | SparseBooleanArray checked = absListView.getCheckedItemPositions(); 106 | List positions = new ArrayList<>(); 107 | int checkedSize = checked.size(); 108 | for (int i = 0; i < checkedSize; ++i) { 109 | if (checked.valueAt(i)) { 110 | positions.add(checked.keyAt(i)); 111 | } 112 | } 113 | return positions; 114 | } 115 | 116 | public static Drawable getActionBarBackground(Context context) { 117 | TypedValue outValue = new TypedValue(); 118 | context.getTheme().resolveAttribute(android.R.attr.actionBarStyle, outValue, true); 119 | TypedArray typedArray = context.obtainStyledAttributes(outValue.resourceId, 120 | new int[] {android.R.attr.background}); 121 | Drawable background = typedArray.getDrawable(0); 122 | typedArray.recycle(); 123 | return background; 124 | } 125 | 126 | public static Drawable getSplitActionBarBackground(Context context) { 127 | TypedValue outValue = new TypedValue(); 128 | context.getTheme().resolveAttribute(android.R.attr.actionBarStyle, outValue, true); 129 | TypedArray typedArray = context.obtainStyledAttributes(outValue.resourceId, 130 | new int[] {android.R.attr.backgroundSplit}); 131 | Drawable background = typedArray.getDrawable(0); 132 | typedArray.recycle(); 133 | return background; 134 | } 135 | 136 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 137 | public static FragmentManager getChildFragmentManagerIfAvailable(Fragment fragment) { 138 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 139 | return fragment.getChildFragmentManager(); 140 | } else { 141 | return fragment.getFragmentManager(); 142 | } 143 | } 144 | 145 | public static PackageInfo getPackageInfo(Context context) { 146 | try { 147 | return context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 148 | } catch (PackageManager.NameNotFoundException e) { 149 | // Should not happen. 150 | throw new RuntimeException(e); 151 | } 152 | } 153 | 154 | public static void installShortcut(int iconRes, int nameRes, Class intentClass, 155 | Context context) { 156 | Intent intent = IntentUtils.makeInstallShortcutWithAction(iconRes, nameRes, intentClass, 157 | context); 158 | context.sendBroadcast(intent); 159 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 160 | // From 4.1.1_r1, launcher will not make a toast for successful shortcut installation. 161 | String message = context.getString(R.string.shortcut_installed_format, 162 | context.getString(nameRes)); 163 | ToastUtils.show(message, context); 164 | } 165 | } 166 | 167 | // Set the intent for shortcut installation as result and then finish the Activity. See the 168 | // users of this method for its usage. 169 | public static void installShortcutAsActivity(Activity activity, int iconRes, int nameRes, 170 | Class intentClass) { 171 | activity.setResult(Activity.RESULT_OK, IntentUtils.makeInstallShortcut(iconRes, nameRes, 172 | intentClass, activity)); 173 | activity.finish(); 174 | } 175 | 176 | public static boolean isFragmentAttached(Fragment fragment) { 177 | return fragment.getActivity() != null; 178 | } 179 | 180 | public static View makeDoneCancelLayout(View.OnClickListener onDoneListener, 181 | View.OnClickListener onCancelListener, 182 | LayoutInflater layoutInflater) { 183 | @SuppressLint("InflateParams") 184 | LinearLayout doneDiscardLayout = (LinearLayout)layoutInflater.inflate(R.layout.done_cancel, 185 | null); 186 | doneDiscardLayout.setLayoutParams(new ActionBar.LayoutParams( 187 | ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.MATCH_PARENT)); 188 | doneDiscardLayout.findViewById(R.id.done_cancel_done).setOnClickListener(onDoneListener); 189 | doneDiscardLayout.findViewById(R.id.done_cancel_cancel) 190 | .setOnClickListener(onCancelListener); 191 | return doneDiscardLayout; 192 | } 193 | 194 | public static View makeDoneCancelLayout(View.OnClickListener onDoneListener, 195 | View.OnClickListener onCancelListener, 196 | Activity activity) { 197 | return makeDoneCancelLayout(onDoneListener, onCancelListener, activity.getLayoutInflater()); 198 | } 199 | 200 | // From http://developer.android.com/training/implementing-navigation/ancestral.html#NavigateUp . 201 | public static void navigateUp(Activity activity, Bundle extras) { 202 | Intent upIntent = NavUtils.getParentActivityIntent(activity); 203 | if (upIntent == null) { 204 | LogUtils.w("No parent found for activity, will just call finish(): " 205 | + activity.getClass().getName()); 206 | } else { 207 | if (extras != null) { 208 | upIntent.putExtras(extras); 209 | } 210 | if (NavUtils.shouldUpRecreateTask(activity, upIntent)) { 211 | LogUtils.i("Creating new task"); 212 | // This activity is NOT part of this app's task, so create a new task 213 | // when navigating up, with a synthesized back stack. 214 | TaskStackBuilder.create(activity) 215 | // Add all of this activity's parents to the back stack. 216 | .addNextIntentWithParentStack(upIntent) 217 | // Navigate up to the closest parent. 218 | .startActivities(); 219 | } else { 220 | // This activity is part of this app's task, so simply 221 | // navigate up to the logical parent activity. 222 | LogUtils.i("Using original task"); 223 | // According to http://stackoverflow.com/a/14792752/2420519 224 | //NavUtils.navigateUpTo(activity, upIntent); 225 | upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 226 | activity.startActivity(upIntent); 227 | } 228 | } 229 | activity.finish(); 230 | } 231 | 232 | public static void navigateUp(Activity activity) { 233 | navigateUp(activity, null); 234 | } 235 | 236 | public static void navigateUpToMainActivityWithDrawerItemId(Activity activity, 237 | int drawerItemId) { 238 | Bundle mainExtras = new Bundle(); 239 | int drawerItemPosition = DrawerManager.getInstance().getPosition(drawerItemId); 240 | mainExtras.putInt(MainActivity.EXTRA_DRAWER_POSITION, drawerItemPosition); 241 | AppUtils.navigateUp(activity, mainExtras); 242 | } 243 | 244 | // Should be called after startActivity() and in onPause(). 245 | public static void overrideActivityTransition(Activity activity, 246 | ActivityTransitionType type) { 247 | int enterAnim, exitAnim; 248 | switch (type) { 249 | case ACTIVITY_OPEN: 250 | enterAnim = R.anim.activity_open_enter_holo; 251 | exitAnim = R.anim.activity_open_exit_holo; 252 | break; 253 | case ACTIVITY_CLOSE: 254 | enterAnim = R.anim.activity_close_enter_holo; 255 | exitAnim = R.anim.activity_close_exit_holo; 256 | break; 257 | case SLIDE_IN_UP: 258 | enterAnim = R.anim.slide_in_up; 259 | exitAnim = R.anim.remain; 260 | break; 261 | case SLIDE_OUT_DOWN: 262 | enterAnim = R.anim.remain; 263 | exitAnim = R.anim.slide_out_down; 264 | break; 265 | case FADE: 266 | enterAnim = R.anim.activity_fade_in; 267 | exitAnim = R.anim.activity_fade_out; 268 | break; 269 | default: 270 | throw new IllegalArgumentException("Unknown activity transition type: " + type); 271 | } 272 | activity.overridePendingTransition(enterAnim, exitAnim); 273 | } 274 | 275 | public static void overrideActivityTransitionForCompat(Activity activity, boolean isOpen) { 276 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 277 | int enterAnim, exitAnim; 278 | if (isOpen) { 279 | enterAnim = R.anim.activity_open_enter_compat; 280 | exitAnim = R.anim.activity_open_exit_compat; 281 | } else { 282 | enterAnim = R.anim.activity_close_enter_compat; 283 | exitAnim = R.anim.activity_close_exit_compat; 284 | } 285 | activity.overridePendingTransition(enterAnim, exitAnim); 286 | } 287 | } 288 | 289 | public static void post(Runnable runnable) { 290 | new Handler().post(runnable); 291 | } 292 | 293 | public static void postDelayed(Runnable runnable, long delayMillis) { 294 | new Handler().postDelayed(runnable, delayMillis); 295 | } 296 | 297 | public static void postOnPreDraw(final View view, final Runnable runnable) { 298 | view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 299 | @Override 300 | public boolean onPreDraw() { 301 | view.getViewTreeObserver().removeOnPreDrawListener(this); 302 | runnable.run(); 303 | return true; 304 | } 305 | }); 306 | } 307 | 308 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 309 | public static void removeAllCookies() { 310 | CookieManager cookieManager = CookieManager.getInstance(); 311 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 312 | cookieManager.removeAllCookies(null); 313 | } else { 314 | //noinspection deprecation 315 | cookieManager.removeAllCookie(); 316 | } 317 | } 318 | 319 | public static void restartActivity(Activity activity) { 320 | activity.recreate(); 321 | } 322 | 323 | @Deprecated 324 | public static void restartApplication(Context context) { 325 | // NOTE: Flag NEW_TASK is required for CLEAR_TASK, and the latter one can clear our tasks so 326 | // that Samsumg ROMs will not automatically recreate our root activity when we exit(0). 327 | Intent intent = new Intent(context, RestartApplicationActivity.class) 328 | .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 329 | context.startActivity(intent); 330 | } 331 | 332 | public static void restartApplicationDelayed(final Context context) { 333 | postDelayed(new Runnable() { 334 | @Override 335 | public void run() { 336 | //noinspection deprecation 337 | restartApplication(context); 338 | } 339 | }, RESTART_APPLICATION_DELAY); 340 | } 341 | 342 | public static void setAbsListViewAllItemsChecked(AbsListView absListView) { 343 | int count = absListView.getCount(); 344 | for (int position = 0; position < count; ++position) { 345 | absListView.setItemChecked(position, true); 346 | } 347 | } 348 | 349 | @TargetApi(Build.VERSION_CODES.KITKAT) 350 | public static void setAlarmExact(Context context, int type, long triggerAtMillis, 351 | PendingIntent pendingIntent) { 352 | AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 353 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 354 | alarmManager.setExact(type, triggerAtMillis, pendingIntent); 355 | } else { 356 | alarmManager.set(type, triggerAtMillis, pendingIntent); 357 | } 358 | } 359 | 360 | // NOTE: Can be used to enable / disable a BroadcastReceiver. 361 | public static void setComponentEnabled(Class componentClass, boolean enabled, 362 | Context context) { 363 | ComponentName componentName = new ComponentName(context, componentClass); 364 | PackageManager packageManager = context.getPackageManager(); 365 | int state = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED 366 | : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; 367 | packageManager.setComponentEnabledSetting(componentName, state, 368 | PackageManager.DONT_KILL_APP); 369 | } 370 | 371 | // Workaround stupid Spinner, always fire onItemSelected(). 372 | public static void setSpinnerSelection(Spinner spinner, int position) { 373 | if (spinner.getSelectedItemPosition() != position) { 374 | spinner.setSelection(position); 375 | } else { 376 | spinner.getOnItemSelectedListener().onItemSelected(spinner, spinner.getSelectedView(), 377 | position, spinner.getAdapter().getItemId(position)); 378 | } 379 | } 380 | 381 | public static void setStatusBarColor(Window window, int color) { 382 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 383 | window.setStatusBarColor(color); 384 | } 385 | } 386 | 387 | public static void setStatusBarColorRes(Window window, int colorId, Context context) { 388 | setStatusBarColor(window, context.getResources().getColor(colorId)); 389 | } 390 | 391 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 392 | public static void setViewBackground(View view, Drawable background) { 393 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 394 | view.setBackground(background); 395 | } else { 396 | //noinspection deprecation 397 | view.setBackgroundDrawable(background); 398 | } 399 | } 400 | 401 | public static void setViewLayoutParamsHeight(View view, int height) { 402 | ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); 403 | layoutParams.height = height; 404 | view.setLayoutParams(layoutParams); 405 | } 406 | 407 | public static void setupActionBar(ActionBar actionBar) { 408 | actionBar.setDisplayOptions(ACTIONBAR_DISPLAY_OPTIONS); 409 | actionBar.setHomeButtonEnabled(true); 410 | // NOTE: 411 | // There is a bug in the handling of ActionView (SearchView) icon inside ActionBarImpl, so 412 | // we have to workaround it by setting the icon to logo programmatically. 413 | actionBar.setIcon(R.drawable.logo); 414 | } 415 | 416 | public static void setupActionBar(Activity activity) { 417 | setupActionBar(activity.getActionBar()); 418 | } 419 | 420 | public static void setupActionBarIfHas(Activity activity) { 421 | ActionBar actionBar = activity.getActionBar(); 422 | if (actionBar != null) { 423 | setupActionBar(actionBar); 424 | } 425 | } 426 | 427 | public static void setupActionBarNoUp(ActionBar actionBar) { 428 | actionBar.setDisplayOptions(ACTIONBAR_DISPLAY_OPTIONS_NO_UP); 429 | actionBar.setHomeButtonEnabled(false); 430 | actionBar.setIcon(R.drawable.logo); 431 | } 432 | 433 | public static void setupActionBarNoUp(Activity activity) { 434 | setupActionBarNoUp(activity.getActionBar()); 435 | } 436 | 437 | public static void setupActionBarDoneCancel(Activity activity, 438 | View.OnClickListener onDoneListener, 439 | View.OnClickListener onCancelListener) { 440 | ActionBar actionBar = activity.getActionBar(); 441 | actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); 442 | actionBar.setCustomView(makeDoneCancelLayout(onDoneListener, onCancelListener, activity)); 443 | } 444 | 445 | public static void startActivity(Intent intent, Context context) { 446 | try { 447 | context.startActivity(intent); 448 | } catch (ActivityNotFoundException e) { 449 | e.printStackTrace(); 450 | ToastUtils.show(R.string.activity_not_found, context); 451 | } 452 | } 453 | 454 | public static void startActivityForResult(Activity activity, Intent intent, int requestCode) { 455 | try { 456 | activity.startActivityForResult(intent, requestCode); 457 | } catch (ActivityNotFoundException e) { 458 | e.printStackTrace(); 459 | ToastUtils.show(R.string.activity_not_found, activity); 460 | } 461 | } 462 | 463 | // NOTE: ListView should make hasStableIds() return true for transition to apply. 464 | @TargetApi(Build.VERSION_CODES.KITKAT) 465 | public static void startDelayedTransitionIfAvailable(ViewGroup viewGroup, Context context) { 466 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 467 | TransitionManager.beginDelayedTransition(viewGroup, new CrossFade(context)); 468 | } 469 | } 470 | 471 | public static void swapViewGroupChildren(ViewGroup viewGroup, View firstView, View secondView) { 472 | int firstIndex = viewGroup.indexOfChild(firstView); 473 | int secondIndex = viewGroup.indexOfChild(secondView); 474 | if (firstIndex < secondIndex) { 475 | viewGroup.removeViewAt(secondIndex); 476 | viewGroup.removeViewAt(firstIndex); 477 | viewGroup.addView(secondView, firstIndex); 478 | viewGroup.addView(firstView, secondIndex); 479 | } else { 480 | viewGroup.removeViewAt(firstIndex); 481 | viewGroup.removeViewAt(secondIndex); 482 | viewGroup.addView(firstView, secondIndex); 483 | viewGroup.addView(secondView, firstIndex); 484 | } 485 | } 486 | 487 | 488 | private AppUtils() {} 489 | 490 | 491 | @TargetApi(Build.VERSION_CODES.KITKAT) 492 | private static class CrossFade extends TransitionSet { 493 | 494 | public CrossFade(Context context) { 495 | setOrdering(ORDERING_TOGETHER); 496 | int duration = context.getResources().getInteger( 497 | android.R.integer.config_shortAnimTime); 498 | setDuration(duration); 499 | addTransition(new Fade(Fade.OUT)); 500 | addTransition(new Fade(Fade.IN)); 501 | } 502 | } 503 | } 504 | --------------------------------------------------------------------------------