();
+
+ public static interface LifeCycleListener {
+ public void onActivityCreated(MonitoredActivity activity);
+ public void onActivityDestroyed(MonitoredActivity activity);
+ public void onActivityStarted(MonitoredActivity activity);
+ public void onActivityStopped(MonitoredActivity activity);
+ }
+
+ public static class LifeCycleAdapter implements LifeCycleListener {
+ public void onActivityCreated(MonitoredActivity activity) {}
+ public void onActivityDestroyed(MonitoredActivity activity) {}
+ public void onActivityStarted(MonitoredActivity activity) {}
+ public void onActivityStopped(MonitoredActivity activity) {}
+ }
+
+ public void addLifeCycleListener(LifeCycleListener listener) {
+ if (listeners.contains(listener)) return;
+ listeners.add(listener);
+ }
+
+ public void removeLifeCycleListener(LifeCycleListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ for (LifeCycleListener listener : listeners) {
+ listener.onActivityCreated(this);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ for (LifeCycleListener listener : listeners) {
+ listener.onActivityDestroyed(this);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ for (LifeCycleListener listener : listeners) {
+ listener.onActivityStarted(this);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ for (LifeCycleListener listener : listeners) {
+ listener.onActivityStopped(this);
+ }
+ }
+
+}
diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java
new file mode 100644
index 0000000..1b1f9a2
--- /dev/null
+++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.soundcloud.crop;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+
+/*
+ * Modified from original in AOSP.
+ */
+class RotateBitmap {
+
+ private Bitmap bitmap;
+ private int rotation;
+
+ public RotateBitmap(Bitmap bitmap, int rotation) {
+ this.bitmap = bitmap;
+ this.rotation = rotation % 360;
+ }
+
+ public void setRotation(int rotation) {
+ this.rotation = rotation;
+ }
+
+ public int getRotation() {
+ return rotation;
+ }
+
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ public void setBitmap(Bitmap bitmap) {
+ this.bitmap = bitmap;
+ }
+
+ public Matrix getRotateMatrix() {
+ // By default this is an identity matrix
+ Matrix matrix = new Matrix();
+ if (bitmap != null && rotation != 0) {
+ // We want to do the rotation at origin, but since the bounding
+ // rectangle will be changed after rotation, so the delta values
+ // are based on old & new width/height respectively.
+ int cx = bitmap.getWidth() / 2;
+ int cy = bitmap.getHeight() / 2;
+ matrix.preTranslate(-cx, -cy);
+ matrix.postRotate(rotation);
+ matrix.postTranslate(getWidth() / 2, getHeight() / 2);
+ }
+ return matrix;
+ }
+
+ public boolean isOrientationChanged() {
+ return (rotation / 90) % 2 != 0;
+ }
+
+ public int getHeight() {
+ if (bitmap == null) return 0;
+ if (isOrientationChanged()) {
+ return bitmap.getWidth();
+ } else {
+ return bitmap.getHeight();
+ }
+ }
+
+ public int getWidth() {
+ if (bitmap == null) return 0;
+ if (isOrientationChanged()) {
+ return bitmap.getHeight();
+ } else {
+ return bitmap.getWidth();
+ }
+ }
+
+ public void recycle() {
+ if (bitmap != null) {
+ bitmap.recycle();
+ bitmap = null;
+ }
+ }
+}
+
diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java
new file mode 100644
index 0000000..271202f
--- /dev/null
+++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java
@@ -0,0 +1,48 @@
+package com.soundcloud.crop;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+
+public class UriUtil {
+
+ public final static String getFileProviderName(Context context){
+ return context.getPackageName()+".fileprovider";
+ }
+
+ /**
+ * 将TakePhoto 提供的Uri 解析出文件绝对路径
+ *
+ * @param uri
+ * @return
+ */
+ public static String parseOwnUri(Context context, Uri uri) {
+ if (uri == null) return null;
+ String path;
+ if (TextUtils.equals(uri.getAuthority(), getFileProviderName(context))) {
+ String target_text_camera_photos = "camera_photos/";
+ if (uri.getPath() != null && uri.getPath().contains(target_text_camera_photos)) {
+ path = new File(uri.getPath().replace(target_text_camera_photos, ""))
+ .getAbsolutePath();
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ path = new File(Environment.getExternalStorageDirectory(),
+ uri.getPath())
+ .getAbsolutePath();
+ } else {
+ path = uri.getPath();
+ }
+ }
+
+ } else {
+ path = uri.getPath();
+ }
+ return path;
+ }
+
+}
diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png
new file mode 100644
index 0000000..373e5d8
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png differ
diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp
new file mode 100644
index 0000000..b929a2b
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp
new file mode 100644
index 0000000..5719d1f
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png
new file mode 100644
index 0000000..373e5d8
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png differ
diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp
new file mode 100644
index 0000000..54dd1eb
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp
new file mode 100644
index 0000000..6be409d
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml b/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml
new file mode 100644
index 0000000..2b25609
--- /dev/null
+++ b/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png
new file mode 100644
index 0000000..b3da354
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png differ
diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp
new file mode 100644
index 0000000..a76ecfe
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp
new file mode 100644
index 0000000..b6230d2
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp
new file mode 100644
index 0000000..8060775
Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp differ
diff --git a/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml b/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml
new file mode 100644
index 0000000..83c4a7d
--- /dev/null
+++ b/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/drawable/crop__texture.xml b/libs/lib_crop/src/main/res/drawable/crop__texture.xml
new file mode 100644
index 0000000..23ec469
--- /dev/null
+++ b/libs/lib_crop/src/main/res/drawable/crop__texture.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml b/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml
new file mode 100644
index 0000000..c133df0
--- /dev/null
+++ b/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml b/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml
new file mode 100644
index 0000000..fe07063
--- /dev/null
+++ b/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/lib_crop/src/main/res/values-ar/strings.xml b/libs/lib_crop/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..b10c50a
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values-ar/strings.xml
@@ -0,0 +1,8 @@
+
+ جارٍ حفظ الصورة...
+ يرجى الانتظار...
+ لا يوجد مصدر صور متاح
+
+ تم
+ إلغاء
+
diff --git a/libs/lib_crop/src/main/res/values-v21/colors.xml b/libs/lib_crop/src/main/res/values-v21/colors.xml
new file mode 100644
index 0000000..55a5350
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values-v21/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #aaaaaa
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml b/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..0eb32d2
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,8 @@
+
+ 正在儲存相片…
+ 請稍候…
+ 沒有可用的圖片來源
+
+ 完成
+ 取消
+
diff --git a/libs/lib_crop/src/main/res/values/attrs.xml b/libs/lib_crop/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..86fed35
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values/attrs.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/values/colors.xml b/libs/lib_crop/src/main/res/values/colors.xml
new file mode 100644
index 0000000..455f448
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+ #f3f3f3
+ #666666
+ #1a000000
+ #77000000
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/values/dimens.xml b/libs/lib_crop/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..9051a35
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 56dp
+
+
\ No newline at end of file
diff --git a/libs/lib_crop/src/main/res/values/strings.xml b/libs/lib_crop/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7543813
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Saving photo…
+ Please wait…
+ No image source available
+
+ Done
+ Cancel
+
+
diff --git a/libs/lib_crop/src/main/res/values/styles.xml b/libs/lib_crop/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2ec6171
--- /dev/null
+++ b/libs/lib_crop/src/main/res/values/styles.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/lib_encipher/.gitignore b/libs/lib_encipher/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/libs/lib_encipher/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/libs/lib_encipher/build.gradle b/libs/lib_encipher/build.gradle
new file mode 100644
index 0000000..59196ae
--- /dev/null
+++ b/libs/lib_encipher/build.gradle
@@ -0,0 +1,15 @@
+apply from : "../lib_standard.gradle"
+
+android {
+ namespace 'com.example.lib_encipher'
+
+ sourceSets{
+ main{
+ jniLibs.srcDirs = ['libs']
+ }
+ }
+}
+
+dependencies {
+ api "androidx.core:core-ktx:1.9.0"
+}
\ No newline at end of file
diff --git a/libs/lib_encipher/consumer-rules.pro b/libs/lib_encipher/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/libs/lib_encipher/libs/arm64-v8a/libencipher.so b/libs/lib_encipher/libs/arm64-v8a/libencipher.so
new file mode 100755
index 0000000..017da66
Binary files /dev/null and b/libs/lib_encipher/libs/arm64-v8a/libencipher.so differ
diff --git a/libs/lib_encipher/libs/armeabi-v7a/libencipher.so b/libs/lib_encipher/libs/armeabi-v7a/libencipher.so
new file mode 100755
index 0000000..b99d083
Binary files /dev/null and b/libs/lib_encipher/libs/armeabi-v7a/libencipher.so differ
diff --git a/libs/lib_encipher/libs/x86/libencipher.so b/libs/lib_encipher/libs/x86/libencipher.so
new file mode 100755
index 0000000..7806d5d
Binary files /dev/null and b/libs/lib_encipher/libs/x86/libencipher.so differ
diff --git a/libs/lib_encipher/libs/x86_64/libencipher.so b/libs/lib_encipher/libs/x86_64/libencipher.so
new file mode 100755
index 0000000..44b3e10
Binary files /dev/null and b/libs/lib_encipher/libs/x86_64/libencipher.so differ
diff --git a/libs/lib_encipher/proguard-rules.pro b/libs/lib_encipher/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/libs/lib_encipher/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/libs/lib_encipher/src/main/AndroidManifest.xml b/libs/lib_encipher/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/libs/lib_encipher/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt b/libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt
new file mode 100644
index 0000000..e7a715f
--- /dev/null
+++ b/libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt
@@ -0,0 +1,58 @@
+package com.secure.encipher
+
+import java.util.regex.Pattern
+
+object EncipherLib {
+
+ init {
+ System.loadLibrary("encipher")
+ }
+
+ fun encryptText(enc: String, key: String): String {
+ return replaceBlank(encrypt(replaceBlank(enc), key) ?: "")
+ }
+
+ fun decryptText(dec: String, key: String): String {
+ return decrypt(dec, key) ?: ""
+ }
+
+ fun encryptTextDef1(enc: String): String {
+ return replaceBlank(encryptDef(replaceBlank(enc)) ?: "")
+ }
+
+ fun decryptTextDef1(dec: String): String {
+ return decryptDef(dec) ?: ""
+ }
+
+ fun encryptTextDef2(enc: String): String {
+ return replaceBlank(encryptDef2(replaceBlank(enc)) ?: "")
+ }
+
+ fun decryptTextDef2(dec: String): String {
+ return decryptDef2(dec) ?: ""
+ }
+
+ private fun replaceBlank(str: String): String {
+ val p = Pattern.compile("\\s*|\t|\r|\n")
+ val m = p.matcher(str)
+ return m.replaceAll("")
+ }
+
+ // DES
+ private external fun encrypt(enc: String, key: String): String?
+
+ // DES
+ private external fun decrypt(dec: String, key: String): String?
+
+ // DES-默认KEY1
+ private external fun encryptDef(enc: String): String?
+
+ // DES-默认KEY1(1ea53d26)
+ private external fun decryptDef(dec: String): String?
+
+ // DES-默认KEY2
+ private external fun encryptDef2(enc: String): String?
+
+ // DES-默认KEY2(9fa73e66)
+ private external fun decryptDef2(dec: String): String?
+}
\ No newline at end of file
diff --git a/libs/lib_standard.gradle b/libs/lib_standard.gradle
new file mode 100644
index 0000000..ada148d
--- /dev/null
+++ b/libs/lib_standard.gradle
@@ -0,0 +1,40 @@
+apply plugin: "com.android.library"
+apply plugin: "org.jetbrains.kotlin.android"
+apply plugin: "kotlin-android"
+apply plugin: "kotlin-kapt"
+
+android {
+ compileSdkVersion COMPILE_SDK_VERSION.toInteger()
+ defaultConfig {
+ minSdkVersion MIN_SDK_VERSION.toInteger()
+ targetSdkVersion TARGET_SDK_VERSION.toInteger()
+ versionCode 1
+ versionName "1.0.0"
+ consumerProguardFiles 'proguard-rules.pro'
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ zipAlignEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ debuggable true
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = '11'
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+}
+
diff --git a/libs/lib_utils/.gitignore b/libs/lib_utils/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/libs/lib_utils/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/libs/lib_utils/build.gradle b/libs/lib_utils/build.gradle
new file mode 100644
index 0000000..c62ad86
--- /dev/null
+++ b/libs/lib_utils/build.gradle
@@ -0,0 +1,10 @@
+apply from : "../lib_standard.gradle"
+
+android {
+ namespace 'com.example.lib_utils'
+}
+
+dependencies {
+ api "androidx.core:core-ktx:1.9.0"
+ api 'androidx.appcompat:appcompat:1.6.1'
+}
\ No newline at end of file
diff --git a/libs/lib_utils/consumer-rules.pro b/libs/lib_utils/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/libs/lib_utils/proguard-rules.pro b/libs/lib_utils/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/libs/lib_utils/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/AndroidManifest.xml b/libs/lib_utils/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/libs/lib_utils/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java b/libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java
new file mode 100644
index 0000000..c836fbf
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java
@@ -0,0 +1,403 @@
+package com.example.lib_utils;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.Window;
+import android.view.inputmethod.InputMethodManager;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ *
+ * author:
+ * ___ ___ ___ ___
+ * _____ / /\ /__/\ /__/| / /\
+ * / /::\ / /::\ \ \:\ | |:| / /:/
+ * / /:/\:\ ___ ___ / /:/\:\ \ \:\ | |:| /__/::\
+ * / /:/~/::\ /__/\ / /\ / /:/~/::\ _____\__\:\ __| |:| \__\/\:\
+ * /__/:/ /:/\:| \ \:\ / /:/ /__/:/ /:/\:\ /__/::::::::\ /__/\_|:|____ \ \:\
+ * \ \:\/:/~/:/ \ \:\ /:/ \ \:\/:/__\/ \ \:\~~\~~\/ \ \:\/:::::/ \__\:\
+ * \ \::/ /:/ \ \:\/:/ \ \::/ \ \:\ ~~~ \ \::/~~~~ / /:/
+ * \ \:\/:/ \ \::/ \ \:\ \ \:\ \ \:\ /__/:/
+ * \ \::/ \__\/ \ \:\ \ \:\ \ \:\ \__\/
+ * \__\/ \__\/ \__\/ \__\/
+ * blog : http://blankj.com
+ * time : 16/12/08
+ * desc : utils about initialization
+ *
+ */
+public final class AppUtils {
+
+ private static final ExecutorService UTIL_POOL = Executors.newFixedThreadPool(3);
+ private static final Handler UTIL_HANDLER = new Handler(Looper.getMainLooper());
+
+ @SuppressLint("StaticFieldLeak")
+ private static Application sApplication;
+
+
+ private AppUtils() {
+ throw new UnsupportedOperationException("u can't instantiate me...");
+ }
+
+ /**
+ * Init utils.
+ * Init it in the class of Application.
+ *
+ * @param context context
+ */
+ public static void init(final Context context) {
+ if (context == null) {
+ init(getApplicationByReflect());
+ return;
+ }
+ init((Application) context.getApplicationContext());
+ }
+
+ /**
+ * Init utils.
+ * Init it in the class of Application.
+ *
+ * @param app application
+ */
+ public static void init(final Application app) {
+ if (sApplication == null) {
+ if (app == null) {
+ sApplication = getApplicationByReflect();
+ } else {
+ sApplication = app;
+ }
+ } else {
+ sApplication = app;
+ }
+ }
+
+ /**
+ * Return the context of Application object.
+ *
+ * @return the context of Application object
+ */
+ public static Application getApp() {
+ if (sApplication != null) return sApplication;
+ Application app = getApplicationByReflect();
+ init(app);
+ return app;
+ }
+
+
+ public static String getPackageName(Context context) {
+ return context.getPackageName();
+ }
+
+ /**
+ * 获取版本名
+ *
+ * @param noSuffix 是否去掉后缀 (如:-debug、-test)
+ */
+ public static String getVersionName(boolean noSuffix) {
+ PackageInfo packageInfo = getPackageInfo(getApp());
+ if (packageInfo != null) {
+ String versionName = packageInfo.versionName;
+ if (noSuffix && versionName != null) {
+ int index = versionName.indexOf("-");
+ if (index >= 0) {
+ return versionName.substring(0, index);
+ }
+ }
+ return versionName;
+ }
+ return "";
+ }
+
+ //版本号
+ public static int getVersionCode() {
+ PackageInfo packageInfo = getPackageInfo(getApp());
+ if (packageInfo != null) {
+ return packageInfo.versionCode;
+ }
+ return 0;
+ }
+
+ /**
+ * 比较版本
+ * 1 = 大于当前版本
+ * 0 = 版本一样
+ * -1 = 当前版本大于更新版本
+ */
+ public static int compareVersionNames(String newVersionName) {
+ try {
+ if (TextUtils.isEmpty(newVersionName)) {
+ return -1;
+ }
+ int res = 0;
+ String currentVersionName = getVersionName(true);
+ if (currentVersionName.equals(newVersionName)) {
+ return 0;
+ }
+
+ String[] oldNumbers = currentVersionName.split("\\.");
+ String[] newNumbers = newVersionName.split("\\.");
+
+ // To avoid IndexOutOfBounds
+ int minIndex = Math.min(oldNumbers.length, newNumbers.length);
+
+ for (int i = 0; i < minIndex; i++) {
+ int oldVersionPart = Integer.parseInt(oldNumbers[i]);
+ int newVersionPart = Integer.parseInt(newNumbers[i]);
+
+ if (oldVersionPart < newVersionPart) {
+ res = 1;
+ break;
+ } else if (oldVersionPart > newVersionPart) {
+ res = -1;
+ break;
+ }
+ }
+
+ // If versions are the same so far, but they have different length...
+ if (res == 0 && oldNumbers.length != newNumbers.length) {
+ res = (oldNumbers.length > newNumbers.length) ? -1 : 1;
+ }
+
+ return res;
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ private static PackageInfo getPackageInfo(Context context) {
+ PackageInfo packageInfo;
+ try {
+ PackageManager pm = context.getPackageManager();
+ packageInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_CONFIGURATIONS);
+ return packageInfo;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ static Task doAsync(final Task task) {
+ UTIL_POOL.execute(task);
+ return task;
+ }
+
+ public static void runOnUiThread(final Runnable runnable) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run();
+ } else {
+ AppUtils.UTIL_HANDLER.post(runnable);
+ }
+ }
+
+ public static void runOnUiThreadDelayed(final Runnable runnable, long delayMillis) {
+ AppUtils.UTIL_HANDLER.postDelayed(runnable, delayMillis);
+ }
+
+ static String getCurrentProcessName() {
+ String name = getCurrentProcessNameByFile();
+ if (!TextUtils.isEmpty(name)) return name;
+ name = getCurrentProcessNameByAms();
+ if (!TextUtils.isEmpty(name)) return name;
+ name = getCurrentProcessNameByReflect();
+ return name;
+ }
+
+ static void fixSoftInputLeaks(final Window window) {
+ InputMethodManager imm =
+ (InputMethodManager) AppUtils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm == null) return;
+ String[] leakViews = new String[]{"mLastSrvView", "mCurRootView", "mServedView", "mNextServedView"};
+ for (String leakView : leakViews) {
+ try {
+ Field leakViewField = InputMethodManager.class.getDeclaredField(leakView);
+ if (leakViewField == null) continue;
+ if (!leakViewField.isAccessible()) {
+ leakViewField.setAccessible(true);
+ }
+ Object obj = leakViewField.get(imm);
+ if (!(obj instanceof View)) continue;
+ View view = (View) obj;
+ if (view.getRootView() == window.getDecorView().getRootView()) {
+ leakViewField.set(imm, null);
+ }
+ } catch (Throwable ignore) {/**/}
+ }
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // private method
+ ///////////////////////////////////////////////////////////////////////////
+
+ private static String getCurrentProcessNameByFile() {
+ try {
+ File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
+ BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
+ String processName = mBufferedReader.readLine().trim();
+ mBufferedReader.close();
+ return processName;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ private static String getCurrentProcessNameByAms() {
+ ActivityManager am = (ActivityManager) AppUtils.getApp().getSystemService(Context.ACTIVITY_SERVICE);
+ if (am == null) return "";
+ List info = am.getRunningAppProcesses();
+ if (info == null || info.size() == 0) return "";
+ int pid = android.os.Process.myPid();
+ for (ActivityManager.RunningAppProcessInfo aInfo : info) {
+ if (aInfo.pid == pid) {
+ if (aInfo.processName != null) {
+ return aInfo.processName;
+ }
+ }
+ }
+ return "";
+ }
+
+ private static String getCurrentProcessNameByReflect() {
+ String processName = "";
+ try {
+ Application app = AppUtils.getApp();
+ Field loadedApkField = app.getClass().getField("mLoadedApk");
+ loadedApkField.setAccessible(true);
+ Object loadedApk = loadedApkField.get(app);
+
+ Field activityThreadField = loadedApk.getClass().getDeclaredField("mActivityThread");
+ activityThreadField.setAccessible(true);
+ Object activityThread = activityThreadField.get(loadedApk);
+
+ Method getProcessName = activityThread.getClass().getDeclaredMethod("getProcessName");
+ processName = (String) getProcessName.invoke(activityThread);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return processName;
+ }
+
+ private static Application getApplicationByReflect() {
+ try {
+ @SuppressLint("PrivateApi")
+ Class> activityThread = Class.forName("android.app.ActivityThread");
+ Object thread = activityThread.getMethod("currentActivityThread").invoke(null);
+ Object app = activityThread.getMethod("getApplication").invoke(thread);
+ if (app == null) {
+ throw new NullPointerException("u should init first");
+ }
+ return (Application) app;
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ throw new NullPointerException("u should init first");
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // interface
+ ///////////////////////////////////////////////////////////////////////////
+
+ public abstract static class Task implements Runnable {
+
+ private static final int NEW = 0;
+ private static final int COMPLETING = 1;
+ private static final int CANCELLED = 2;
+ private static final int EXCEPTIONAL = 3;
+
+ private volatile int state = NEW;
+
+ abstract Result doInBackground();
+
+ private final Callback mCallback;
+
+ public Task(final Callback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void run() {
+ try {
+ final Result t = doInBackground();
+
+ if (state != NEW) return;
+ state = COMPLETING;
+ UTIL_HANDLER.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onCall(t);
+ }
+ });
+ } catch (Throwable th) {
+ if (state != NEW) return;
+ state = EXCEPTIONAL;
+ }
+ }
+
+ public void cancel() {
+ state = CANCELLED;
+ }
+
+ public boolean isDone() {
+ return state != NEW;
+ }
+
+ public boolean isCanceled() {
+ return state == CANCELLED;
+ }
+ }
+
+ public interface Callback {
+ void onCall(T data);
+ }
+
+ /**
+ * 判断是否打开定位
+ */
+ public static boolean getGpsStatus(Context ctx) {
+ //从系统服务中获取定位管理器
+ LocationManager locationManager
+ = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
+ // 通过GPS卫星定位,定位级别可以精确到街(通过24颗卫星定位,在室外和空旷的地方定位准确、速度快)
+ boolean gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ // 通过WLAN或移动网络(3G/2G)确定的位置(也称作AGPS,辅助GPS定位。主要用于在室内或遮盖物(建筑群或茂密的深林等)密集的地方定位)
+ boolean network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
+ if (gps || network) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 打开系统定位界面
+ */
+ public static void goToOpenGps(Context ctx) {
+ Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ ctx.startActivity(intent);
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java b/libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java
new file mode 100644
index 0000000..e732063
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java
@@ -0,0 +1,1294 @@
+package com.example.lib_utils;
+
+
+import android.content.Intent;
+import android.net.Uri;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import javax.net.ssl.HttpsURLConnection;
+
+/**
+ *
+ * author: Blankj
+ * blog : http://blankj.com
+ * user_ic_time : 2016/05/03
+ * desc : utils home_ic_about_us file
+ *
+ * https://github.com/Blankj/AndroidUtilCode/blob/master/lib/utilcode/src/main/java/com/blankj/utilcode/util/FileUtils.java
+ */
+public final class FileUtils2 {
+
+ private static final String LINE_SEP = System.getProperty("line.separator");
+
+ private FileUtils2() {
+ throw new UnsupportedOperationException("u can't instantiate me...");
+ }
+
+ /**
+ * Return the file by path.
+ *
+ * @param filePath The path of file.
+ * @return the file
+ */
+ public static File getFileByPath(final String filePath) {
+ return isSpace(filePath) ? null : new File(filePath);
+ }
+
+ /**
+ * Return whether the file exists.
+ *
+ * @param filePath The path of file.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isFileExists(final String filePath) {
+ return isFileExists(getFileByPath(filePath));
+ }
+
+ /**
+ * Return whether the file exists.
+ *
+ * @param file The file.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isFileExists(final File file) {
+ return file != null && file.exists();
+ }
+
+ /**
+ * Rename the file.
+ *
+ * @param filePath The path of file.
+ * @param newName The new name of file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean rename(final String filePath, final String newName) {
+ return rename(getFileByPath(filePath), newName);
+ }
+
+ /**
+ * Rename the file.
+ *
+ * @param file The file.
+ * @param newName The new name of file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean rename(final File file, final String newName) {
+ // file is null then return false
+ if (file == null) return false;
+ // file doesn't exist then return false
+ if (!file.exists()) return false;
+ // the new name is space then return false
+ if (isSpace(newName)) return false;
+ // the new name equals old name then return true
+ if (newName.equals(file.getName())) return true;
+ File newFile = new File(file.getParent() + File.separator + newName);
+ // the new name of file exists then return false
+ return !newFile.exists()
+ && file.renameTo(newFile);
+ }
+
+ /**
+ * Return whether it is a directory.
+ *
+ * @param dirPath The path of directory.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isDir(final String dirPath) {
+ return isDir(getFileByPath(dirPath));
+ }
+
+ /**
+ * Return whether it is a directory.
+ *
+ * @param file The file.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isDir(final File file) {
+ return file != null && file.exists() && file.isDirectory();
+ }
+
+ /**
+ * Return whether it is a file.
+ *
+ * @param filePath The path of file.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isFile(final String filePath) {
+ return isFile(getFileByPath(filePath));
+ }
+
+ /**
+ * Return whether it is a file.
+ *
+ * @param file The file.
+ * @return {@code true}: yes
{@code false}: no
+ */
+ public static boolean isFile(final File file) {
+ return file != null && file.exists() && file.isFile();
+ }
+
+ /**
+ * Create a directory if it doesn't exist, otherwise do nothing.
+ *
+ * @param dirPath The path of directory.
+ * @return {@code true}: exists or creates successfully
{@code false}: otherwise
+ */
+ public static boolean createOrExistsDir(final String dirPath) {
+ return createOrExistsDir(getFileByPath(dirPath));
+ }
+
+ /**
+ * Create a directory if it doesn't exist, otherwise do nothing.
+ *
+ * @param file The file.
+ * @return {@code true}: exists or creates successfully
{@code false}: otherwise
+ */
+ public static boolean createOrExistsDir(final File file) {
+ return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
+ }
+
+ /**
+ * Create a file if it doesn't exist, otherwise do nothing.
+ *
+ * @param filePath The path of file.
+ * @return {@code true}: exists or creates successfully
{@code false}: otherwise
+ */
+ public static boolean createOrExistsFile(final String filePath) {
+ return createOrExistsFile(getFileByPath(filePath));
+ }
+
+ /**
+ * Create a file if it doesn't exist, otherwise do nothing.
+ *
+ * @param file The file.
+ * @return {@code true}: exists or creates successfully
{@code false}: otherwise
+ */
+ public static boolean createOrExistsFile(final File file) {
+ if (file == null) return false;
+ if (file.exists()) return file.isFile();
+ if (!createOrExistsDir(file.getParentFile())) return false;
+ try {
+ return file.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Create a file if it doesn't exist, otherwise delete old file before creating.
+ *
+ * @param filePath The path of file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean createFileByDeleteOldFile(final String filePath) {
+ return createFileByDeleteOldFile(getFileByPath(filePath));
+ }
+
+ /**
+ * Create a file if it doesn't exist, otherwise delete old file before creating.
+ *
+ * @param file The file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean createFileByDeleteOldFile(final File file) {
+ if (file == null) return false;
+ // file exists and unsuccessfully delete then return false
+ if (file.exists() && !file.delete()) return false;
+ if (!createOrExistsDir(file.getParentFile())) return false;
+ try {
+ return file.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Copy the directory.
+ *
+ * @param srcDirPath The path of source directory.
+ * @param destDirPath The path of destination directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyDir(final String srcDirPath,
+ final String destDirPath) {
+ return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath));
+ }
+
+ /**
+ * Copy the directory.
+ *
+ * @param srcDirPath The path of source directory.
+ * @param destDirPath The path of destination directory.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyDir(final String srcDirPath,
+ final String destDirPath,
+ final OnReplaceListener listener) {
+ return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener);
+ }
+
+ /**
+ * Copy the directory.
+ *
+ * @param srcDir The source directory.
+ * @param destDir The destination directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyDir(final File srcDir,
+ final File destDir) {
+ return copyOrMoveDir(srcDir, destDir, false);
+ }
+
+ /**
+ * Copy the directory.
+ *
+ * @param srcDir The source directory.
+ * @param destDir The destination directory.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyDir(final File srcDir,
+ final File destDir,
+ final OnReplaceListener listener) {
+ return copyOrMoveDir(srcDir, destDir, listener, false);
+ }
+
+ /**
+ * Copy the file.
+ *
+ * @param srcFilePath The path of source file.
+ * @param destFilePath The path of destination file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyFile(final String srcFilePath,
+ final String destFilePath) {
+ return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath));
+ }
+
+ /**
+ * Copy the file.
+ *
+ * @param srcFilePath The path of source file.
+ * @param destFilePath The path of destination file.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyFile(final String srcFilePath,
+ final String destFilePath,
+ final OnReplaceListener listener) {
+ return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener);
+ }
+
+ /**
+ * Copy the file.
+ *
+ * @param srcFile The source file.
+ * @param destFile The destination file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyFile(final File srcFile,
+ final File destFile) {
+ return copyOrMoveFile(srcFile, destFile, false);
+ }
+
+ /**
+ * Copy the file.
+ *
+ * @param srcFile The source file.
+ * @param destFile The destination file.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean copyFile(final File srcFile,
+ final File destFile,
+ final OnReplaceListener listener) {
+ return copyOrMoveFile(srcFile, destFile, listener, false);
+ }
+
+ /**
+ * Move the directory.
+ *
+ * @param srcDirPath The path of source directory.
+ * @param destDirPath The path of destination directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveDir(final String srcDirPath,
+ final String destDirPath) {
+ return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath));
+ }
+
+ /**
+ * Move the directory.
+ *
+ * @param srcDirPath The path of source directory.
+ * @param destDirPath The path of destination directory.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveDir(final String srcDirPath,
+ final String destDirPath,
+ final OnReplaceListener listener) {
+ return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener);
+ }
+
+ /**
+ * Move the directory.
+ *
+ * @param srcDir The source directory.
+ * @param destDir The destination directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveDir(final File srcDir,
+ final File destDir) {
+ return copyOrMoveDir(srcDir, destDir, true);
+ }
+
+ /**
+ * Move the directory.
+ *
+ * @param srcDir The source directory.
+ * @param destDir The destination directory.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveDir(final File srcDir,
+ final File destDir,
+ final OnReplaceListener listener) {
+ return copyOrMoveDir(srcDir, destDir, listener, true);
+ }
+
+ /**
+ * Move the file.
+ *
+ * @param srcFilePath The path of source file.
+ * @param destFilePath The path of destination file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveFile(final String srcFilePath,
+ final String destFilePath) {
+ return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath));
+ }
+
+ /**
+ * Move the file.
+ *
+ * @param srcFilePath The path of source file.
+ * @param destFilePath The path of destination file.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveFile(final String srcFilePath,
+ final String destFilePath,
+ final OnReplaceListener listener) {
+ return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener);
+ }
+
+ /**
+ * Move the file.
+ *
+ * @param srcFile The source file.
+ * @param destFile The destination file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveFile(final File srcFile,
+ final File destFile) {
+ return copyOrMoveFile(srcFile, destFile, true);
+ }
+
+ /**
+ * Move the file.
+ *
+ * @param srcFile The source file.
+ * @param destFile The destination file.
+ * @param listener The replace listener.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean moveFile(final File srcFile,
+ final File destFile,
+ final OnReplaceListener listener) {
+ return copyOrMoveFile(srcFile, destFile, listener, true);
+ }
+
+ private static boolean copyOrMoveDir(final File srcDir,
+ final File destDir,
+ final boolean isMove) {
+ return copyOrMoveDir(srcDir, destDir, new OnReplaceListener() {
+ @Override
+ public boolean onReplace() {
+ return true;
+ }
+ }, isMove);
+ }
+
+ private static boolean copyOrMoveDir(final File srcDir,
+ final File destDir,
+ final OnReplaceListener listener,
+ final boolean isMove) {
+ if (srcDir == null || destDir == null) return false;
+ // destDir's path locate in srcDir's path then return false
+ String srcPath = srcDir.getPath() + File.separator;
+ String destPath = destDir.getPath() + File.separator;
+ if (destPath.contains(srcPath)) return false;
+ if (!srcDir.exists() || !srcDir.isDirectory()) return false;
+ if (destDir.exists()) {
+ if (listener == null || listener.onReplace()) {// require delete the old directory
+ if (!deleteAllInDir(destDir)) {// unsuccessfully delete then return false
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+ if (!createOrExistsDir(destDir)) return false;
+ File[] files = srcDir.listFiles();
+ for (File file : files) {
+ File oneDestFile = new File(destPath + file.getName());
+ if (file.isFile()) {
+ if (!copyOrMoveFile(file, oneDestFile, listener, isMove)) return false;
+ } else if (file.isDirectory()) {
+ if (!copyOrMoveDir(file, oneDestFile, listener, isMove)) return false;
+ }
+ }
+ return !isMove || deleteDir(srcDir);
+ }
+
+ private static boolean copyOrMoveFile(final File srcFile,
+ final File destFile,
+ final boolean isMove) {
+ return copyOrMoveFile(srcFile, destFile, new OnReplaceListener() {
+ @Override
+ public boolean onReplace() {
+ return true;
+ }
+ }, isMove);
+ }
+
+ private static boolean copyOrMoveFile(final File srcFile,
+ final File destFile,
+ final OnReplaceListener listener,
+ final boolean isMove) {
+ if (srcFile == null || destFile == null) return false;
+ // srcFile equals destFile then return false
+ if (srcFile.equals(destFile)) return false;
+ // srcFile doesn't exist or isn't a file then return false
+ if (!srcFile.exists() || !srcFile.isFile()) return false;
+ if (destFile.exists()) {
+ if (listener == null || listener.onReplace()) {// require delete the old file
+ if (!destFile.delete()) {// unsuccessfully delete then return false
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+ if (!createOrExistsDir(destFile.getParentFile())) return false;
+ try {
+ return writeFileFromIS(destFile, new FileInputStream(srcFile))
+ && !(isMove && !deleteFile(srcFile));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * Delete the directory.
+ *
+ * @param filePath The path of file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean delete(final String filePath) {
+ return delete(getFileByPath(filePath));
+ }
+
+ /**
+ * Delete the directory.
+ *
+ * @param file The file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean delete(final File file) {
+ if (file == null) return false;
+ if (file.isDirectory()) {
+ return deleteDir(file);
+ }
+ return deleteFile(file);
+ }
+
+ /**
+ * Delete the directory.
+ *
+ * @param dirPath The path of directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteDir(final String dirPath) {
+ return deleteDir(getFileByPath(dirPath));
+ }
+
+ /**
+ * Delete the directory.
+ *
+ * @param dir The directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteDir(final File dir) {
+ if (dir == null) return false;
+ // dir doesn't exist then return true
+ if (!dir.exists()) return true;
+ // dir isn't a directory then return false
+ if (!dir.isDirectory()) return false;
+ File[] files = dir.listFiles();
+ if (files != null && files.length != 0) {
+ for (File file : files) {
+ if (file.isFile()) {
+ if (!file.delete()) return false;
+ } else if (file.isDirectory()) {
+ if (!deleteDir(file)) return false;
+ }
+ }
+ }
+ return dir.delete();
+ }
+
+ /**
+ * Delete the file.
+ *
+ * @param srcFilePath The path of source file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteFile(final String srcFilePath) {
+ return deleteFile(getFileByPath(srcFilePath));
+ }
+
+ /**
+ * Delete the file.
+ *
+ * @param file The file.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteFile(final File file) {
+ return file != null && (!file.exists() || file.isFile() && file.delete());
+ }
+
+ /**
+ * Delete the all in directory.
+ *
+ * @param dirPath The path of directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteAllInDir(final String dirPath) {
+ return deleteAllInDir(getFileByPath(dirPath));
+ }
+
+ /**
+ * Delete the all in directory.
+ *
+ * @param dir The directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteAllInDir(final File dir) {
+ return deleteFilesInDirWithFilter(dir, new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Delete all files in directory.
+ *
+ * @param dirPath The path of directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteFilesInDir(final String dirPath) {
+ return deleteFilesInDir(getFileByPath(dirPath));
+ }
+
+ /**
+ * Delete all files in directory.
+ *
+ * @param dir The directory.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteFilesInDir(final File dir) {
+ return deleteFilesInDirWithFilter(dir, new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.isFile();
+ }
+ });
+ }
+
+ /**
+ * Delete all files that satisfy the filter in directory.
+ *
+ * @param dirPath The path of directory.
+ * @param filter The filter.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteFilesInDirWithFilter(final String dirPath,
+ final FileFilter filter) {
+ return deleteFilesInDirWithFilter(getFileByPath(dirPath), filter);
+ }
+
+ /**
+ * Delete all files that satisfy the filter in directory.
+ *
+ * @param dir The directory.
+ * @param filter The filter.
+ * @return {@code true}: success
{@code false}: fail
+ */
+ public static boolean deleteFilesInDirWithFilter(final File dir, final FileFilter filter) {
+ if (dir == null) return false;
+ // dir doesn't exist then return true
+ if (!dir.exists()) return true;
+ // dir isn't a directory then return false
+ if (!dir.isDirectory()) return false;
+ File[] files = dir.listFiles();
+ if (files != null && files.length != 0) {
+ for (File file : files) {
+ if (filter.accept(file)) {
+ if (file.isFile()) {
+ if (!file.delete()) return false;
+ } else if (file.isDirectory()) {
+ if (!deleteDir(file)) return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return the files in directory.
+ * Doesn't traverse subdirectories
+ *
+ * @param dirPath The path of directory.
+ * @return the files in directory
+ */
+ public static List listFilesInDir(final String dirPath) {
+ return listFilesInDir(dirPath, false);
+ }
+
+ /**
+ * Return the files in directory.
+ * Doesn't traverse subdirectories
+ *
+ * @param dir The directory.
+ * @return the files in directory
+ */
+ public static List listFilesInDir(final File dir) {
+ return listFilesInDir(dir, false);
+ }
+
+ /**
+ * Return the files in directory.
+ *
+ * @param dirPath The path of directory.
+ * @param isRecursive True to traverse subdirectories, false otherwise.
+ * @return the files in directory
+ */
+ public static List listFilesInDir(final String dirPath, final boolean isRecursive) {
+ return listFilesInDir(getFileByPath(dirPath), isRecursive);
+ }
+
+ /**
+ * Return the files in directory.
+ *
+ * @param dir The directory.
+ * @param isRecursive True to traverse subdirectories, false otherwise.
+ * @return the files in directory
+ */
+ public static List listFilesInDir(final File dir, final boolean isRecursive) {
+ return listFilesInDirWithFilter(dir, new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return true;
+ }
+ }, isRecursive);
+ }
+
+ /**
+ * Return the files that satisfy the filter in directory.
+ * Doesn't traverse subdirectories
+ *
+ * @param dirPath The path of directory.
+ * @param filter The filter.
+ * @return the files that satisfy the filter in directory
+ */
+ public static List listFilesInDirWithFilter(final String dirPath,
+ final FileFilter filter) {
+ return listFilesInDirWithFilter(getFileByPath(dirPath), filter, false);
+ }
+
+ /**
+ * Return the files that satisfy the filter in directory.
+ * Doesn't traverse subdirectories
+ *
+ * @param dir The directory.
+ * @param filter The filter.
+ * @return the files that satisfy the filter in directory
+ */
+ public static List listFilesInDirWithFilter(final File dir,
+ final FileFilter filter) {
+ return listFilesInDirWithFilter(dir, filter, false);
+ }
+
+ /**
+ * Return the files that satisfy the filter in directory.
+ *
+ * @param dirPath The path of directory.
+ * @param filter The filter.
+ * @param isRecursive True to traverse subdirectories, false otherwise.
+ * @return the files that satisfy the filter in directory
+ */
+ public static List listFilesInDirWithFilter(final String dirPath,
+ final FileFilter filter,
+ final boolean isRecursive) {
+ return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive);
+ }
+
+ /**
+ * Return the files that satisfy the filter in directory.
+ *
+ * @param dir The directory.
+ * @param filter The filter.
+ * @param isRecursive True to traverse subdirectories, false otherwise.
+ * @return the files that satisfy the filter in directory
+ */
+ public static List listFilesInDirWithFilter(final File dir,
+ final FileFilter filter,
+ final boolean isRecursive) {
+ if (!isDir(dir)) return null;
+ List list = new ArrayList<>();
+ File[] files = dir.listFiles();
+ if (files != null && files.length != 0) {
+ for (File file : files) {
+ if (filter.accept(file)) {
+ list.add(file);
+ }
+ if (isRecursive && file.isDirectory()) {
+ //noinspection ConstantConditions
+ list.addAll(listFilesInDirWithFilter(file, filter, true));
+ }
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Return the user_ic_time that the file was last modified.
+ *
+ * @param filePath The path of file.
+ * @return the user_ic_time that the file was last modified
+ */
+
+ public static long getFileLastModified(final String filePath) {
+ return getFileLastModified(getFileByPath(filePath));
+ }
+
+ /**
+ * Return the user_ic_time that the file was last modified.
+ *
+ * @param file The file.
+ * @return the user_ic_time that the file was last modified
+ */
+ public static long getFileLastModified(final File file) {
+ if (file == null) return -1;
+ return file.lastModified();
+ }
+
+ /**
+ * Return the charset of file simply.
+ *
+ * @param filePath The path of file.
+ * @return the charset of file simply
+ */
+ public static String getFileCharsetSimple(final String filePath) {
+ return getFileCharsetSimple(getFileByPath(filePath));
+ }
+
+ /**
+ * Return the charset of file simply.
+ *
+ * @param file The file.
+ * @return the charset of file simply
+ */
+ public static String getFileCharsetSimple(final File file) {
+ int p = 0;
+ InputStream is = null;
+ try {
+ is = new BufferedInputStream(new FileInputStream(file));
+ p = (is.read() << 8) + is.read();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ switch (p) {
+ case 0xefbb:
+ return "UTF-8";
+ case 0xfffe:
+ return "Unicode";
+ case 0xfeff:
+ return "UTF-16BE";
+ default:
+ return "GBK";
+ }
+ }
+
+ /**
+ * Return the number of lines of file.
+ *
+ * @param filePath The path of file.
+ * @return the number of lines of file
+ */
+ public static int getFileLines(final String filePath) {
+ return getFileLines(getFileByPath(filePath));
+ }
+
+ /**
+ * Return the number of lines of file.
+ *
+ * @param file The file.
+ * @return the number of lines of file
+ */
+ public static int getFileLines(final File file) {
+ int count = 1;
+ InputStream is = null;
+ try {
+ is = new BufferedInputStream(new FileInputStream(file));
+ byte[] buffer = new byte[1024];
+ int readChars;
+ if (LINE_SEP.endsWith("\n")) {
+ while ((readChars = is.read(buffer, 0, 1024)) != -1) {
+ for (int i = 0; i < readChars; ++i) {
+ if (buffer[i] == '\n') ++count;
+ }
+ }
+ } else {
+ while ((readChars = is.read(buffer, 0, 1024)) != -1) {
+ for (int i = 0; i < readChars; ++i) {
+ if (buffer[i] == '\r') ++count;
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Return the size of directory.
+ *
+ * @param dirPath The path of directory.
+ * @return the size of directory
+ */
+ public static String getDirSize(final String dirPath) {
+ return getDirSize(getFileByPath(dirPath));
+ }
+
+ /**
+ * Return the size of directory.
+ *
+ * @param dir The directory.
+ * @return the size of directory
+ */
+ public static String getDirSize(final File dir) {
+ long len = getDirLength(dir);
+ return len == -1 ? "" : byte2FitMemorySize(len);
+ }
+
+ /**
+ * Return the length of file.
+ *
+ * @param filePath The path of file.
+ * @return the length of file
+ */
+ public static String getFileSize(final String filePath) {
+ long len = getFileLength(filePath);
+ return len == -1 ? "" : byte2FitMemorySize(len);
+ }
+
+ /**
+ * Return the length of file.
+ *
+ * @param file The file.
+ * @return the length of file
+ */
+ public static String getFileSize(final File file) {
+ long len = getFileLength(file);
+ return len == -1 ? "" : byte2FitMemorySize(len);
+ }
+
+ /**
+ * Return the length of directory.
+ *
+ * @param dirPath The path of directory.
+ * @return the length of directory
+ */
+ public static long getDirLength(final String dirPath) {
+ return getDirLength(getFileByPath(dirPath));
+ }
+
+ /**
+ * Return the length of directory.
+ *
+ * @param dir The directory.
+ * @return the length of directory
+ */
+ public static long getDirLength(final File dir) {
+ if (!isDir(dir)) return -1;
+ long len = 0;
+ File[] files = dir.listFiles();
+ if (files != null && files.length != 0) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ len += getDirLength(file);
+ } else {
+ len += file.length();
+ }
+ }
+ }
+ return len;
+ }
+
+ /**
+ * Return the length of file.
+ *
+ * @param filePath The path of file.
+ * @return the length of file
+ */
+ public static long getFileLength(final String filePath) {
+ boolean isURL = filePath.matches("[a-zA-z]+://[^\\s]*");
+ if (isURL) {
+ try {
+ HttpsURLConnection conn = (HttpsURLConnection) new URL(filePath).openConnection();
+ conn.setRequestProperty("Accept-Encoding", "identity");
+ conn.connect();
+ if (conn.getResponseCode() == 200) {
+ return conn.getContentLength();
+ }
+ return -1;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return getFileLength(getFileByPath(filePath));
+ }
+
+ /**
+ * Return the length of file.
+ *
+ * @param file The file.
+ * @return the length of file
+ */
+ public static long getFileLength(final File file) {
+ if (!isFile(file)) return -1;
+ return file.length();
+ }
+
+ /**
+ * Return the MD5 of file.
+ *
+ * @param filePath The path of file.
+ * @return the md5 of file
+ */
+ public static String getFileMD5ToString(final String filePath) {
+ File file = isSpace(filePath) ? null : new File(filePath);
+ return getFileMD5ToString(file);
+ }
+
+ /**
+ * Return the MD5 of file.
+ *
+ * @param file The file.
+ * @return the md5 of file
+ */
+ public static String getFileMD5ToString(final File file) {
+ return bytes2HexString(getFileMD5(file));
+ }
+
+ /**
+ * Return the MD5 of file.
+ *
+ * @param filePath The path of file.
+ * @return the md5 of file
+ */
+ public static byte[] getFileMD5(final String filePath) {
+ return getFileMD5(getFileByPath(filePath));
+ }
+
+ /**
+ * Return the MD5 of file.
+ *
+ * @param file The file.
+ * @return the md5 of file
+ */
+ public static byte[] getFileMD5(final File file) {
+ if (file == null) return null;
+ DigestInputStream dis = null;
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ dis = new DigestInputStream(fis, md);
+ byte[] buffer = new byte[1024 * 256];
+ while (true) {
+ if (!(dis.read(buffer) > 0)) break;
+ }
+ md = dis.getMessageDigest();
+ return md.digest();
+ } catch (NoSuchAlgorithmException | IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if (dis != null) {
+ dis.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the file's path of directory.
+ *
+ * @param file The file.
+ * @return the file's path of directory
+ */
+ public static String getDirName(final File file) {
+ if (file == null) return "";
+ return getDirName(file.getAbsolutePath());
+ }
+
+ /**
+ * Return the file's path of directory.
+ *
+ * @param filePath The path of file.
+ * @return the file's path of directory
+ */
+ public static String getDirName(final String filePath) {
+ if (isSpace(filePath)) return "";
+ int lastSep = filePath.lastIndexOf(File.separator);
+ return lastSep == -1 ? "" : filePath.substring(0, lastSep + 1);
+ }
+
+ /**
+ * Return the name of file.
+ *
+ * @param file The file.
+ * @return the name of file
+ */
+ public static String getFileName(final File file) {
+ if (file == null) return "";
+ return getFileName(file.getAbsolutePath());
+ }
+
+ /**
+ * Return the name of file.
+ *
+ * @param filePath The path of file.
+ * @return the name of file
+ */
+ public static String getFileName(final String filePath) {
+ if (isSpace(filePath)) return "";
+ int lastSep = filePath.lastIndexOf(File.separator);
+ return lastSep == -1 ? filePath : filePath.substring(lastSep + 1);
+ }
+
+ /**
+ * Return the name of file without extension.
+ *
+ * @param file The file.
+ * @return the name of file without extension
+ */
+ public static String getFileNameNoExtension(final File file) {
+ if (file == null) return "";
+ return getFileNameNoExtension(file.getPath());
+ }
+
+ /**
+ * Return the name of file without extension.
+ *
+ * @param filePath The path of file.
+ * @return the name of file without extension
+ */
+ public static String getFileNameNoExtension(final String filePath) {
+ if (isSpace(filePath)) return "";
+ int lastPoi = filePath.lastIndexOf('.');
+ int lastSep = filePath.lastIndexOf(File.separator);
+ if (lastSep == -1) {
+ return (lastPoi == -1 ? filePath : filePath.substring(0, lastPoi));
+ }
+ if (lastPoi == -1 || lastSep > lastPoi) {
+ return filePath.substring(lastSep + 1);
+ }
+ return filePath.substring(lastSep + 1, lastPoi);
+ }
+
+ /**
+ * Return the extension of file.
+ *
+ * @param file The file.
+ * @return the extension of file
+ */
+ public static String getFileExtension(final File file) {
+ if (file == null) return "";
+ return getFileExtension(file.getPath());
+ }
+
+ /**
+ * Return the extension of file.
+ *
+ * @param filePath The path of file.
+ * @return the extension of file
+ */
+ public static String getFileExtension(final String filePath) {
+ if (isSpace(filePath)) return "";
+ int lastPoi = filePath.lastIndexOf('.');
+ int lastSep = filePath.lastIndexOf(File.separator);
+ if (lastPoi == -1 || lastSep >= lastPoi) return "";
+ return filePath.substring(lastPoi + 1);
+ }
+
+ /**
+ * Notify system to scan the file.
+ *
+ * @param file The file.
+ */
+ public static void notifySystemToScan(final File file) {
+ if (file == null || !file.exists()) return;
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ Uri uri = Uri.fromFile(file);
+ intent.setData(uri);
+ AppUtils.getApp().sendBroadcast(intent);
+ }
+
+ /**
+ * Notify system to scan the file.
+ *
+ * @param filePath The path of file.
+ */
+ public static void notifySystemToScan(final String filePath) {
+ notifySystemToScan(getFileByPath(filePath));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // interface
+ ///////////////////////////////////////////////////////////////////////////
+
+ public interface OnReplaceListener {
+ boolean onReplace();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // other utils methods
+ ///////////////////////////////////////////////////////////////////////////
+
+ private static final char[] HEX_DIGITS =
+ {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+ private static String bytes2HexString(final byte[] bytes) {
+ if (bytes == null) return "";
+ int len = bytes.length;
+ if (len <= 0) return "";
+ char[] ret = new char[len << 1];
+ for (int i = 0, j = 0; i < len; i++) {
+ ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
+ ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
+ }
+ return new String(ret);
+ }
+
+ public static String byte2FitMemorySize(final long byteNum) {
+ if (byteNum < 0) {
+ return "shouldn't be less than zero!";
+ } else if (byteNum < 1024) {
+ return String.format(Locale.getDefault(), "%.3fB", (double) byteNum);
+ } else if (byteNum < 1048576) {
+ return String.format(Locale.getDefault(), "%.3fKB", (double) byteNum / 1024);
+ } else if (byteNum < 1073741824) {
+ return String.format(Locale.getDefault(), "%.3fMB", (double) byteNum / 1048576);
+ } else {
+ return String.format(Locale.getDefault(), "%.3fGB", (double) byteNum / 1073741824);
+ }
+ }
+
+ private static boolean isSpace(final String s) {
+ if (s == null) return true;
+ for (int i = 0, len = s.length(); i < len; ++i) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean writeFileFromIS(final File file,
+ final InputStream is) {
+ OutputStream os = null;
+ try {
+ os = new BufferedOutputStream(new FileOutputStream(file));
+ byte[] data = new byte[8192];
+ int len;
+ while ((len = is.read(data, 0, 8192)) != -1) {
+ os.write(data, 0, len);
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ try {
+ if (os != null) {
+ os.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt
new file mode 100644
index 0000000..4268077
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt
@@ -0,0 +1,14 @@
+package com.example.lib_utils
+
+
+/**
+ * Created by Max on 2023/10/26 11:50
+ * Desc:清除释放统一接口
+ **/
+interface ICleared {
+
+ /**
+ * 清除/释放
+ */
+ fun onCleared() {}
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt
new file mode 100644
index 0000000..3f8a3ea
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt
@@ -0,0 +1,521 @@
+package com.example.lib_utils
+
+
+import android.app.Application
+import android.os.Build
+import android.os.Environment
+import java.io.File
+
+/**
+ * getRootPath : 获取根路径
+ * getDataPath : 获取数据路径
+ * getDownloadCachePath : 获取下载缓存路径
+ * getInternalAppDataPath : 获取内存应用数据路径
+ * getInternalAppCodeCacheDir : 获取内存应用代码缓存路径
+ * getInternalAppCachePath : 获取内存应用缓存路径
+ * getInternalAppDbsPath : 获取内存应用数据库路径
+ * getInternalAppDbPath : 获取内存应用数据库路径
+ * getInternalAppFilesPath : 获取内存应用文件路径
+ * getInternalAppSpPath : 获取内存应用 SP 路径
+ * getInternalAppNoBackupFilesPath: 获取内存应用未备份文件路径
+ * getExternalStoragePath : 获取外存路径
+ * getExternalMusicPath : 获取外存音乐路径
+ * getExternalPodcastsPath : 获取外存播客路径
+ * getExternalRingtonesPath : 获取外存铃声路径
+ * getExternalAlarmsPath : 获取外存闹铃路径
+ * getExternalNotificationsPath : 获取外存通知路径
+ * getExternalPicturesPath : 获取外存图片路径
+ * getExternalMoviesPath : 获取外存影片路径
+ * getExternalDownloadsPath : 获取外存下载路径
+ * getExternalDcimPath : 获取外存数码相机图片路径
+ * getExternalDocumentsPath : 获取外存文档路径
+ * getExternalAppDataPath : 获取外存应用数据路径
+ * getExternalAppCachePath : 获取外存应用缓存路径
+ * getExternalAppFilesPath : 获取外存应用文件路径
+ * getExternalAppMusicPath : 获取外存应用音乐路径
+ * getExternalAppPodcastsPath : 获取外存应用播客路径
+ * getExternalAppRingtonesPath : 获取外存应用铃声路径
+ * getExternalAppAlarmsPath : 获取外存应用闹铃路径
+ * getExternalAppNotificationsPath: 获取外存应用通知路径
+ * getExternalAppPicturesPath : 获取外存应用图片路径
+ * getExternalAppMoviesPath : 获取外存应用影片路径
+ * getExternalAppDownloadPath : 获取外存应用下载路径
+ * getExternalAppDcimPath : 获取外存应用数码相机图片路径
+ * getExternalAppDocumentsPath : 获取外存应用文档路径
+ * getExternalAppObbPath : 获取外存应用 OBB 路径
+ * 路径 工具类 By https://github.com/Blankj/AndroidUtilCode -> PathUtils.java
+ * Created by Max on 2018/12/12.
+ */
+object PathUtils {
+
+
+ /**
+ * Return the path of /system.
+ *
+ * @return the path of /system
+ */
+ val rootPath: String
+ get() = Environment.getRootDirectory().absolutePath
+
+ /**
+ * Return the path of /data.
+ *
+ * @return the path of /data
+ */
+ val dataPath: String
+ get() = Environment.getDataDirectory().absolutePath
+
+ /**
+ * Return the path of /cache.
+ *
+ * @return the path of /cache
+ */
+ val downloadCachePath: String
+ get() = Environment.getDownloadCacheDirectory().absolutePath
+
+ /**
+ * Return the path of /data/data/package.
+ *
+ * @return the path of /data/data/package
+ */
+ fun getInternalAppDataPath(application: Application): String {
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ application.applicationInfo.dataDir
+ } else application.dataDir.absolutePath
+ }
+
+ /**
+ * Return the path of /data/data/package/code_cache.
+ *
+ * @return the path of /data/data/package/code_cache
+ */
+ fun getInternalAppCodeCacheDir(application: Application): String {
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ application.applicationInfo.dataDir + "/code_cache"
+ } else application.codeCacheDir.absolutePath
+ }
+
+ /**
+ * Return the path of /data/data/package/cache.
+ *
+ * @return the path of /data/data/package/cache
+ */
+ fun getInternalAppCachePath(application: Application): String {
+ return application.cacheDir.absolutePath
+ }
+
+ /**
+ * Return the path of /data/data/package/databases.
+ *
+ * @return the path of /data/data/package/databases
+ */
+ fun getInternalAppDbsPath(application: Application): String {
+ return application.applicationInfo.dataDir + "/databases"
+ }
+
+ /**
+ * Return the path of /data/data/package/databases/name.
+ *
+ * @param name The name of database.
+ * @return the path of /data/data/package/databases/name
+ */
+ fun getInternalAppDbPath(application: Application, name: String?): String {
+ return application.getDatabasePath(name).absolutePath
+ }
+
+ /**
+ * Return the path of /data/data/package/files.
+ *
+ * @return the path of /data/data/package/files
+ */
+ fun getInternalAppFilesPath(application: Application): String {
+ return application.filesDir.absolutePath
+ }
+
+ /**
+ * Return the path of /data/data/package/shared_prefs.
+ *
+ * @return the path of /data/data/package/shared_prefs
+ */
+ fun getInternalAppSpPath(application: Application): String {
+ return application.applicationInfo.dataDir + "shared_prefs"
+ }
+
+ /**
+ * Return the path of /data/data/package/no_backup.
+ *
+ * @return the path of /data/data/package/no_backup
+ */
+ fun getInternalAppNoBackupFilesPath(application: Application): String {
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ application.applicationInfo.dataDir + "no_backup"
+ } else application.noBackupFilesDir.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0.
+ *
+ * @return the path of /storage/emulated/0
+ */
+ val externalStoragePath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStorageDirectory().absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Music.
+ *
+ * @return the path of /storage/emulated/0/Music
+ */
+ val externalMusicPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_MUSIC
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Podcasts.
+ *
+ * @return the path of /storage/emulated/0/Podcasts
+ */
+ val externalPodcastsPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PODCASTS
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Ringtones.
+ *
+ * @return the path of /storage/emulated/0/Ringtones
+ */
+ val externalRingtonesPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_RINGTONES
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Alarms.
+ *
+ * @return the path of /storage/emulated/0/Alarms
+ */
+ val externalAlarmsPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_ALARMS
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Notifications.
+ *
+ * @return the path of /storage/emulated/0/Notifications
+ */
+ val externalNotificationsPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_NOTIFICATIONS
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Pictures.
+ *
+ * @return the path of /storage/emulated/0/Pictures
+ */
+ val externalPicturesPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Movies.
+ *
+ * @return the path of /storage/emulated/0/Movies
+ */
+ val externalMoviesPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_MOVIES
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Download.
+ *
+ * @return the path of /storage/emulated/0/Download
+ */
+ val externalDownloadsPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/DCIM.
+ *
+ * @return the path of /storage/emulated/0/DCIM
+ */
+ val externalDcimPath: String?
+ get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DCIM
+ ).absolutePath
+
+ /**
+ * Return the path of /storage/emulated/0/Documents.
+ *
+ * @return the path of /storage/emulated/0/Documents
+ */
+ val externalDocumentsPath: String?
+ get() {
+ if (isExternalStorageDisable) return null
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ Environment.getExternalStorageDirectory().absolutePath + "/Documents"
+ } else Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOCUMENTS
+ ).absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package
+ */
+ fun getExternalAppDataPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.externalCacheDir?.parentFile?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/cache.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/cache
+ */
+ fun getExternalAppCachePath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.externalCacheDir?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files
+ */
+ fun getExternalAppFilesPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(null)?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Music.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Music
+ */
+ fun getExternalAppMusicPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_MUSIC
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Podcasts.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Podcasts
+ */
+ fun getExternalAppPodcastsPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_PODCASTS
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Ringtones.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Ringtones
+ */
+ fun getExternalAppRingtonesPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_RINGTONES
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Alarms.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Alarms
+ */
+ fun getExternalAppAlarmsPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_ALARMS
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Notifications.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Notifications
+ */
+ fun getExternalAppNotificationsPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_NOTIFICATIONS
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Pictures.
+ *
+ * @return path of /storage/emulated/0/Android/data/package/files/Pictures
+ */
+ fun getExternalAppPicturesPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_PICTURES
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Movies.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Movies
+ */
+ fun getExternalAppMoviesPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_MOVIES
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Download.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Download
+ */
+ fun getExternalAppDownloadPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_DOWNLOADS
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/DCIM.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/DCIM
+ */
+ fun getExternalAppDcimPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.getExternalFilesDir(
+ Environment.DIRECTORY_DCIM
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/data/package/files/Documents.
+ *
+ * @return the path of /storage/emulated/0/Android/data/package/files/Documents
+ */
+ fun getExternalAppDocumentsPath(application: Application): String? {
+ if (isExternalStorageDisable) return null
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ application.getExternalFilesDir(null)?.absolutePath + "/Documents"
+ } else application.getExternalFilesDir(
+ Environment.DIRECTORY_DOCUMENTS
+ )?.absolutePath
+ }
+
+ /**
+ * Return the path of /storage/emulated/0/Android/obb/package.
+ *
+ * @return the path of /storage/emulated/0/Android/obb/package
+ */
+ fun getExternalAppObbPath(application: Application): String? {
+ return if (isExternalStorageDisable) null else application.obbDir.absolutePath
+ }
+
+ private val isExternalStorageDisable: Boolean
+ private get() = Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()
+
+ /**
+ * 判断sub是否在parent之下的文件或子文件夹
+ *
+ * @param parent
+ * @param sub
+ * @return
+ */
+ fun isSub(parent: File, sub: File): Boolean {
+ return try {
+ sub.absolutePath.startsWith(parent.absolutePath)
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ /**
+ * 获取子绝对路径与父绝对路径的相对路径
+ *
+ * @param parentPath
+ * @param subPath
+ * @return
+ */
+ fun getRelativePath(parentPath: String?, subPath: String?): String? {
+ return try {
+ if (parentPath == null || subPath == null) {
+ return null
+ }
+ if (subPath.startsWith(parentPath)) {
+ subPath.substring(parentPath.length)
+ } else {
+ null
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ /**
+ * 拼接两个路径
+ *
+ * @param pathA 路径A
+ * @param pathB 路径B
+ * @return 拼接后的路径
+ */
+ fun plusPath(pathA: String?, pathB: String?): String? {
+ if (pathA == null) {
+ return pathB
+ }
+ if (pathB == null) {
+ return pathA
+ }
+ return plusPathNotNull(pathA, pathB)
+ }
+
+ /**
+ * 拼接两个路径
+ *
+ * @param pathA 路径A
+ * @param pathB 路径B
+ * @return 拼接后的路径
+ */
+ fun plusPathNotNull(pathA: String, pathB: String): String {
+ val pathAEndSeparator = pathA.endsWith(File.separator)
+ val pathBStartSeparator = pathB.startsWith(File.separator)
+ return if (pathAEndSeparator && pathBStartSeparator) {
+ pathA + pathB.substring(1)
+ } else if (pathAEndSeparator || pathBStartSeparator) {
+ pathA + pathB
+ } else {
+ pathA + File.separator + pathB
+ }
+ }
+
+ /**
+ * 获取后缀名称
+ * @param path 路径
+ * @return 后缀格式 .mp4 .gif 等
+ */
+ fun getSuffixType(path: String): String? {
+ if (path.isEmpty()) {
+ return null
+ }
+ val dotIndex = path.indexOfLast {
+ '.' == it
+ }
+ val separatorIndex = path.indexOfLast {
+ '/' == it
+ }
+ if (dotIndex >= 0 && dotIndex > separatorIndex) {
+ val suffix = path.substring(dotIndex)
+ val askIndex = suffix.indexOfLast {
+ '?' == it
+ }
+ return if (askIndex >= 0) {
+ suffix.substring(0, askIndex)
+ } else {
+ suffix
+ }
+ }
+ return null
+ }
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt
new file mode 100644
index 0000000..3dbf085
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt
@@ -0,0 +1,25 @@
+package com.example.lib_utils
+
+import android.os.SystemClock
+
+/**
+ * Created by Max on 2023/10/24 15:11
+ * Desc:服务器时间
+ */
+object ServiceTime {
+
+ // 服务器时间与系统开机时间的时差
+ private var serviceTimeDiff: Long? = null
+
+ val time
+ get() = if (serviceTimeDiff == null) System.currentTimeMillis()
+ else SystemClock.elapsedRealtime() + serviceTimeDiff!!
+
+ /**
+ * 刷新服务器时间
+ */
+ fun refreshServiceTime(time: Long) {
+ //serviceTimeDiff = 服务器时间 - 此刻系统启动时间
+ serviceTimeDiff = time - SystemClock.elapsedRealtime()
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt
new file mode 100644
index 0000000..2a6258b
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt
@@ -0,0 +1,42 @@
+package com.example.lib_utils
+
+import android.graphics.Outline
+import android.view.View
+import android.view.ViewOutlineProvider
+import kotlin.math.min
+
+/**
+ * Created by Max on 2023/10/24 15:11
+ * Desc:
+ */
+class ShapeViewOutlineProvider {
+
+ /**
+ * Created by Max on 2/25/21 1:48 PM
+ * Desc:圆角
+ */
+ class Round(var corner: Float) : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(
+ 0,
+ 0,
+ view.width,
+ view.height,
+ corner
+ )
+ }
+ }
+
+ /**
+ * Created by Max on 2/25/21 1:48 PM
+ * Desc:圆形
+ */
+ class Circle : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ val min = min(view.width, view.height)
+ val left = (view.width - min) / 2
+ val top = (view.height - min) / 2
+ outline.setOval(left, top, min, min)
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt
new file mode 100644
index 0000000..ca71030
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt
@@ -0,0 +1,39 @@
+package com.example.lib_utils
+
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import android.widget.EditText
+
+class SolveEditTextScrollClash(private val editText: EditText) : OnTouchListener {
+ override fun onTouch(view: View, event: MotionEvent): Boolean {
+ //触摸的是EditText而且当前EditText能够滚动则将事件交给EditText处理。否则将事件交由其父类处理
+ if (view.id == editText.id && canVerticalScroll(editText)) {
+ view.parent.requestDisallowInterceptTouchEvent(true)
+ if (event.action == MotionEvent.ACTION_UP) {
+ view.parent.requestDisallowInterceptTouchEvent(false)
+ }
+ }
+ return false
+ }
+
+ /**
+ * EditText竖直方向能否够滚动
+ * @param editText 须要推断的EditText
+ * @return true:能够滚动 false:不能够滚动
+ */
+ private fun canVerticalScroll(editText: EditText): Boolean {
+ //滚动的距离
+ val scrollY = editText.scrollY
+ //控件内容的总高度
+ val scrollRange = editText.layout.height
+ //控件实际显示的高度
+ val scrollExtent =
+ editText.height - editText.compoundPaddingTop - editText.compoundPaddingBottom
+ //控件内容总高度与实际显示高度的差值
+ val scrollDifference = scrollRange - scrollExtent
+ return if (scrollDifference == 0) {
+ false
+ } else scrollY > 0 || scrollY < scrollDifference - 1
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt
new file mode 100644
index 0000000..0562645
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt
@@ -0,0 +1,68 @@
+package com.example.lib_utils
+
+import java.util.regex.Pattern
+
+/**
+ * Created by Max on 2/10/21 4:56 PM
+ * Desc:字符串工具
+ */
+object StringUtils2 {
+
+ fun toInt(str: String?): Int {
+ return str?.toIntOrNull() ?: 0
+ }
+
+ /**
+ * 拆分字符串(根据匹配规则,按顺序拆分出来)
+ * @param pattern 匹配节点的规则模式
+ * @param onNormalNode<节点内容> 普通节点
+ * @param onMatchNode<节点内容> 匹配节点
+ */
+ fun split(
+ content: String,
+ pattern: Pattern,
+ onNormalNode: (String) -> Unit,
+ onMatchNode: (String) -> Unit,
+ ) {
+ try {
+ if (content.isEmpty()) {
+ onNormalNode.invoke(content)
+ return
+ }
+ val matcher = pattern.matcher(content)
+ // 最后一个匹配项的结束位置
+ var lastItemEnd = 0
+ var noMatch = true
+ while (matcher.find()) {
+ noMatch = false
+ // 匹配元素的开启位置
+ val start = matcher.start()
+ // 匹配元素的结束位置
+ val end = matcher.end()
+ // 匹配元素的文本
+ val text = matcher.group()
+ // 匹配元素的对应索引
+// logD("split() start:$start ,end:$end ,text:$text")
+ if (start > lastItemEnd) {
+ // 普通节点
+ val nodeContent = content.substring(lastItemEnd, start)
+ onNormalNode.invoke(nodeContent)
+ }
+ // 匹配节点显示内容
+ onMatchNode.invoke(text)
+ lastItemEnd = end
+ }
+ if (lastItemEnd > 0 && lastItemEnd < content.length) {
+ // 最后的匹配项不是尾部(追加最后的尾部)
+ val nodeContent = content.substring(lastItemEnd, content.length)
+ onNormalNode.invoke(nodeContent)
+ }
+ if (noMatch) {
+ // 无匹配
+ onNormalNode.invoke(content)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt
new file mode 100644
index 0000000..210b4ba
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt
@@ -0,0 +1,148 @@
+package com.example.lib_utils
+
+import android.content.Context
+import android.telephony.TelephonyManager
+import com.example.lib_utils.log.ILog
+
+/**
+ * Created by Max on 2023/11/14 10:17
+ * Desc:TelephonyManager 相关工具
+ **/
+object TelephonyUtils : ILog {
+
+ /**
+ * 是否为中国运营商(任意卡属于中国就为true)
+ */
+ fun isChinaOperator(): Boolean {
+ try {
+ val tm =
+ AppUtils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
+ ?: return false
+ if (tm.simState == TelephonyManager.SIM_STATE_READY) {
+ if (!tm.simOperator.isNullOrEmpty() && tm.simOperator.startsWith("460")) {
+ return true
+ }
+ if (isChainOperator(tm.simOperatorName)) {
+ return true
+ }
+ }
+
+ if (!tm.networkOperator.isNullOrEmpty() && tm.networkOperator.startsWith("460")) {
+ return true
+ }
+ if (isChainOperator(tm.networkOperatorName)) {
+ return true
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return false
+ }
+
+ /**
+ * 获取运营商(优先SIM)
+ */
+ fun getOperatorFirstSim(): String? {
+ val operator = getSimOperator()
+ return if (operator.isNullOrEmpty()) {
+ getNetWorkOperator()
+ } else {
+ operator
+ }
+ }
+
+ /**
+ * 获取SIM运营商名称
+ */
+ fun getSimOperator(): String? {
+ try {
+ val tm =
+ AppUtils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
+ ?: return null
+ if (tm.simState != TelephonyManager.SIM_STATE_READY) {
+ logD("SIM状态不对:${tm.simState}")
+ return null
+ }
+ val simOperator = tm.simOperator
+ logD("getSimOperator()获取的MCC+MNC为:$simOperator")
+ logD("getOperatorName()方法获取的运营商名称为:${tm.simOperatorName} ")
+ logD("通过getSimOperator()人为判断的运营商名称是: ${getOperatorName(simOperator)}")
+ return simOperator
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ }
+ }
+
+ /**
+ * 获取网络运营商
+ */
+ fun getNetWorkOperator(): String? {
+ try {
+ val tm =
+ AppUtils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
+ ?: return null
+ //用于判断拨号那张卡的运营商
+ val networkOperator = tm.networkOperator
+ logD("getNetWorkOperator() 获取的MCC+MNC为:$networkOperator")
+ logD("getNetWorkOperator() phoneType:${tm.phoneType}")
+ logD("getNetworkOperatorName()方法获取的网络类型名称是: ${tm.networkOperatorName}")
+ logD("通过getNetWorkOperator()人为判断的运营商名称是: ${getOperatorName(networkOperator)}")
+ return tm.networkOperator
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ }
+ }
+
+ /**
+ * 是否中国运营商
+ */
+ private fun isChainOperator(operatorName: String?): Boolean {
+ if (operatorName == null) return false
+ if (operatorName == "CUCC"
+ || operatorName == "CMCC"
+ || operatorName == "CTCC"
+ || operatorName == "CTT"
+ || operatorName.contains("中国")
+ || operatorName.contains("中國")
+ ) {
+ return true
+ }
+ return false
+ }
+
+ /**
+ * 运营商类型
+ */
+ private fun getOperatorName(simOperator: String?): String? {
+ if (simOperator == null) {
+ return null
+ }
+ return when (simOperator) {
+ "46001", "46006", "46009" -> {
+ // 联通
+ "CUCC"
+ }
+
+ "46000", "46002", "46004", "46007" -> {
+ // 移动
+ "CMCC"
+ }
+
+ "46003", "46005", "46011" -> {
+ // 电信
+ "CTCC"
+ }
+
+ "46020" -> {
+ // 铁通
+ "CTT"
+ }
+
+ else -> {
+ "OHTER"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt
new file mode 100644
index 0000000..e8fb503
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt
@@ -0,0 +1,79 @@
+package com.example.lib_utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.os.LocaleList
+import android.util.DisplayMetrics
+import android.util.TypedValue
+import android.view.View
+import android.view.WindowManager
+import androidx.core.text.TextUtilsCompat
+import androidx.core.view.ViewCompat
+import java.util.Locale
+
+
+/**
+ * Created by Max on 2023/10/24 15:11
+ */
+
+
+object UiUtils {
+ fun getScreenWidth(context: Context): Int {
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
+ val outMetrics = DisplayMetrics()
+ wm?.defaultDisplay?.getMetrics(outMetrics)
+ return outMetrics.widthPixels
+ }
+
+ fun getScreenHeight(context: Context): Int {
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
+ val outMetrics = DisplayMetrics()
+ wm?.defaultDisplay?.getMetrics(outMetrics)
+ return outMetrics.heightPixels
+ }
+
+ fun getScreenRatio(context: Context): Float {
+ return getScreenWidth(context) * 1.0f / getScreenHeight(context)
+ }
+
+ /**
+ * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
+ */
+ fun dip2px(dpValue: Float): Int {
+ return dip2px(AppUtils.getApp(), dpValue)
+ }
+
+ /**
+ * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
+ */
+ fun px2dip(pxValue: Float): Float {
+ return px2dip(AppUtils.getApp(), pxValue)
+ }
+
+ /**
+ * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
+ */
+ fun dip2px(context: Context, dpValue: Float): Int {
+ return (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.resources.displayMetrics) + 0.5f).toInt()
+ }
+
+ /**
+ * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
+ */
+ fun px2dip(context: Context, pxValue: Float): Float {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, pxValue, context.resources.displayMetrics)
+ }
+
+ /**
+ * 是否从右到左布局
+ */
+ fun isRtl(context: Context): Boolean {
+ val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ context.resources.configuration.locales.get(0)
+ } else {
+ context.resources.configuration.locale
+ }
+ return TextUtilsCompat.getLayoutDirectionFromLocale(locale) == ViewCompat.LAYOUT_DIRECTION_RTL
+ }
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt
new file mode 100644
index 0000000..ddb3255
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt
@@ -0,0 +1,70 @@
+package com.example.lib_utils.ktx
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+
+/**
+ * Created by Max on 2023/10/25 15:57
+ * Desc:Context相关工具
+ **/
+
+
+/**
+ * Context转换为Activity
+ */
+fun Context?.asActivity(): Activity? {
+ return when {
+ this is Activity -> {
+ this
+ }
+ (this as? ContextWrapper)?.baseContext?.applicationContext != null -> {
+ baseContext.asActivity()
+ }
+ else -> {
+ null
+ }
+ }
+}
+
+/**
+ * Context转换为Lifecycle
+ */
+fun Context?.asLifecycle(): Lifecycle? {
+ if (this == null) return null
+ return when (this) {
+ is Lifecycle -> {
+ this
+ }
+ is LifecycleOwner -> {
+ this.lifecycle
+ }
+ is ContextWrapper -> {
+ this.baseContext.asLifecycle()
+ }
+ else -> {
+ null
+ }
+ }
+}
+
+
+/**
+ * Context转换为LifecycleOwner
+ */
+fun Context?.asLifecycleOwner(): LifecycleOwner? {
+ if (this == null) return null
+ return when (this) {
+ is LifecycleOwner -> {
+ this
+ }
+ is ContextWrapper -> {
+ this.baseContext.asLifecycleOwner()
+ }
+ else -> {
+ null
+ }
+ }
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt
new file mode 100644
index 0000000..9e65bae
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt
@@ -0,0 +1,105 @@
+package com.example.lib_utils.ktx
+
+import android.text.Editable
+import android.text.InputFilter
+import android.text.InputFilter.LengthFilter
+import android.text.TextWatcher
+import android.text.method.HideReturnsTransformationMethod
+import android.text.method.PasswordTransformationMethod
+import android.widget.EditText
+
+
+/**
+ * 设置editText输入监听
+ * @param onChanged 改变事件
+ * @return 是否接受此次文本的改变
+ */
+inline fun EditText.setOnInputChangedListener(
+ /**
+ * @param Int:当前长度
+ * @return 是否接受此次文本的改变
+ */
+ crossinline onChanged: (Int).() -> Boolean
+) {
+ this.addTextChangedListener(object : TextWatcher {
+
+ var flag = false
+
+ override fun afterTextChanged(p0: Editable?) {
+ if (flag) {
+ return
+ }
+ if (!onChanged(p0?.length ?: 0)) {
+ flag = true
+ this@setOnInputChangedListener.setText(
+ this@setOnInputChangedListener.getTag(
+ 1982329101
+ ) as? String
+ )
+ this@setOnInputChangedListener.setSelection(this@setOnInputChangedListener.length())
+ flag = false
+ } else {
+ flag = false
+ }
+ }
+
+ override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
+ this@setOnInputChangedListener.setTag(1982329101, p0?.toString())
+ }
+
+ override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
+ }
+ })
+}
+
+/**
+ * 切换密码可见度
+ */
+fun EditText.switchPasswordVisibility(visibility: Boolean) {
+ transformationMethod =
+ if (!visibility) HideReturnsTransformationMethod.getInstance() else PasswordTransformationMethod.getInstance()
+
+}
+
+/**
+ * 设置输入功能是否启用(不启用就相当于TextView)
+ */
+fun EditText.setInputEnabled(isEnabled: Boolean) {
+ if (isEnabled) {
+ isFocusable = true
+ isFocusableInTouchMode = true
+ isClickable = true
+ } else {
+ isFocusable = false
+ isFocusableInTouchMode = false
+ isClickable = false
+ keyListener = null
+ }
+}
+
+/**
+ * 添加输入长度限制过滤器
+ */
+fun EditText.addLengthFilter(maxLength: Int) {
+ val newFilters = filters.copyOf(filters.size + 1)
+ newFilters[newFilters.size - 1] = LengthFilter(maxLength)
+ filters = newFilters
+}
+
+
+/**
+ * 添加禁用文本过滤器
+ * @param disableText 不允许输入该文本
+ */
+fun EditText.addDisableFilter(vararg disableText: CharSequence) {
+ val newFilters = filters.copyOf(filters.size + 1)
+ newFilters[newFilters.size - 1] = InputFilter { source, p1, p2, p3, p4, p5 ->
+ disableText.forEach {
+ if (source.equals(it)) {
+ return@InputFilter ""
+ }
+ }
+ return@InputFilter null
+ }
+ filters = newFilters
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt
new file mode 100644
index 0000000..8cfb5d5
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt
@@ -0,0 +1,250 @@
+package com.example.lib_utils.ktx
+
+import android.content.*
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import android.util.Log
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.InputStream
+import java.io.OutputStream
+
+
+private const val TAG = "ImageToAlbumKtx"
+
+private val ALBUM_DIR = Environment.DIRECTORY_PICTURES
+
+private class OutputFileTaker(var file: File? = null)
+
+/**
+ * 复制图片文件到相册的Pictures文件夹
+ *
+ * @param context 上下文
+ * @param fileName 文件名。 需要携带后缀
+ * @param relativePath 相对于Pictures的路径
+ */
+fun File.copyToAlbum(context: Context, fileName: String, relativePath: String?): Uri? {
+ if (!this.canRead() || !this.exists()) {
+ Log.w(TAG, "check: read file error: $this")
+ return null
+ }
+ return this.inputStream().use {
+ it.saveToAlbum(context, fileName, relativePath)
+ }
+}
+
+/**
+ * 保存图片Stream到相册的Pictures文件夹
+ *
+ * @param context 上下文
+ * @param fileName 文件名。 需要携带后缀
+ * @param relativePath 相对于Pictures的路径
+ */
+fun InputStream.saveToAlbum(context: Context, fileName: String, relativePath: String?): Uri? {
+ val resolver = context.contentResolver
+ val outputFile = OutputFileTaker()
+ val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile)
+ if (imageUri == null) {
+ Log.w(TAG, "insert: error: uri == null")
+ return null
+ }
+
+ (imageUri.outputStream(resolver) ?: return null).use { output ->
+ this.use { input ->
+ input.copyTo(output)
+ imageUri.finishPending(context, resolver, outputFile.file)
+ }
+ }
+ return imageUri
+}
+
+/**
+ * 保存Bitmap到相册的Pictures文件夹
+ *
+ * https://developer.android.google.cn/training/data-storage/shared/media
+ *
+ * @param context 上下文
+ * @param fileName 文件名。 需要携带后缀
+ * @param relativePath 相对于Pictures的路径
+ * @param quality 质量
+ */
+fun Bitmap.saveToAlbum(
+ context: Context,
+ fileName: String,
+ relativePath: String? = null,
+ quality: Int = 75,
+): Uri? {
+ // 插入图片信息
+ val resolver = context.contentResolver
+ val outputFile = OutputFileTaker()
+ val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile)
+ if (imageUri == null) {
+ Log.w(TAG, "insert: error: uri == null")
+ return null
+ }
+
+ // 保存图片
+ (imageUri.outputStream(resolver) ?: return null).use {
+ val format = fileName.getBitmapFormat()
+ this@saveToAlbum.compress(format, quality, it)
+ imageUri.finishPending(context, resolver, outputFile.file)
+ }
+ return imageUri
+}
+
+private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
+ return try {
+ resolver.openOutputStream(this)
+ } catch (e: FileNotFoundException) {
+ Log.e(TAG, "save: open stream error: $e")
+ null
+ }
+}
+
+private fun Uri.finishPending(
+ context: Context,
+ resolver: ContentResolver,
+ outputFile: File?,
+) {
+ val imageValues = ContentValues()
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if (outputFile != null) {
+ imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
+ }
+ resolver.update(this, imageValues, null, null)
+ // 通知媒体库更新
+ val intent = Intent(@Suppress("DEPRECATION") Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
+ context.sendBroadcast(intent)
+ } else {
+ // Android Q添加了IS_PENDING状态,为0时其他应用才可见
+ imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+ resolver.update(this, imageValues, null, null)
+ }
+}
+
+private fun String.getBitmapFormat(): Bitmap.CompressFormat {
+ val fileName = this.lowercase()
+ return when {
+ fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG
+ fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG
+ fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+ Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP
+ else -> Bitmap.CompressFormat.PNG
+ }
+}
+
+private fun String.getMimeType(): String? {
+ val fileName = this.lowercase()
+ return when {
+ fileName.endsWith(".png") -> "image/png"
+ fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
+ fileName.endsWith(".webp") -> "image/webp"
+ fileName.endsWith(".gif") -> "image/gif"
+ else -> null
+ }
+}
+
+/**
+ * 插入图片到媒体库
+ */
+private fun ContentResolver.insertMediaImage(
+ fileName: String,
+ relativePath: String?,
+ outputFileTaker: OutputFileTaker? = null,
+): Uri? {
+ // 图片信息
+ val imageValues = ContentValues().apply {
+ val mimeType = fileName.getMimeType()
+ if (mimeType != null) {
+ put(MediaStore.Images.Media.MIME_TYPE, mimeType)
+ }
+ val date = System.currentTimeMillis() / 1000
+ put(MediaStore.Images.Media.DATE_ADDED, date)
+ put(MediaStore.Images.Media.DATE_MODIFIED, date)
+ }
+ // 保存的位置
+ val collection: Uri
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val path = if (relativePath != null) "${ALBUM_DIR}/${relativePath}" else ALBUM_DIR
+ imageValues.apply {
+ put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
+ put(MediaStore.Images.Media.RELATIVE_PATH, path)
+ put(MediaStore.Images.Media.IS_PENDING, 1)
+ }
+ collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+ // 高版本不用查重直接插入,会自动重命名
+ } else {
+ // 老版本
+ val pictures =
+ @Suppress("DEPRECATION") Environment.getExternalStoragePublicDirectory(ALBUM_DIR)
+ val saveDir = if (relativePath != null) File(pictures, relativePath) else pictures
+
+ if (!saveDir.exists() && !saveDir.mkdirs()) {
+ Log.e(TAG, "save: error: can't create Pictures directory")
+ return null
+ }
+
+ // 文件路径查重,重复的话在文件名后拼接数字
+ var imageFile = File(saveDir, fileName)
+ val fileNameWithoutExtension = imageFile.nameWithoutExtension
+ val fileExtension = imageFile.extension
+
+ var queryUri = this.queryMediaImage28(imageFile.absolutePath)
+ var suffix = 1
+ while (queryUri != null) {
+ val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension
+ imageFile = File(saveDir, newName)
+ queryUri = this.queryMediaImage28(imageFile.absolutePath)
+ }
+
+ imageValues.apply {
+ put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
+ // 保存路径
+ val imagePath = imageFile.absolutePath
+ Log.v(TAG, "save file: $imagePath")
+ put(@Suppress("DEPRECATION") MediaStore.Images.Media.DATA, imagePath)
+ }
+ outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
+ collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ }
+ // 插入图片信息
+ return this.insert(collection, imageValues)
+}
+
+/**
+ * Android Q以下版本,查询媒体库中当前路径是否存在
+ * @return Uri 返回null时说明不存在,可以进行图片插入逻辑
+ */
+private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
+
+ val imageFile = File(imagePath)
+ if (imageFile.canRead() && imageFile.exists()) {
+ Log.v(TAG, "query: path: $imagePath exists")
+ // 文件已存在,返回一个file://xxx的uri
+ return Uri.fromFile(imageFile)
+ }
+ // 保存的位置
+ val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+
+ // 查询是否已经存在相同图片
+ val query = this.query(
+ collection,
+ arrayOf(MediaStore.Images.Media._ID, @Suppress("DEPRECATION") MediaStore.Images.Media.DATA),
+ "${@Suppress("DEPRECATION") MediaStore.Images.Media.DATA} == ?",
+ arrayOf(imagePath), null
+ )
+ query?.use {
+ while (it.moveToNext()) {
+ val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+ val id = it.getLong(idColumn)
+ val existsUri = ContentUris.withAppendedId(collection, id)
+ Log.v(TAG, "query: path: $imagePath exists uri: $existsUri")
+ return existsUri
+ }
+ }
+ return null
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt
new file mode 100644
index 0000000..2eace68
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt
@@ -0,0 +1,216 @@
+package com.example.lib_utils.ktx
+
+import android.app.Activity
+import android.content.Context
+import android.content.res.TypedArray
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.Fragment
+import com.example.lib_utils.AppUtils
+
+/**
+ * Created by Max on 2023/10/24 15:11
+ * 资源工具类
+ */
+
+
+/**
+ * 获取颜色
+ */
+fun Fragment.getColorById(@ColorRes colorResId: Int): Int {
+ return ContextCompat.getColor(context!!, colorResId)
+}
+
+
+/**
+ * 获取图片
+ */
+fun Fragment.getDrawableById(@DrawableRes drawableRedId: Int): Drawable? {
+ return ContextCompat.getDrawable(context!!, drawableRedId)
+}
+
+
+/**
+ * 获取颜色
+ */
+fun Activity.getColorById(@ColorRes colorResId: Int): Int {
+ return ContextCompat.getColor(this, colorResId)
+}
+
+/**
+ * 获取图片
+ */
+fun Activity.getDrawableById(@DrawableRes drawableRedId: Int): Drawable? {
+ return ContextCompat.getDrawable(this, drawableRedId)
+}
+
+
+/**
+ * 获取颜色
+ */
+fun Context.getColorById(@ColorRes colorResId: Int): Int {
+ return ContextCompat.getColor(this, colorResId)
+}
+
+/**
+ * 获取图片
+ */
+fun Context.getDrawableById(@DrawableRes drawableRedId: Int): Drawable? {
+ return ContextCompat.getDrawable(this, drawableRedId)
+}
+
+
+/**
+ * 获取字符串资源
+ */
+fun Any.getStringById(@StringRes stringResId: Int): String {
+ return AppUtils.getApp().getString(stringResId)
+}
+
+/**
+ * 获取字符串资源
+ */
+fun Int.getString(): String {
+ return AppUtils.getApp().getString(this)
+}
+fun Int.getString(vararg : Any): String {
+ return AppUtils.getApp().getString(this,vararg)
+}
+
+/**
+ * *any 使用 *来展开数组
+ */
+fun Int.getString(vararg any : Any): String {
+ return AppUtils.getApp().getString(this,*any)
+}
+
+fun Int.getDimension(): Float {
+ return AppUtils.getApp().resources.getDimension(this)
+}
+
+fun Int.getDimensionToInt(): Int {
+ return this.getDimension().toInt()
+}
+
+/**
+ * 获取资源drawable
+ * */
+fun Int.getDrawable(): Drawable? {
+ return ContextCompat.getDrawable(AppUtils.getApp(), this)
+}
+
+/**
+ * 获取资源color
+ * */
+fun Int.getColor(): Int {
+ return ContextCompat.getColor(AppUtils.getApp(), this)
+}
+
+/**
+ * 通过自定义属性-获取DrawableRes
+ */
+@DrawableRes
+fun Context.getDrawableResFromAttr(
+ @AttrRes attrResId: Int,
+ typedValue: TypedValue = TypedValue(),
+ resolveRefs: Boolean = true
+): Int? {
+ return try {
+ theme.resolveAttribute(attrResId, typedValue, resolveRefs)
+ return typedValue.resourceId
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+}
+
+/**
+ * 通过自定义属性-获取Drawable
+ */
+fun Context.getDrawableFromAttr(@AttrRes attrId: Int): Drawable? {
+ return try {
+ val drawableRes = getDrawableResFromAttr(attrId) ?: return null
+ ResourcesCompat.getDrawable(resources, drawableRes, null)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+}
+
+/**
+ * 通过自定义属性-获取ColorRes
+ */
+@ColorRes
+fun Context.getColorResFromAttr(
+ @AttrRes attrResId: Int,
+ typedValue: TypedValue = TypedValue(),
+ resolveRefs: Boolean = true
+): Int? {
+ return try {
+ theme.resolveAttribute(attrResId, typedValue, resolveRefs)
+ return typedValue.resourceId
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+}
+
+/**
+ * 通过自定义属性-获取Color
+ */
+@ColorRes
+fun Context.getColorFromAttr(
+ @AttrRes attrResId: Int
+): Int? {
+ return try {
+ val colorRes = getColorFromAttr(attrResId) ?: return null
+ ResourcesCompat.getColor(resources, colorRes, null)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+}
+
+/**
+ * 通过自定义属性-获取LayoutRes
+ */
+@LayoutRes
+fun Context.getLayoutResFromAttr(
+ @AttrRes attrResId: Int,
+ typedValue: TypedValue = TypedValue(),
+ resolveRefs: Boolean = true
+): Int? {
+ return try {
+ theme.resolveAttribute(attrResId, typedValue, resolveRefs)
+ return typedValue.resourceId
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+}
+
+/**
+ * 通过自定义属性-获取Boolean
+ */
+fun Context.getBooleanResFromAttr(
+ @AttrRes attrResId: Int,
+ defValue: Boolean = false
+): Boolean {
+ var attrs: TypedArray? = null
+ try {
+ attrs = obtainStyledAttributes(null, intArrayOf(attrResId))
+ return attrs.getBoolean(0, defValue)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ attrs?.recycle()
+ }
+ return defValue
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt
new file mode 100644
index 0000000..5636663
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt
@@ -0,0 +1,51 @@
+package com.example.lib_utils.ktx
+
+import com.example.lib_utils.UiUtils
+import kotlin.math.roundToInt
+
+/**
+ * Created by Max on 2023/10/24 15:11
+ */
+
+
+/**
+ * 转换为PX值
+ */
+val Float.dp: Int get() = this.toPX()
+val Int.dp: Int get() = this.toPX()
+
+/**
+ * 转换为DP值
+ */
+val Float.px: Int get() = this.toDP().roundToInt()
+val Int.px: Int get() = this.toDP().roundToInt()
+
+
+fun Long.toDP(): Float {
+ return UiUtils.px2dip(this.toFloat())
+}
+
+
+fun Float.toDP(): Float {
+ return UiUtils.px2dip(this)
+}
+
+
+fun Int.toDP(): Float {
+ return UiUtils.px2dip(this.toFloat())
+}
+
+
+fun Long.toPX(): Int {
+ return UiUtils.dip2px(this.toFloat())
+}
+
+
+fun Float.toPX(): Int {
+ return UiUtils.dip2px(this)
+}
+
+
+fun Int.toPX(): Int {
+ return UiUtils.dip2px(this.toFloat())
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt
new file mode 100644
index 0000000..a60a15b
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt
@@ -0,0 +1,192 @@
+package com.example.lib_utils.ktx
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import android.os.Build
+import android.os.SystemClock
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Checkable
+import android.widget.TextView
+import androidx.core.view.ScrollingView
+import com.example.lib_utils.ShapeViewOutlineProvider
+
+
+/**
+ * 展示or隐藏
+ */
+fun View.visibleOrGone(isShow: Boolean) {
+ visibility = if (isShow) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+}
+
+/**
+ * 展示or隐藏
+ */
+inline fun View.visibleOrGone(show: View.() -> Boolean = { true }) {
+ visibility = if (show(this)) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+}
+
+/**
+ * 展示or不可见
+ */
+inline fun View.visibleOrInvisible(show: View.() -> Boolean = { true }) {
+ visibility = if (show(this)) {
+ View.VISIBLE
+ } else {
+ View.INVISIBLE
+ }
+}
+
+/**
+ * 点击事件
+ */
+inline fun T.singleClick(time: Long = 800, crossinline block: (T) -> Unit) {
+ setOnClickListener(object : View.OnClickListener {
+ private var lastClickTime: Long = 0L
+ override fun onClick(v: View?) {
+ val currentTimeMillis = SystemClock.elapsedRealtime()
+ if (currentTimeMillis - lastClickTime > time || this is Checkable) {
+ lastClickTime = currentTimeMillis
+ block(this@singleClick)
+ }
+ }
+ })
+}
+
+/**
+ * 点击事件
+ */
+fun T.singleClick(onClickListener: View.OnClickListener, time: Long = 800) {
+ setOnClickListener(object : View.OnClickListener {
+ private var lastClickTime: Long = 0L
+ override fun onClick(v: View?) {
+ val currentTimeMillis = SystemClock.elapsedRealtime()
+ if (currentTimeMillis - lastClickTime > time || this is Checkable) {
+ lastClickTime = currentTimeMillis
+ onClickListener.onClick(v)
+ Log.v("点击","点击执行")
+ } else {
+ Log.v("点击","点击被拦截了")
+ }
+ }
+ })
+}
+
+
+/**
+ * 设置View圆角矩形
+ */
+fun T.roundCorner(corner: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (outlineProvider == null || outlineProvider !is ShapeViewOutlineProvider.Round) {
+ outlineProvider = ShapeViewOutlineProvider.Round(corner.toFloat())
+ } else if (outlineProvider != null && outlineProvider is ShapeViewOutlineProvider.Round) {
+ (outlineProvider as ShapeViewOutlineProvider.Round).corner = corner.toFloat()
+ }
+ clipToOutline = true
+ }
+}
+
+/**
+ * 设置View为圆形
+ */
+fun T.circle() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (outlineProvider == null || outlineProvider !is ShapeViewOutlineProvider.Circle) {
+ outlineProvider = ShapeViewOutlineProvider.Circle()
+ }
+ clipToOutline = true
+ }
+}
+
+fun View.getBitmap(): Bitmap {
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+ canvas.translate(scrollX.toFloat(), scrollY.toFloat())
+ draw(canvas)
+ return bitmap
+}
+
+/**
+ * 设置边距
+ */
+fun View?.setMargin(start: Int? = null, top: Int? = null, end: Int? = null, bottom: Int? = null) {
+ (this?.layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
+ start?.let {
+ this.marginStart = start
+ }
+ top?.let {
+ this.topMargin = top
+ }
+ end?.let {
+ this.marginEnd = end
+ }
+ bottom?.let {
+ this.bottomMargin = bottom
+ }
+ }
+}
+
+
+/**
+ * 设置内边距
+ */
+fun View?.setPadding2(start: Int? = null, top: Int? = null, end: Int? = null, bottom: Int? = null) {
+ if (this == null) return
+ this.setPadding(
+ start ?: paddingStart, top ?: paddingTop, end ?: paddingEnd, bottom ?: paddingBottom
+ )
+}
+
+/**
+ * 描边宽度
+ */
+fun TextView.strokeWidth(width: Float) {
+ this.paint?.style = Paint.Style.FILL_AND_STROKE
+ this.paint?.strokeWidth = width
+ this.invalidate()
+}
+
+/**
+ * 模拟点击并取消
+ */
+fun ScrollingView.simulateClickAndCancel() {
+ val view = this as? View ?: return
+ val downEvent = MotionEvent.obtain(
+ System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_DOWN, (view.right - view.left) / 2f, (view.bottom - view.top) / 2f, 0
+ )
+ view.dispatchTouchEvent(downEvent)
+ val cancelEvent = MotionEvent.obtain(
+ System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_CANCEL, (view.right - view.left) / 2f, (view.bottom - view.top) / 2f, 0
+ )
+ view.dispatchTouchEvent(cancelEvent)
+}
+
+/**
+ * 使用灰色滤镜
+ */
+fun View.applyGrayFilter(isGray: Boolean) {
+ try {
+ val paint = Paint()
+ val colorMatrix = ColorMatrix()
+ colorMatrix.setSaturation(if (isGray) 0f else 1f)
+ paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
+ setLayerType(View.LAYER_TYPE_HARDWARE, paint)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+}
+
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt
new file mode 100644
index 0000000..a2b5400
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt
@@ -0,0 +1,13 @@
+package com.example.lib_utils.log
+
+import android.util.Log
+
+/**
+ * Created by Max on 2023/10/26 10:29
+ * Desc:Android日志
+ */
+class AndroidLogPrinter : LogPrinter {
+ override fun println(level: Int, tag: String, message: String) {
+ Log.println(level, tag, message)
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt
new file mode 100644
index 0000000..d9de4c0
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt
@@ -0,0 +1,61 @@
+package com.example.lib_utils.log
+
+/**
+ * Created by Max on 2023/10/26 10:29
+ * Desc:日志快捷使用接口
+ */
+interface ILog {
+
+ companion object {
+ /**
+ * 清理(退出APP时调用)
+ */
+ fun onCleared() {
+
+ }
+ }
+
+ /**
+ * 默认日志Tag
+ */
+ fun getLogTag(): String {
+ return "LogUtil"
+ }
+
+ fun logI(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
+ LogUtil.i(tag, message, filePrinter)
+ }
+
+ fun logV(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
+ LogUtil.v(tag, message, filePrinter)
+ }
+
+ fun logW(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
+ LogUtil.w(tag, message, filePrinter)
+ }
+
+ fun logD(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
+ LogUtil.d(tag, message, filePrinter)
+ }
+
+ fun logE(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
+ LogUtil.e(tag, message, filePrinter)
+ }
+
+ fun logE(
+ throwable: Throwable,
+ tag: String = getLogTag(),
+ filePrinter: Boolean = false
+ ) {
+ LogUtil.e(tag, throwable, filePrinter)
+ }
+
+ fun logE(
+ message: String,
+ throwable: Throwable,
+ tag: String = getLogTag(),
+ filePrinter: Boolean = false
+ ) {
+ LogUtil.e(tag, message, throwable, filePrinter)
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt
new file mode 100644
index 0000000..73664cb
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt
@@ -0,0 +1,14 @@
+package com.example.lib_utils.log
+
+
+/**
+ * Created by Max on 2023/10/26 10:29
+ * Desc: 日志打印
+ */
+interface LogPrinter {
+ /**
+ * 打印
+ * @param level 级别 [android.util.Log]
+ */
+ fun println(level: Int, tag: String, message: String)
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt
new file mode 100644
index 0000000..931017d
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt
@@ -0,0 +1,106 @@
+package com.example.lib_utils.log
+
+import android.util.Log
+
+/**
+ * Created by Max on 2023/10/26 10:29
+ * Desc:日志工具
+ */
+object LogUtil {
+
+ private var consolePrinter: LogPrinter? = AndroidLogPrinter()
+
+ private var filePrinter: LogPrinter? = null
+
+ // 是否启动控制台打印
+ var consolePrinterEnabled: Boolean = true
+
+ // 是否启动文件打印
+ var filePrinterEnabled: Boolean = true
+
+ /**
+ * 设置文件打印
+ */
+ fun setFilePrinter(filePrinter: LogPrinter) {
+ LogUtil.filePrinter = filePrinter
+ }
+
+ fun e(tag: String, message: String, filePrinter: Boolean = false) {
+ log(Log.ERROR, tag, message, filePrinter)
+ }
+
+ fun e(tag: String, throwable: Throwable, filePrinter: Boolean = false) {
+ val cause = Log.getStackTraceString(throwable)
+ if (cause.isEmpty()) {
+ return
+ }
+ e(tag, cause, filePrinter)
+ }
+
+ fun e(tag: String, message: String?, throwable: Throwable, filePrinter: Boolean = false) {
+ val cause = Log.getStackTraceString(throwable)
+ if (message == null && cause.isEmpty()) {
+ return
+ }
+ e(tag, message + "\t\t" + cause, filePrinter)
+ }
+
+ @JvmStatic
+ fun d(tag: String, message: String, filePrinter: Boolean = false) {
+ log(Log.DEBUG, tag, message, filePrinter)
+ }
+ @JvmStatic
+ fun d(message: String, filePrinter: Boolean = false) {
+ log(Log.DEBUG, "LogUtil", message, filePrinter)
+ }
+
+ fun i(tag: String, message: String, filePrinter: Boolean = false) {
+ log(Log.INFO, tag, message, filePrinter)
+ }
+
+ fun v(tag: String, message: String, filePrinter: Boolean = false) {
+ log(Log.VERBOSE, tag, message, filePrinter)
+ }
+
+ fun w(tag: String, message: String, filePrinter: Boolean = false) {
+ log(Log.WARN, tag, message, filePrinter)
+ }
+
+ /**
+ * 输出日志
+ */
+ fun log(level: Int = Log.INFO, tag: String?, message: String?, filePrinter: Boolean = false) {
+ if (tag.isNullOrEmpty()) {
+ return
+ }
+ if (message.isNullOrEmpty()) {
+ return
+ }
+ // 输出控制台
+ logConsole(level, tag, message)
+ // 输出文件
+ if (filePrinter) {
+ logFile(level, tag, message)
+ }
+ }
+
+ /**
+ * 输出到控制台
+ */
+ fun logConsole(level: Int = Log.INFO, tag: String, message: String) {
+ if (!consolePrinterEnabled) {
+ return
+ }
+ consolePrinter?.println(level, tag, message)
+ }
+
+ /**
+ * 输出到文件
+ */
+ fun logFile(level: Int = Log.INFO, tag: String, message: String) {
+ if (!filePrinterEnabled) {
+ return
+ }
+ filePrinter?.println(level, tag, message)
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java
new file mode 100644
index 0000000..a10f22b
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java
@@ -0,0 +1,156 @@
+package com.example.lib_utils.spannable;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.ReplacementSpan;
+import android.util.TypedValue;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Created by Max on 2023/10/26 20:14
+ **/
+public class IconTextSpan extends ReplacementSpan {
+ private Context mContext;
+ private int mBgColorResId; //Icon背景颜色
+ private String mText; //Icon内文字
+ private float mBgHeight; //Icon背景高度
+ private float mBgWidth; //Icon背景宽度
+ private float mRadius; //Icon圆角半径
+ private float mRightMargin; //右边距
+ private float mTextSize; //文字大小
+ private int mTextColorResId; //文字颜色
+
+ private Paint mBgPaint; //icon背景画笔
+ private Paint mTextPaint; //icon文字画笔
+ private int paddingHorizontal = 0;
+
+ public IconTextSpan(Context context, int bgColorResId, String text, int textColor, int mTextSize, int round, int marginRight, int paddingHorizontal) {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+
+ this.paddingHorizontal = paddingHorizontal;
+ //初始化默认数值
+ initDefaultValue(context, bgColorResId, text, textColor, mTextSize, round, marginRight);
+ //计算背景的宽度
+ this.mBgWidth = caculateBgWidth(text);
+ //初始化画笔
+ initPaint();
+ }
+
+ /**
+ * 初始化画笔
+ */
+ private void initPaint() {
+ //初始化背景画笔
+ mBgPaint = new Paint();
+ mBgPaint.setColor(mBgColorResId);
+ mBgPaint.setStyle(Paint.Style.FILL);
+ mBgPaint.setAntiAlias(true);
+
+ //初始化文字画笔
+ mTextPaint = new TextPaint();
+ mTextPaint.setColor(mTextColorResId);
+ mTextPaint.setTextSize(mTextSize);
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ /**
+ * 初始化默认数值
+ *
+ * @param context 上下文
+ * @param textColor 字体颜色
+ */
+ private void initDefaultValue(Context context, int bgColorResId, String text, int textColor, int textSize, int round, int marginRight) {
+ this.mContext = context.getApplicationContext();
+ this.mBgColorResId = bgColorResId;
+ this.mText = text;
+ this.mBgHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18f, mContext.getResources().getDisplayMetrics());
+ this.mRightMargin = marginRight;
+ this.mRadius = round;
+ this.mTextSize = textSize;
+ this.mTextColorResId = textColor;
+ }
+
+ /**
+ * 计算icon背景宽度
+ *
+ * @param text icon内文字
+ */
+ private float caculateBgWidth(String text) {
+// if (text.length() > 1) {
+ //多字,宽度=文字宽度+padding
+ Rect textRect = new Rect();
+ Paint paint = new Paint();
+ paint.setTextSize(mTextSize);
+ paint.getTextBounds(text, 0, text.length(), textRect);
+ float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingHorizontal, mContext.getResources().getDisplayMetrics());
+ return textRect.width() + padding * 2;
+// } else {
+ //单字,宽高一致为正方形
+// return mBgHeight + paddingHorizontal;
+// }
+ }
+
+ /**
+ * 设置右边距
+ * @param rightMarginDpValue 右边边距
+ */
+ public void setRightMarginDpValue(int rightMarginDpValue) {
+ this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightMarginDpValue, mContext.getResources().getDisplayMetrics());
+ }
+
+ /**
+ * 设置宽度,宽度=背景宽度+右边距
+ */
+ @Override
+ public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+ return (int) (mBgWidth + mRightMargin);
+ }
+
+ /**
+ * draw
+ *
+ * @param text 完整文本
+ * @param start setSpan里设置的start
+ * @param end setSpan里设置的start
+ * @param top 当前span所在行的上方y
+ * @param y y其实就是metric里baseline的位置
+ * @param bottom 当前span所在行的下方y(包含了行间距),会和下一行的top重合
+ * @param paint 使用此span的画笔
+ */
+ @Override
+ public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
+ //画背景
+ Paint bgPaint = new Paint();
+ bgPaint.setColor(mBgColorResId);
+ bgPaint.setStyle(Paint.Style.FILL);
+ bgPaint.setAntiAlias(true);
+ Paint.FontMetrics metrics = paint.getFontMetrics();
+
+ float textHeight = metrics.descent - metrics.ascent;
+ //算出背景开始画的y坐标
+ float bgStartY = y + (textHeight - mBgHeight) / 2 + metrics.ascent;
+
+ //画背景
+ RectF bgRect = new RectF(x, bgStartY, x + mBgWidth , bgStartY + mBgHeight);
+ canvas.drawRoundRect(bgRect, mRadius, mRadius, bgPaint);
+
+ //把字画在背景中间
+ TextPaint textPaint = new TextPaint();
+ textPaint.setColor(mTextColorResId);
+ textPaint.setTextSize(mTextSize);
+ textPaint.setAntiAlias(true);
+ textPaint.setTextAlign(Paint.Align.CENTER); //这个只针对x有效
+ Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
+ float textRectHeight = fontMetrics.bottom - fontMetrics.top;
+ canvas.drawText(mText, x + mBgWidth / 2, bgStartY + (mBgHeight - textRectHeight) / 2 - fontMetrics.top, textPaint);
+ }
+}
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt
new file mode 100644
index 0000000..d1810e8
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt
@@ -0,0 +1,29 @@
+package com.example.lib_utils.spannable
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
+import android.text.style.ReplacementSpan
+
+/**
+ * Created by Max on 2023/10/26 20:14
+ * Desc:文字 圆背景
+ **/
+class RoundBackgroundColorSpan(var textColor: Int, var textSize: Int, var bgColor: Int, var paddingHorizontal: Int, var paddingVertical: Int, var marginHorizontal: Int,var round:Int) : ReplacementSpan() {
+
+ override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
+ return paint.measureText(text, start, end).toInt()+(paddingHorizontal)+marginHorizontal
+ }
+
+ override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
+
+ paint.color = this.textColor
+ paint.textSize = textSize.toFloat()
+ canvas.drawText(text.toString(), start, end, x + paddingHorizontal+marginHorizontal, y.toFloat()-paddingVertical, paint)
+ paint.color = paint.color
+
+ paint.color = this.bgColor;
+ val rectF = RectF(x+marginHorizontal, top.toFloat(), (paint.measureText(text.toString())) , bottom.toFloat())
+ canvas.drawRoundRect(rectF, round.toFloat(), round.toFloat(), paint)
+ }
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt
new file mode 100644
index 0000000..544d7fa
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt
@@ -0,0 +1,517 @@
+package com.example.lib_utils.spannable
+
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.TextPaint
+import android.text.method.LinkMovementMethod
+import android.text.style.*
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import com.example.lib_utils.ktx.dp
+
+/**
+ * Created by Max on 2023/10/26 20:14
+ * Desc:可扩展文本
+ **/
+class SpannableTextBuilder(private val textView: TextView) {
+
+ private val spannableBuilder: SpannableStringBuilder by lazy {
+ SpannableStringBuilder()
+ }
+
+ /**
+ * 添加一段文本
+ */
+ fun appendText(node: TextNode) {
+ val onClick: ((String) -> Unit)? = if (node.getOnClickListener() != null) {
+ {
+ node.getOnClickListener()?.invoke(node)
+ }
+ } else {
+ null
+ }
+ appendText(
+ text = node.getContent(),
+ textColor = node.getTextColor(),
+ textSize = node.getTextSize(),
+ backgroundColor = node.getBackgroundColor(),
+ underline = node.isUnderline(),
+ clickListener = onClick
+ )
+ }
+
+ /**
+ * 添加一段文本
+ * @param text 文本
+ * @param textColor 文本颜色
+ * @param backgroundColor 背景颜色
+ * @param textSize 文本大小
+ * @param textStyle 文本样式
+ * @param underline 是否有下划线
+ * @param clickListener 点击事件
+ */
+ fun appendText(
+ text: String,
+ @ColorInt textColor: Int? = null,
+ @ColorInt backgroundColor: Int? = null,
+ textSize: Int? = null,
+ textStyle: Int? = null,
+ underline: Boolean? = null,
+ clickListener: ((String) -> Unit)? = null
+ ): SpannableTextBuilder {
+ val start = spannableBuilder.length
+ spannableBuilder.append(text)
+ val end = spannableBuilder.length
+ setTextStyle(
+ text,
+ start,
+ end,
+ textColor,
+ backgroundColor,
+ textSize,
+ textStyle,
+ underline,
+ null,
+ clickListener
+ )
+ return this
+ }
+ fun appendText(
+ text: String
+ ): SpannableTextBuilder {
+ val start = spannableBuilder.length
+ spannableBuilder.append(text)
+ val end = spannableBuilder.length
+ setTextStyle(
+ text,
+ start,
+ end,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ )
+ return this
+ }
+
+ fun setTextStyle(
+ text: String,
+ @ColorInt textColor: Int? = null,
+ @ColorInt backgroundColor: Int? = null,
+ textSize: Int? = null,
+ textStyle: Int? = null,
+ underline: Boolean? = null,
+ clickListener: ((String) -> Unit)? = null
+ ): SpannableTextBuilder {
+ if (text.isEmpty()) {
+ return this
+ }
+ val start = spannableBuilder.indexOf(text)
+ if (start == -1) {
+ return this
+ }
+ val end = start + text.length
+ return setTextStyle(
+ text,
+ start,
+ end,
+ textColor,
+ backgroundColor,
+ textSize,
+ textStyle,
+ underline,
+ null,
+ clickListener
+ )
+ }
+
+ fun setTextStyle(
+ text: String,
+ @ColorInt textColor: Int? = null,
+ @ColorInt backgroundColor: Int? = null,
+ textSize: Int? = null,
+ textStyle: Int? = null,
+ underline: Boolean? = null,
+ delLine: Boolean? = null,
+ textStart: Int = 0,
+ clickListener: ((String) -> Unit)? = null
+ ): SpannableTextBuilder {
+
+ if (text.isEmpty()) {
+ return this
+ };
+
+ val start = spannableBuilder.indexOf(text, textStart)
+ if (start == -1) {
+ return this
+ }
+
+ val end = start + text.length
+ return setTextStyle(
+ text,
+ start,
+ end,
+ textColor,
+ backgroundColor,
+ textSize,
+ textStyle,
+ underline,
+ delLine,
+ clickListener
+ )
+ }
+
+ /**
+ * 添加图片
+ * @param drawable 图片
+ * @param clickListener 点击事件
+ */
+ fun appendDrawable(
+ @DrawableRes drawable: Int,
+ clickListener: ((Int) -> Unit)?
+ ): SpannableTextBuilder {
+ // 需要时再完善
+ val start = spannableBuilder.length
+ spannableBuilder.append("[icon}")
+ val end = spannableBuilder.length
+
+ // 图片
+ val imageSpan: ImageSpan = VerticalImageSpan(textView.context, drawable)
+ spannableBuilder.setSpan(imageSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+
+ // 点击事件
+ if (clickListener != null) {
+ textView.movementMethod = LinkMovementMethod.getInstance()
+ val clickableSpan = DrawableClickableSpan(clickListener, drawable)
+ spannableBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ return this
+ }
+
+ /**
+ * 添加有背景圆角的文字
+ * @param text 文本
+ * @param textColor 文本颜色
+ * @param backgroundColor 背景颜色
+ * @param paddingHorizontal 内横向边距
+ * @param paddingVertical 内竖向边距
+ * @param marginHorizontal 外横向边距
+ */
+ fun appendTextRoundBackground(
+ text: String,
+ @ColorInt textColor: Int,
+ textSize: Int,
+ @ColorInt backgroundColor: Int,
+ paddingHorizontal: Int,
+ paddingVertical: Int,
+ marginHorizontal: Int,
+ round: Int
+ ): SpannableTextBuilder {
+ val start = spannableBuilder.length
+ spannableBuilder.append(text)
+ val end = spannableBuilder.length
+ spannableBuilder.setSpan(
+ RoundBackgroundColorSpan(
+ textColor,
+ textSize,
+ backgroundColor,
+ paddingHorizontal,
+ paddingVertical,
+ marginHorizontal,
+ round
+ ), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ return this
+ }
+
+ /**
+ * 添加有背景圆角的文字
+ * @param text 文本
+ * @param textColor 文本颜色
+ * @param backgroundColor 背景颜色
+ * @param paddingHorizontal 内横向边距
+ * @param paddingVertical 内竖向边距
+ * @param marginHorizontal 外横向边距
+ */
+ fun appendIconTextRoundBackground(
+ text: String,
+ @ColorInt textColor: Int,
+ textSize: Int,
+ @ColorInt backgroundColor: Int,
+ marginRight: Int,
+ round: Int
+ ): SpannableTextBuilder {
+ val start = spannableBuilder.length
+ spannableBuilder.append(text)
+ val end = spannableBuilder.length
+ spannableBuilder.setSpan(
+ com.example.lib_utils.spannable.IconTextSpan(
+ textView.context,
+ backgroundColor,
+ text,
+ textColor,
+ textSize,
+ round,
+ marginRight,
+ 2.dp
+ ),
+ start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ return this
+ }
+
+ private fun setTextStyle(
+ text: String,
+ start: Int,
+ end: Int,
+ @ColorInt textColor: Int? = null,
+ @ColorInt backgroundColor: Int? = null,
+ textSize: Int? = null,
+ textStyle: Int? = null,
+ underline: Boolean? = null,
+ delLine: Boolean? = null,
+ clickListener: ((String) -> Unit)? = null
+ ): SpannableTextBuilder {
+ if (start < 0 || end > spannableBuilder.length) {
+ return this
+ }
+ // 文本颜色
+ if (textColor != null) {
+ spannableBuilder.setSpan(
+ ForegroundColorSpan(textColor),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ // 文本背景颜色
+ if (backgroundColor != null) {
+ spannableBuilder.setSpan(
+ BackgroundColorSpan(backgroundColor),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ // 文本大小
+ if (textSize != null) {
+ spannableBuilder.setSpan(
+ AbsoluteSizeSpan(textSize, true),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ // 文本样式
+ if (textStyle != null) {
+ spannableBuilder.setSpan(
+ StyleSpan(textStyle),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ // 下划线
+ if (underline == true) {
+ spannableBuilder.setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+
+ // 删除线
+ if (delLine == true) {
+ spannableBuilder.setSpan(
+ StrikethroughSpan(),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ // 点击事件
+ if (clickListener != null) {
+ // 设置highlightColor=Color.TRANSPARENT,可以解决点击时的高亮色问题,但光标的区域选中也是透明的,貌似对用户体验不太好
+// textView.highlightColor = Color.TRANSPARENT
+ textView.movementMethod = LinkMovementMethod.getInstance()
+ val clickableSpan = TextClickableSpan(
+ clickListener, text, textColor
+ ?: textView.currentTextColor, underline ?: false
+ )
+ spannableBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ return this
+ }
+
+
+ fun build(): SpannableStringBuilder {
+ return spannableBuilder
+ }
+
+ /**
+ * 应用
+ */
+ fun apply() {
+ textView.text = spannableBuilder
+ }
+
+ /**
+ * 文本点击
+ */
+ class TextClickableSpan(
+ private val clickListener: ((String) -> Unit)? = null,
+ private val text: String,
+ private val textColor: Int,
+ private val underline: Boolean
+ ) : ClickableSpan() {
+ override fun onClick(widget: View) {
+ clickListener?.invoke(text)
+ }
+
+ override fun updateDrawState(ds: TextPaint) {
+ ds.color = textColor
+ ds.isUnderlineText = underline
+ }
+ }
+
+
+ /**
+ * 图片点击
+ */
+ class DrawableClickableSpan(
+ private val clickListener: ((Int) -> Unit)? = null,
+ private val drawable: Int
+ ) : ClickableSpan() {
+ override fun onClick(widget: View) {
+ clickListener?.invoke(drawable)
+ }
+ }
+
+
+ interface TextNode {
+ /**
+ * 内容
+ */
+ fun getContent(): String
+
+ /**
+ * 文本颜色
+ */
+ fun getTextSize(): Int? {
+ return null
+ }
+
+ /**
+ * 文本颜色
+ */
+ fun getTextColor(): Int? {
+ return null
+ }
+
+ /**
+ * 文本样式
+ */
+ fun getTextStyle(): Int? {
+ return null
+ }
+
+ /**
+ * 背景颜色
+ */
+ fun getBackgroundColor(): Int? {
+ return null
+ }
+
+ /**
+ * 是否有下划线
+ */
+ fun isUnderline(): Boolean {
+ return false
+ }
+
+ /**
+ * 获取点击事件
+ */
+ fun getOnClickListener(): ((TextNode) -> Unit)? {
+ return null
+ }
+ }
+
+ public class TextStyleBean {
+ var text: String = ""
+
+ @ColorInt
+ var textColor: Int? = null
+
+ @ColorInt
+ var backgroundColor: Int? = null
+
+ var textSize: Int? = null
+ var textStyle: Int? = null
+ var underline: Boolean? = null
+ var delLine: Boolean? = null
+ var textStart: Int = 0
+ var clickListener: ((String) -> Unit)? = null
+ }
+
+ //按添加顺序 匹配,上一个匹配的结束索引位置,是下一个的起始位置
+ fun addTextStyleList(list: List) : SpannableTextBuilder{
+ var start = 0;
+ list.forEach {
+ val indexStart = start
+ val findIndex = spannableBuilder.toString().indexOf(it.text, indexStart)
+ val end = findIndex + it.text.length
+ start = end
+ setTextStyle(
+ it.text,
+ findIndex,
+ end,
+ it.textColor,
+ it.backgroundColor,
+ it.textSize,
+ it.textStyle,
+ it.underline,
+ it.delLine,
+ it.clickListener
+ )
+ }
+ return this
+ }
+
+ //全局匹配
+ fun addTextStyleListAll(list: List) : SpannableTextBuilder{
+ list.forEach {
+ val indexStart = 0
+ val findIndex = spannableBuilder.toString().indexOf(it.text, indexStart)
+ val end = findIndex + it.text.length
+ setTextStyle(
+ it.text,
+ findIndex,
+ end,
+ it.textColor,
+ it.backgroundColor,
+ it.textSize,
+ it.textStyle,
+ it.underline,
+ it.delLine,
+ it.clickListener
+ )
+ }
+ return this
+ }
+}
+
+
+/**
+ * 快速构建生成器
+ */
+fun TextView.spannableBuilder(): SpannableTextBuilder {
+ return SpannableTextBuilder(this)
+}
\ No newline at end of file
diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt
new file mode 100644
index 0000000..515d651
--- /dev/null
+++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt
@@ -0,0 +1,66 @@
+package com.example.lib_utils.spannable
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.drawable.Drawable
+import android.text.style.ImageSpan
+
+/**
+ * Created by Max on 2023/10/26 20:14
+ * Desc:垂直居中的ImageSpan
+ **/
+class VerticalImageSpan : ImageSpan {
+ constructor(drawable: Drawable) : super(drawable)
+ constructor(context: Context, resourceId: Int) : super(context, resourceId)
+
+ /**
+ * update the text line height
+ */
+ override fun getSize(
+ paint: Paint, text: CharSequence?, start: Int, end: Int,
+ fontMetricsInt: Paint.FontMetricsInt?
+ ): Int {
+ val drawable = drawable
+ val rect = drawable.bounds
+ if (fontMetricsInt != null) {
+ val fmPaint = paint.fontMetricsInt
+ val fontHeight = fmPaint.descent - fmPaint.ascent
+ val drHeight = rect.bottom - rect.top
+ val centerY = fmPaint.ascent + fontHeight / 2
+ fontMetricsInt.ascent = centerY - drHeight / 2
+ fontMetricsInt.top = fontMetricsInt.ascent
+ fontMetricsInt.bottom = centerY + drHeight / 2
+ fontMetricsInt.descent = fontMetricsInt.bottom
+ }
+ return rect.right
+ }
+
+ /**
+ * see detail message in android.text.TextLine
+ *
+ * @param canvas the canvas, can be null if not rendering
+ * @param text the text to be draw
+ * @param start the text start position
+ * @param end the text end position
+ * @param x the edge of the replacement closest to the leading margin
+ * @param top the top of the line
+ * @param y the baseline
+ * @param bottom the bottom of the line
+ * @param paint the work paint
+ */
+ override fun draw(
+ canvas: Canvas, text: CharSequence, start: Int, end: Int,
+ x: Float, top: Int, y: Int, bottom: Int, paint: Paint
+ ) {
+ val drawable = drawable
+ canvas.save()
+ val fmPaint = paint.fontMetricsInt
+ val fontHeight = fmPaint.descent - fmPaint.ascent
+ val centerY = y + fmPaint.descent - fontHeight / 2
+ val transY = centerY - (drawable.bounds.bottom - drawable.bounds.top) / 2
+ canvas.translate(x, transY.toFloat())
+ drawable.draw(canvas)
+ canvas.restore()
+ }
+}
\ No newline at end of file