From 60b8b5380782381a83adb59bdedf54a24030fe3e Mon Sep 17 00:00:00 2001 From: razerdp Date: Tue, 30 Jul 2019 23:25:13 +0800 Subject: [PATCH] =?UTF-8?q?//=20=E7=AC=AC=E4=B8=89=E6=AC=A1=E9=87=8D?= =?UTF-8?q?=E6=9E=84(first=20commit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + README_REFACTOR.md | 18 + app/build.gradle | 51 +- app/src/main/AndroidManifest.xml | 10 - build.gradle | 73 ++- circle_base_library/build.gradle | 39 +- .../src/main/res/values/strings.xml | 33 - common/.gitignore | 1 + common/build.gradle | 8 + common/proguard-rules.pro | 21 + .../common/ExampleInstrumentedTest.java | 26 + common/src/main/AndroidManifest.xml | 17 + .../github/common/base/BaseAppActivity.java | 17 + .../github/common/entity/ImageInfo.java | 107 ++++ .../manager/localphoto/LPException.java | 16 + .../manager/localphoto/LocalPhotoManager.java | 459 ++++++++++++++ .../modules/base/BaseModuleApplication.java | 9 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4920 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2450 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 8277 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 16315 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 24682 bytes common/src/main/res/values/colors.xml | 4 + common/src/main/res/values/strings.xml | 3 + common/src/main/res/values/styles.xml | 52 ++ .../github/common/ExampleUnitTest.java | 17 + config.gradle | 35 +- gradle.properties | 4 - lib/.gitignore | 1 + lib/build.gradle | 11 + lib/proguard-rules.pro | 21 + .../github/lib/ExampleInstrumentedTest.java | 26 + lib/src/main/AndroidManifest.xml | 2 + .../razerdp/github/lib/api/AppContext.java | 130 ++++ .../github/lib/base/BaseLibActivity.java | 153 +++++ .../github/lib/base/BaseLibFragment.java | 140 +++++ .../github/lib/helper/AppFileHelper.java | 192 ++++++ .../github/lib/helper/PermissionHelper.java | 333 ++++++++++ .../lib/helper/PermissionHelperCompat.java | 46 ++ .../lib/helper/SharePreferenceHelper.java | 120 ++++ .../lib/interfaces/ClearMemoryObject.java | 12 + .../lib/interfaces/ExtSimpleCallback.java | 17 + .../github/lib/interfaces/IPermission.java | 11 + .../lib/interfaces/MultiClickListener.java | 44 ++ .../interfaces/OnPermissionGrantListener.java | 25 + .../github/lib/interfaces/SimpleCallback.java | 8 + .../lib/interfaces/SingleClickListener.java | 33 + .../adapter/TextWatcherAdapter.java | 25 + .../lib/manager/KeyboardControlMnanager.java | 79 +++ .../github/lib/manager/ThreadPoolManager.java | 69 +++ .../compress/BaseCompressTaskHelper.java | 88 +++ .../lib/manager/compress/CompressManager.java | 92 +++ .../lib/manager/compress/CompressOption.java | 112 ++++ .../lib/manager/compress/CompressResult.java | 97 +++ .../manager/compress/CompressTaskHelper.java | 146 +++++ .../manager/compress/CompressTaskQueue.java | 93 +++ .../manager/compress/OnCompressListener.java | 34 + .../razerdp/github/lib/utils/BitmapUtil.java | 225 +++++++ .../github/lib/utils/EncodingUtils.java | 116 ++++ .../razerdp/github/lib/utils/EncryUtil.java | 76 +++ .../razerdp/github/lib/utils/FileUtil.java | 586 ++++++++++++++++++ .../razerdp/github/lib/utils/GsonUtil.java | 196 ++++++ .../github/lib/utils/ImageSelectUtil.java | 173 ++++++ .../com/razerdp/github/lib/utils/KLog.java | 48 ++ .../github/lib/utils/KeyBoardUtil.java | 77 +++ .../com/razerdp/github/lib/utils/OSUtils.java | 121 ++++ .../github/lib/utils/SimpleObjectPool.java | 49 ++ .../razerdp/github/lib/utils/StringUtil.java | 55 ++ .../razerdp/github/lib/utils/TimeUtil.java | 337 ++++++++++ .../razerdp/github/lib/utils/ToolUtil.java | 76 +++ .../razerdp/github/lib/utils/VersionUtil.java | 41 ++ .../razerdp/github/lib/utils/WeakHandler.java | 503 +++++++++++++++ lib/src/main/res/values/colors.xml | 4 + lib/src/main/res/values/strings.xml | 35 ++ .../razerdp/github/lib/ExampleUnitTest.java | 17 + module_main/.gitignore | 1 + module_main/build.gradle | 6 + module_main/proguard-rules.pro | 21 + .../module/main/ExampleInstrumentedTest.java | 26 + module_main/src/main/AndroidManifest.xml | 2 + module_main/src/main/res/values/strings.xml | 3 + .../github/module/main/ExampleUnitTest.java | 17 + module_main_impl/.gitignore | 1 + module_main_impl/build.gradle | 6 + module_main_impl/proguard-rules.pro | 21 + .../impl/main/ExampleInstrumentedTest.java | 26 + module_main_impl/src/main/AndroidManifest.xml | 24 + .../module/impl/main/MainApplication.java | 10 + .../module/impl/main/ui/MainActivity.java | 16 + .../src/main/res/layout/activity_main.xml | 16 + .../src/main/res/values/strings.xml | 3 + .../module/impl/main/ExampleUnitTest.java | 17 + network/.gitignore | 1 + network/build.gradle | 5 + network/proguard-rules.pro | 21 + .../network/ExampleInstrumentedTest.java | 26 + network/src/main/AndroidManifest.xml | 2 + network/src/main/res/values/strings.xml | 3 + .../github/network/ExampleUnitTest.java | 17 + router/.gitignore | 1 + router/build.gradle | 6 + router/proguard-rules.pro | 21 + .../router/ExampleInstrumentedTest.java | 26 + router/src/main/AndroidManifest.xml | 2 + router/src/main/res/values/strings.xml | 3 + .../github/router/ExampleUnitTest.java | 17 + settings.gradle | 12 +- uilib/.gitignore | 1 + uilib/build.gradle | 8 + uilib/proguard-rules.pro | 21 + .../github/uilib/ExampleInstrumentedTest.java | 26 + uilib/src/main/AndroidManifest.xml | 2 + uilib/src/main/res/values/colors.xml | 9 + uilib/src/main/res/values/strings.xml | 3 + .../razerdp/github/uilib/ExampleUnitTest.java | 17 + 115 files changed, 6212 insertions(+), 172 deletions(-) create mode 100644 README_REFACTOR.md create mode 100644 common/.gitignore create mode 100644 common/build.gradle create mode 100644 common/proguard-rules.pro create mode 100644 common/src/androidTest/java/com/razerdp/github/common/ExampleInstrumentedTest.java create mode 100644 common/src/main/AndroidManifest.xml create mode 100644 common/src/main/java/com/razerdp/github/common/base/BaseAppActivity.java create mode 100644 common/src/main/java/com/razerdp/github/common/entity/ImageInfo.java create mode 100644 common/src/main/java/com/razerdp/github/common/manager/localphoto/LPException.java create mode 100644 common/src/main/java/com/razerdp/github/common/manager/localphoto/LocalPhotoManager.java create mode 100644 common/src/main/java/com/razerdp/github/common/modules/base/BaseModuleApplication.java create mode 100644 common/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 common/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 common/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 common/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 common/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 common/src/main/res/values/colors.xml create mode 100644 common/src/main/res/values/strings.xml create mode 100644 common/src/main/res/values/styles.xml create mode 100644 common/src/test/java/com/razerdp/github/common/ExampleUnitTest.java create mode 100644 lib/.gitignore create mode 100644 lib/build.gradle create mode 100644 lib/proguard-rules.pro create mode 100644 lib/src/androidTest/java/com/razerdp/github/lib/ExampleInstrumentedTest.java create mode 100644 lib/src/main/AndroidManifest.xml create mode 100644 lib/src/main/java/com/razerdp/github/lib/api/AppContext.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/base/BaseLibActivity.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/base/BaseLibFragment.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/helper/AppFileHelper.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelper.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelperCompat.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/helper/SharePreferenceHelper.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/ClearMemoryObject.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/ExtSimpleCallback.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/IPermission.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/MultiClickListener.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/OnPermissionGrantListener.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/SimpleCallback.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/SingleClickListener.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/interfaces/adapter/TextWatcherAdapter.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/KeyboardControlMnanager.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/ThreadPoolManager.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/BaseCompressTaskHelper.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressManager.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressOption.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressResult.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskHelper.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskQueue.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/manager/compress/OnCompressListener.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/BitmapUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/EncodingUtils.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/EncryUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/FileUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/GsonUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/ImageSelectUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/KLog.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/KeyBoardUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/OSUtils.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/SimpleObjectPool.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/StringUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/TimeUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/ToolUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/VersionUtil.java create mode 100644 lib/src/main/java/com/razerdp/github/lib/utils/WeakHandler.java create mode 100644 lib/src/main/res/values/colors.xml create mode 100644 lib/src/main/res/values/strings.xml create mode 100644 lib/src/test/java/com/razerdp/github/lib/ExampleUnitTest.java create mode 100644 module_main/.gitignore create mode 100644 module_main/build.gradle create mode 100644 module_main/proguard-rules.pro create mode 100644 module_main/src/androidTest/java/com/razerdp/github/module/main/ExampleInstrumentedTest.java create mode 100644 module_main/src/main/AndroidManifest.xml create mode 100644 module_main/src/main/res/values/strings.xml create mode 100644 module_main/src/test/java/com/razerdp/github/module/main/ExampleUnitTest.java create mode 100644 module_main_impl/.gitignore create mode 100644 module_main_impl/build.gradle create mode 100644 module_main_impl/proguard-rules.pro create mode 100644 module_main_impl/src/androidTest/java/com/razerdp/module/impl/main/ExampleInstrumentedTest.java create mode 100644 module_main_impl/src/main/AndroidManifest.xml create mode 100644 module_main_impl/src/main/java/com/razerdp/module/impl/main/MainApplication.java create mode 100644 module_main_impl/src/main/java/com/razerdp/module/impl/main/ui/MainActivity.java create mode 100644 module_main_impl/src/main/res/layout/activity_main.xml create mode 100644 module_main_impl/src/main/res/values/strings.xml create mode 100644 module_main_impl/src/test/java/com/razerdp/module/impl/main/ExampleUnitTest.java create mode 100644 network/.gitignore create mode 100644 network/build.gradle create mode 100644 network/proguard-rules.pro create mode 100644 network/src/androidTest/java/com/razerdp/github/network/ExampleInstrumentedTest.java create mode 100644 network/src/main/AndroidManifest.xml create mode 100644 network/src/main/res/values/strings.xml create mode 100644 network/src/test/java/com/razerdp/github/network/ExampleUnitTest.java create mode 100644 router/.gitignore create mode 100644 router/build.gradle create mode 100644 router/proguard-rules.pro create mode 100644 router/src/androidTest/java/com/razerdp/github/router/ExampleInstrumentedTest.java create mode 100644 router/src/main/AndroidManifest.xml create mode 100644 router/src/main/res/values/strings.xml create mode 100644 router/src/test/java/com/razerdp/github/router/ExampleUnitTest.java create mode 100644 uilib/.gitignore create mode 100644 uilib/build.gradle create mode 100644 uilib/proguard-rules.pro create mode 100644 uilib/src/androidTest/java/com/razerdp/github/uilib/ExampleInstrumentedTest.java create mode 100644 uilib/src/main/AndroidManifest.xml create mode 100644 uilib/src/main/res/values/colors.xml create mode 100644 uilib/src/main/res/values/strings.xml create mode 100644 uilib/src/test/java/com/razerdp/github/uilib/ExampleUnitTest.java diff --git a/README.md b/README.md index cb2c3ba..c0b39ef 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ FriendCircle 【简略更新日志】 --- +* 2019/07/30 + * 第三次初步重构。 + * 2019/07/23 * 准备开始第三轮重构 diff --git a/README_REFACTOR.md b/README_REFACTOR.md new file mode 100644 index 0000000..e2ea653 --- /dev/null +++ b/README_REFACTOR.md @@ -0,0 +1,18 @@ +朋友圈工程第三次重构记录 + +### 2019/07/30 + +第三次组件化大致结构: + +基类lib +↑ +ui类库uilib +↑ +通用类common + network +↑ +路由router +↑ +各个组件 +↑ +组件私有application + diff --git a/app/build.gradle b/app/build.gradle index e4e0912..5a0834a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,62 +5,13 @@ android { defaultConfig { applicationId "razerdp.friendcircle" - minSdkVersion rootProject.ext.android.minSdkVersion - targetSdkVersion rootProject.ext.android.targetSdkVersion - versionCode rootProject.ext.android.versionCode - versionName rootProject.ext.android.versionName - renderscriptTargetApi 27 + renderscriptTargetApi 18 renderscriptSupportModeEnabled true - - if (isModule.toBoolean()) { - buildConfigField 'boolean', 'isModule', 'true' - } else { - buildConfigField 'boolean', 'isModule', 'false' - } - - javaCompileOptions { - annotationProcessorOptions { - arguments = [AROUTER_MODULE_NAME: project.getName()] - } - } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - //jni - sourceSets { - main { - jniLibs.srcDirs = ['libs'] - jni.srcDirs = [] //disable automatic ndk-build call - } - - } - - lintOptions { - abortOnError false } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') testImplementation 'junit:junit:4.12' - //图片选择模块尚未完成,所以暂时不需要分开依赖 - if (!isModule.toBoolean()) { - implementation project(':circle_base_library') - implementation project(':circle_base_ui') - implementation project(':circle_photoselect') - implementation project(':circle_publish') - annotationProcessor rootProject.ext.dependencies.arouter_compiler - - } else { - implementation project(':circle_base_library') - implementation project(':circle_base_ui') - implementation project(':circle_common') - } - } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3fb7f6..2c78f37 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,16 +3,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - - - - - - - - - - + afterEvaluate { + //配置android通用属性 + if (project.hasProperty("android")) { + android { + compileSdkVersion rootProject.ext.android.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.android.minSdkVersion + targetSdkVersion rootProject.ext.android.targetSdkVersion + versionCode rootProject.ext.android.versionCode + versionName rootProject.ext.android.versionName + } + + lintOptions { + abortOnError false + } + } + } + + //对library进行配置 + if (project.plugins.hasPlugin('com.android.library')) { + android { + buildTypes { + debug { + minifyEnabled false + consumerProguardFiles 'proguard-rules.pro' + } + release { + minifyEnabled true + consumerProguardFiles 'proguard-rules.pro' + } + } + } + } else if (project.plugins.hasPlugin('com.android.application')) { + //对application工程配置混淆 + android { + buildTypes { + debug { + minifyEnabled false + shrinkResources false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + shrinkResources true + zipAlignEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + jni.srcDirs = [] //disable automatic ndk-build call + } + } + + dexOptions { + javaMaxHeapSize '4g' + jumboMode true + } + } + } + + } +} diff --git a/circle_base_library/build.gradle b/circle_base_library/build.gradle index 6a9d9be..51f4d80 100644 --- a/circle_base_library/build.gradle +++ b/circle_base_library/build.gradle @@ -1,35 +1,5 @@ apply plugin: 'com.android.library' -android { - compileSdkVersion rootProject.ext.android.compileSdkVersion - - defaultConfig { - minSdkVersion rootProject.ext.android.minSdkVersion - targetSdkVersion rootProject.ext.android.targetSdkVersion - versionCode rootProject.ext.baselib.versionCode - versionName rootProject.ext.baselib.versionName - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - javaCompileOptions { - annotationProcessorOptions { - arguments = [AROUTER_MODULE_NAME: project.getName()] - } - } - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - debug { - resValue("string", "PORT_NUMBER", "8083") - } - } - lintOptions { - abortOnError false - } -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { @@ -38,10 +8,6 @@ dependencies { testImplementation 'junit:junit:4.12' - api rootProject.ext.dependencies.appcompat_v7 - api rootProject.ext.dependencies.design - api rootProject.ext.dependencies.support_util - api 'com.github.razerdp:BasePopup:2.1.9' //over scroller helper api 'me.everything:overscroll-decor-android:1.0.4' @@ -69,10 +35,7 @@ dependencies { exclude group: 'glide-parent' exclude group: 'com.squareup.okio' } - - //ARouter - api rootProject.ext.dependencies.arouter_api - annotationProcessor rootProject.ext.dependencies.arouter_compiler //朋友圈九宫格控件 api 'com.github.razerdp:PhotoContents:1.4.5' + api 'org.apmem.tools:layouts:1.10@aar' } diff --git a/circle_base_library/src/main/res/values/strings.xml b/circle_base_library/src/main/res/values/strings.xml index 82181a4..8542005 100644 --- a/circle_base_library/src/main/res/values/strings.xml +++ b/circle_base_library/src/main/res/values/strings.xml @@ -1,35 +1,2 @@ - BaseLibrary - - - %s年前 - %s个月前 - %s天前 - %s小时前 - %s分钟前 - 刚刚 - - - - - @string/permission_recode_audio_hint - @string/permission_get_accounts_hint - @string/permission_read_phone_hint - @string/permission_call_phone_hint - @string/permission_camera_hint - @string/permission_access_fine_location_hint - @string/permission_access_coarse_location_hint - @string/permission_read_external_hint - @string/permission_white_external_hint - - - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_GET_ACCOUNTS】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_READ_PHONE_STATE】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_CALL_PHONE】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_CAMERA】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_ACCESS_FINE_LOCATION】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_ACCESS_COARSE_LOCATION】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_READ_EXTERNAL_STORAGE】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_WRITE_EXTERNAL_STORAGE】 - 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_RECORD_AUDIO】 diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..d53c895 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'com.android.library' + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + api project(':uilib') +} diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/common/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 diff --git a/common/src/androidTest/java/com/razerdp/github/common/ExampleInstrumentedTest.java b/common/src/androidTest/java/com/razerdp/github/common/ExampleInstrumentedTest.java new file mode 100644 index 0000000..ae5efb4 --- /dev/null +++ b/common/src/androidTest/java/com/razerdp/github/common/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.github.common; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.github.common.test", appContext.getPackageName()); + } +} diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..da795af --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/common/src/main/java/com/razerdp/github/common/base/BaseAppActivity.java b/common/src/main/java/com/razerdp/github/common/base/BaseAppActivity.java new file mode 100644 index 0000000..074d8a1 --- /dev/null +++ b/common/src/main/java/com/razerdp/github/common/base/BaseAppActivity.java @@ -0,0 +1,17 @@ +package com.razerdp.github.common.base; + +import android.content.Intent; + +import com.razerdp.github.lib.base.BaseLibActivity; + +/** + * Created by 大灯泡 on 2019/7/30. + * + * app用的activity + */ +public class BaseAppActivity extends BaseLibActivity { + @Override + public void onHandleIntent(Intent intent) { + + } +} diff --git a/common/src/main/java/com/razerdp/github/common/entity/ImageInfo.java b/common/src/main/java/com/razerdp/github/common/entity/ImageInfo.java new file mode 100644 index 0000000..052dab8 --- /dev/null +++ b/common/src/main/java/com/razerdp/github/common/entity/ImageInfo.java @@ -0,0 +1,107 @@ +package com.razerdp.github.common.entity; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import com.razerdp.github.lib.utils.StringUtil; + +import java.io.Serializable; + +import androidx.annotation.NonNull; + + +/** + * Created by 大灯泡 on 2017/10/12. + */ +public class ImageInfo implements Serializable, Parcelable, Cloneable, Comparable { + public final String imagePath; + public final String thumbnailPath; + public final String albumName; + public final long time; + public final int orientation; + + public ImageInfo(String imagePath, String thumbnailPath, String albumName, long time, int orientation) { + this.imagePath = imagePath; + this.thumbnailPath = thumbnailPath; + this.albumName = albumName; + this.time = time; + this.orientation = orientation; + } + + protected ImageInfo(Parcel in) { + imagePath = in.readString(); + thumbnailPath = in.readString(); + albumName = in.readString(); + time = in.readLong(); + orientation = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(imagePath); + dest.writeString(thumbnailPath); + dest.writeString(albumName); + dest.writeLong(time); + dest.writeInt(orientation); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ImageInfo createFromParcel(Parcel in) { + return new ImageInfo(in); + } + + @Override + public ImageInfo[] newArray(int size) { + return new ImageInfo[size]; + } + }; + + public boolean checkValided() { + return StringUtil.noEmpty(imagePath) || StringUtil.noEmpty(thumbnailPath); + } + + @Override + public ImageInfo clone() throws CloneNotSupportedException { + //因为这里只有一些基础数据,所以就浅复制足够了 + return (ImageInfo) super.clone(); + } + + @Override + public int compareTo(@NonNull ImageInfo o) { + if (o == null) return -1; + if (TextUtils.isEmpty(o.getImagePath())) return -1; + if (TextUtils.equals(o.getImagePath(), getImagePath())) return 0; + return -1; + } + + //深复制,暂时不需要,另外利用流的方法的话,类需要实现Serializable接口 + /* public Object cloneDeepInternal() throws IOException, ClassNotFoundException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(this); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + return ois.readObject(); + }*/ + + @Override + public String toString() { + return "ImageInfo{" + + "imagePath='" + imagePath + '\'' + + ", thumbnailPath='" + thumbnailPath + '\'' + + ", albumName='" + albumName + '\'' + + ", time=" + time + + ", orientation=" + orientation + + '}'; + } + + public String getImagePath() { + return TextUtils.isEmpty(imagePath) ? thumbnailPath : imagePath; + } +} diff --git a/common/src/main/java/com/razerdp/github/common/manager/localphoto/LPException.java b/common/src/main/java/com/razerdp/github/common/manager/localphoto/LPException.java new file mode 100644 index 0000000..3561552 --- /dev/null +++ b/common/src/main/java/com/razerdp/github/common/manager/localphoto/LPException.java @@ -0,0 +1,16 @@ +package com.razerdp.github.common.manager.localphoto; + +/** + * Created by 大灯泡 on 2017/3/23. + */ + +public class LPException extends Exception { + + public LPException(String r) { + this(r, null); + } + + public LPException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/common/src/main/java/com/razerdp/github/common/manager/localphoto/LocalPhotoManager.java b/common/src/main/java/com/razerdp/github/common/manager/localphoto/LocalPhotoManager.java new file mode 100644 index 0000000..c842f11 --- /dev/null +++ b/common/src/main/java/com/razerdp/github/common/manager/localphoto/LocalPhotoManager.java @@ -0,0 +1,459 @@ +package com.razerdp.github.common.manager.localphoto; + +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.provider.MediaStore; + +import com.google.gson.reflect.TypeToken; +import com.razerdp.github.common.entity.ImageInfo; +import com.razerdp.github.lib.api.AppContext; +import com.razerdp.github.lib.helper.AppFileHelper; +import com.razerdp.github.lib.helper.SharePreferenceHelper; +import com.razerdp.github.lib.manager.ThreadPoolManager; +import com.razerdp.github.lib.utils.FileUtil; +import com.razerdp.github.lib.utils.GsonUtil; +import com.razerdp.github.lib.utils.KLog; +import com.razerdp.github.lib.utils.TimeUtil; +import com.razerdp.github.lib.utils.WeakHandler; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import androidx.annotation.Nullable; + + +/** + * Created by 大灯泡 on 2017/3/23. + *

+ * 本地照片管理类(单例) + */ + +public enum LocalPhotoManager { + INSTANCE; + private static final String TAG = "LocalPhotoManager"; + public static final String LOCAL_FILE_NAME = "LocalPhotoFile"; + private static final String ALL_PHOTO_TITLE = "所有照片"; + private static final String QUERY_ORDER = " ASC"; + private static final boolean SCAN_EXTERNAL_SD = true; + //1分钟内不再扫描 + private static final long SCAN_INTERVAL = 60 * 1000; + + private WeakHandler handler = new WeakHandler(); + + boolean isScaning; + long lastScanTime; + + + //访问系统数据库的大图查询数据 + public static final String[] STORE_IMGS = {MediaStore.Images.ImageColumns._ID, + MediaStore.Images.ImageColumns.DATA, + MediaStore.Images.ImageColumns.ORIENTATION, + MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME, + MediaStore.Images.ImageColumns.DATE_TAKEN}; + //访问系统数据库的小图查询数据 + public static final String[] THUMBNAIL_STORE_IMAGE = {MediaStore.Images.Thumbnails._ID, MediaStore.Images.Thumbnails.DATA}; + + private static final LinkedHashMap> sALBUM = new LinkedHashMap<>(); + private static final LinkedHashMap> sWRITE_ALBUM = new LinkedHashMap<>(); + + private final ProgressRunnable progressRunnable = new ProgressRunnable(); + + public synchronized void scanImg(@Nullable final OnScanListener listener) { + ThreadPoolManager.execute(new Runnable() { + @Override + public void run() { + scanImgInternal(listener); + } + }); + } + + private synchronized void scanImgInternal(@Nullable OnScanListener listener) { + if (isScaning) { + callError(listener, "scan task is running", new LPException("scan task is running")); + return; + } + isScaning = true; + callStart(listener); + lastScanTime = SharePreferenceHelper.loadLongPreferenceByKey(SharePreferenceHelper.APP_LAST_SCAN_IMG_TIME, 0); + + boolean callImmediately = checkLocalSerializableFile(); + //如果本地文件已经有了,那么可以立即回调,提高用户体验。 + //然后再后台扫一次更新本地文件记录 + if (callImmediately) { + callProgress(listener, 100); + callFinish(listener); + reset(); + return; + } + long curTime = System.currentTimeMillis(); + if (curTime - lastScanTime <= SCAN_INTERVAL) { + if (new File(AppFileHelper.getAppDataPath().concat(LOCAL_FILE_NAME)).exists()) { + callError(listener, "1分钟内不应该再次扫描", new IllegalStateException("1分钟内不应该再次扫描")); + reset(); + return; + } + } + scan(sALBUM, listener); + } + + private void scanAsync(final LinkedHashMap> sALBUM, @Nullable final OnScanListener listener) { + ThreadPoolManager.execute(new Runnable() { + @Override + public void run() { + scan(sALBUM, listener); + } + }); + } + + private void scan(LinkedHashMap> sALBUM, @Nullable OnScanListener listener) { + if (sALBUM == null) return; + KLog.i(TAG, "isWrite >> " + (sALBUM == sWRITE_ALBUM)); + Cursor cursor = AppContext.getAppContext() + .getContentResolver() + .query(SCAN_EXTERNAL_SD ? MediaStore.Images.Media.EXTERNAL_CONTENT_URI : MediaStore.Images.Media.INTERNAL_CONTENT_URI, + STORE_IMGS, + null, + null, + MediaStore.Images.ImageColumns.DATE_TAKEN.concat(QUERY_ORDER)); + if (cursor == null) { + callError(listener, "cursor为空", null); + reset(); + return; + } + sALBUM.clear(); + //构造thumb的查询语句(where id = xxx) + final String[] thumbWhereQuery = new String[1]; + List allImageInfoLists = new ArrayList<>(); + final int cursorCount = cursor.getCount(); + sALBUM.put(ALL_PHOTO_TITLE, allImageInfoLists); + cursor.moveToFirst(); + while (cursor.moveToNext()) { + int imgId = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)); + String imgPath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); + int orientation = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.ImageColumns.ORIENTATION)); + String albumName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)); + long dateTaken = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)); + + File imageFile = new File(imgPath); + if (!imageFile.exists() || imageFile.isDirectory() || imageFile.length() <= 0) continue; + + thumbWhereQuery[0] = String.valueOf(imgId); + String thumbImgPath = getThumbPath(thumbWhereQuery); + + List imageInfoList = sALBUM.get(albumName); + ImageInfo imageInfo = new ImageInfo(imgPath, thumbImgPath, albumName, dateTaken, orientation); + try { + if (imageInfo.checkValided()) { + allImageInfoLists.add(imageInfo.clone()); + } + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + KLog.e(e); + } + if (imageInfoList == null) { + imageInfoList = new ArrayList<>(); + sALBUM.put(albumName, imageInfoList); + } else { + if (imageInfo.checkValided()) { + imageInfoList.add(imageInfo); + } + } + callProgress(listener, (int) (cursor.getPosition() * 100.0f / cursorCount)); + } + lastScanTime = System.currentTimeMillis(); + SharePreferenceHelper.saveLongPreferenceByKey(SharePreferenceHelper.APP_LAST_SCAN_IMG_TIME, lastScanTime); + cursor.close(); + callFinish(listener); + reset(); + KLog.i(TAG, "scan完成"); + if (sALBUM == sWRITE_ALBUM && !sALBUM.isEmpty()) { + sALBUM.clear(); + sALBUM.putAll(sWRITE_ALBUM); + } + //事实上io流的速度也是杠杠的,所以这里可以采取写入到本地文件的方法来存储扫描结果 + ThreadPoolManager.execute(new WriteToLocalRunnable(sALBUM)); + } + + private void reset() { + isScaning = false; + progressRunnable.reset(); + } + + public void registerContentObserver(Handler handler) { + AppContext.getAppContext() + .getContentResolver() + .registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, new MediaImageContentObserver(handler)); + } + + private boolean checkLocalSerializableFile() { + if (!sALBUM.isEmpty()) return true; + File file = new File(AppFileHelper.getAppDataPath().concat(LOCAL_FILE_NAME)); + if (file.exists()) { + KLog.i(TAG, "当前时间 >>> " + TimeUtil.longToTimeStr(System.currentTimeMillis(), TimeUtil.YYYYMMDDHHMMSS)); + KLog.i(TAG, "文件修改时间 >>> " + TimeUtil.longToTimeStr(file.lastModified(), TimeUtil.YYYYMMDDHHMMSS)); + //每30分钟进行一次静默扫描 + boolean reScanSilent = System.currentTimeMillis() - file.lastModified() > 30 * TimeUtil.MINUTE * 1000; + //每周进行一次回调扫描 + boolean reScanFullAndCallBack = System.currentTimeMillis() - file.lastModified() > 24 * 7 * TimeUtil.HOUR * 1000; + KLog.i(TAG, "需要重新扫描? >>> " + reScanSilent); + try { + LinkedHashMap> map = GsonUtil.INSTANCE.toLinkHashMap(FileUtil.Read(file.getAbsolutePath()), + new TypeToken>>() { + }.getType()); + if (!map.isEmpty()) { + sALBUM.clear(); + sALBUM.putAll(map); + if (reScanFullAndCallBack) { + KLog.i(TAG, "全量回调扫描"); + return false; + } + if (reScanSilent) { + KLog.i(TAG, "静默扫描"); + scanAsync(sWRITE_ALBUM, null); + } + return true; + } + } catch (Exception e) { + KLog.e(e); + return false; + } + } + return false; + } + + public boolean hasData() { + return !sALBUM.isEmpty(); + } + + private String getThumbPath(String[] whereQuery) { + String result = null; + //通过大图的id,并且构造cursor的查询语句来获取大图对应的小图 + Cursor cursor = AppContext.getAppContext() + .getContentResolver() + .query(SCAN_EXTERNAL_SD ? MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI : MediaStore.Images.Thumbnails.INTERNAL_CONTENT_URI, + THUMBNAIL_STORE_IMAGE, + MediaStore.Images.Thumbnails.IMAGE_ID.concat(" = ?"), + whereQuery, + null); + if (cursor == null) return null; + + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + result = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Thumbnails.DATA)); + } + cursor.close(); + return result; + } + + public LinkedHashMap> getLocalImagesMap() { + return new LinkedHashMap<>(sALBUM); + } + + public List getLocalImages(String albumName) { + List infos = sALBUM.get(albumName); + if (infos != null) { + return new ArrayList(infos); + } + return new ArrayList(); + } + + public String getAllPhotoTitle() { + return ALL_PHOTO_TITLE; + } + + public void writeToLocal() { + KLog.i(TAG, "图库记录写入本地"); + ThreadPoolManager.execute(new WriteToLocalRunnable(sALBUM)); + } + + private void callStart(final OnScanListener listener) { + if (listener != null) { + handler.post(new Runnable() { + @Override + public void run() { + listener.onStart(); + } + }); + } + } + + private void callFinish(final OnScanListener listener) { + if (listener != null) { + handler.post(new Runnable() { + @Override + public void run() { + listener.onFinish(); + } + }); + } + } + + private void callError(final OnScanListener listener, final String message, final Exception e) { + if (listener != null) { + handler.post(new Runnable() { + @Override + public void run() { + listener.onError(new LPException(message, e)); + } + }); + } + } + + private void callProgress(OnScanListener listener, int progress) { + KLog.i(TAG, "progress >> " + progress); + if (listener instanceof OnScanProgresslistener) { + if (progressRunnable.getListener() == null) { + progressRunnable.setListener((OnScanProgresslistener) listener); + } + progressRunnable.setProgress(progress); + handler.post(progressRunnable); + } + } + + public interface OnScanListener { + void onStart(); + + void onFinish(); + + void onError(LPException e); + } + + public abstract static class OnScanProgresslistener implements OnScanListener { + public abstract void onProgress(int progress); + } + + private static class ProgressRunnable implements Runnable { + + int progress; + OnScanProgresslistener listener; + + public ProgressRunnable() { + this(null); + } + + public ProgressRunnable(OnScanProgresslistener listener) { + progress = 0; + this.listener = listener; + } + + public void setListener(OnScanProgresslistener listener) { + this.listener = listener; + } + + public OnScanProgresslistener getListener() { + return listener; + } + + private void setProgress(int progress) { + this.progress = progress; + } + + private void reset() { + this.progress = 0; + this.listener = null; + } + + + @Override + public void run() { + if (listener != null) { + listener.onProgress(progress); + } + } + } + + private static class WriteToLocalRunnable implements Runnable { + LinkedHashMap> write; + + public WriteToLocalRunnable(LinkedHashMap> write) { + if (write != null) { + this.write = new LinkedHashMap<>(write); + } else { + this.write = new LinkedHashMap<>(); + } + } + + @Override + public void run() { + if (!write.isEmpty()) { + FileUtil.writeToFile(AppFileHelper.getAppDataPath().concat(LOCAL_FILE_NAME), GsonUtil.INSTANCE.toString(write).getBytes()); + } + } + } + + /** + * 本监听存在于整个app生命期 + */ + class MediaImageContentObserver extends ContentObserver { + + /** + * Creates a content observer. + * + * @param handler The handler to run {@link #onChange} on, or null if none. + */ + public MediaImageContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (uri.compareTo(MediaStore.Images.Media.EXTERNAL_CONTENT_URI) != 0) return; + KLog.i(TAG, "监听到系统图库更新 >>> " + uri.toString()); + + //查询5分钟内更新的数据 + long queryTime = System.currentTimeMillis() - (5 * 60 * 1000); + final String[] whereQuery = {String.valueOf(queryTime)}; + + + Cursor cursor = AppContext.getAppContext() + .getContentResolver() + .query(uri, STORE_IMGS, MediaStore.Images.ImageColumns.DATE_TAKEN.concat(" > ?"), + whereQuery, MediaStore.Images.ImageColumns.DATE_TAKEN.concat(QUERY_ORDER)); + if (cursor == null) return; + KLog.i(TAG, "查询到 >> " + cursor.getCount() + " 条数据"); + // FIXME: 2017/3/24 没错。。。他喵的又是上面的重复步骤,有空把它抽取出来 + final String[] thumbWhereQuery = new String[1]; + List allImageInfoLists = sALBUM.get(ALL_PHOTO_TITLE); + if (cursor.moveToLast()) { + int imgId = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)); + String imgPath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); + int orientation = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.ImageColumns.ORIENTATION)); + String albumName = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME)); + long dateTaken = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)); + + if (!new File(imgPath).exists()) return; + + thumbWhereQuery[0] = String.valueOf(imgId); + String thumbImgPath = getThumbPath(thumbWhereQuery); + + List imageInfoList = sALBUM.get(albumName); + ImageInfo imageInfo = new ImageInfo(imgPath, thumbImgPath, albumName, dateTaken, orientation); + try { + if (allImageInfoLists != null && imageInfo.checkValided()) { + allImageInfoLists.add(imageInfo.clone()); + } + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + KLog.e(e); + } + if (imageInfoList == null) { + imageInfoList = new ArrayList<>(); + sALBUM.put(albumName, imageInfoList); + } else { + if (imageInfo.checkValided()) { + imageInfoList.add(imageInfo); + } + } + + KLog.i(TAG, "成功刷新到一条数据 >>> " + imageInfo.toString()); + } + cursor.close(); + } + } + +} diff --git a/common/src/main/java/com/razerdp/github/common/modules/base/BaseModuleApplication.java b/common/src/main/java/com/razerdp/github/common/modules/base/BaseModuleApplication.java new file mode 100644 index 0000000..9d9624b --- /dev/null +++ b/common/src/main/java/com/razerdp/github/common/modules/base/BaseModuleApplication.java @@ -0,0 +1,9 @@ +package com.razerdp.github.common.modules.base; + +import android.app.Application; + +/** + * Created by 大灯泡 on 2019/7/30. + */ +public abstract class BaseModuleApplication extends Application { +} diff --git a/common/src/main/res/mipmap-hdpi/ic_launcher.png b/common/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..82d3d1e167899da96d68ceafa44c69840f51a31d GIT binary patch literal 4920 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 literal 0 HcmV?d00001 diff --git a/common/src/main/res/mipmap-xhdpi/ic_launcher.png b/common/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..48c8a4093fc894b5e5d89bc61dbbba3260921945 GIT binary patch literal 8277 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 literal 0 HcmV?d00001 diff --git a/common/src/main/res/mipmap-xxhdpi/ic_launcher.png b/common/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..462de5711ba6f6603979594c92fa9059c5a5a8df GIT binary patch literal 16315 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 F{U5a&ZZ7}; literal 0 HcmV?d00001 diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml new file mode 100644 index 0000000..ec17feb --- /dev/null +++ b/common/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #d7dce1 + \ No newline at end of file diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml new file mode 100644 index 0000000..9e44af5 --- /dev/null +++ b/common/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + common + diff --git a/common/src/main/res/values/styles.xml b/common/src/main/res/values/styles.xml new file mode 100644 index 0000000..cccd023 --- /dev/null +++ b/common/src/main/res/values/styles.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/test/java/com/razerdp/github/common/ExampleUnitTest.java b/common/src/test/java/com/razerdp/github/common/ExampleUnitTest.java new file mode 100644 index 0000000..935be50 --- /dev/null +++ b/common/src/test/java/com/razerdp/github/common/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.github.common; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/config.gradle b/config.gradle index 567edb0..4639952 100644 --- a/config.gradle +++ b/config.gradle @@ -1,43 +1,14 @@ ext { - android = [ compileSdkVersion: 28, minSdkVersion : 17, targetSdkVersion : 28, versionCode : 26, - versionName : "2.6" - ] - - baselib = [ - versionCode: 3, - versionName: "1.2" - ] - - baseuilib = [ - versionCode: 2, - versionName: "1.1" - ] - - imagelib = [ - versionCode: 1, - versionName: "1.0" - ] - - photoselect = [ - versionCode: 1, - versionName: "1.0" - ] - - publish = [ - versionCode: 1, - versionName: "1.0" + versionName : "3" ] dependencies = [ - appcompat_v7 : 'com.android.support:appcompat-v7:28.0.0', - design : 'com.android.support:design:28.0.0', - support_util : 'com.android.support:support-core-utils:28.0.0', - arouter_api : 'com.alibaba:arouter-api:1.4.1', - arouter_compiler: 'com.alibaba:arouter-compiler:1.2.2' + androidx: 'androidx.appcompat:appcompat:1.1.0-rc01', + gson : 'com.google.code.gson:gson:2.8.5' ] } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2fb5a91..df298d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,3 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Tue Feb 09 19:17:15 CST 2016 - -# лmoduleappģʽʱǵsyncһ -isModule=false -#isModule=true diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..905bd59 --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'com.android.library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + api rootProject.ext.dependencies.androidx + + //gson + api rootProject.ext.dependencies.gson + +} diff --git a/lib/proguard-rules.pro b/lib/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/lib/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 diff --git a/lib/src/androidTest/java/com/razerdp/github/lib/ExampleInstrumentedTest.java b/lib/src/androidTest/java/com/razerdp/github/lib/ExampleInstrumentedTest.java new file mode 100644 index 0000000..c4a904d --- /dev/null +++ b/lib/src/androidTest/java/com/razerdp/github/lib/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.github.lib; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.github.lib.test", appContext.getPackageName()); + } +} diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..896d15c --- /dev/null +++ b/lib/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/lib/src/main/java/com/razerdp/github/lib/api/AppContext.java b/lib/src/main/java/com/razerdp/github/lib/api/AppContext.java new file mode 100644 index 0000000..6ec34d7 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/api/AppContext.java @@ -0,0 +1,130 @@ +package com.razerdp.github.lib.api; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Looper; +import android.util.Log; + + +/** + * Created by 大灯泡 on 2017/3/22. + *

+ * baselib appcontext类 + *

+ * 该工具类方法来自: + * https://github.com/kymjs/Common/blob/master/Common/common/src/main/java/com/kymjs/common/App.java + */ + +public class AppContext { + + private static final String TAG = "AppContext"; + public static final Application sApplication; + private static final InnerLifecycleHandler INNER_LIFECYCLE_HANDLER; + + static { + Application app = null; + try { + app = (Application) Class.forName("android.app.AppGlobals").getMethod("getInitialApplication").invoke(null); + if (app == null) + throw new IllegalStateException("Static initialization of Applications must be on main thread."); + } catch (final Exception e) { + Log.e(TAG, "Failed to get current application from AppGlobals." + e.getMessage()); + try { + app = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null); + } catch (final Exception ex) { + Log.e(TAG, "Failed to get current application from ActivityThread." + e.getMessage()); + } + } finally { + sApplication = app; + } + INNER_LIFECYCLE_HANDLER = new InnerLifecycleHandler(); + if (sApplication != null) { + sApplication.registerActivityLifecycleCallbacks(INNER_LIFECYCLE_HANDLER); + } + } + + public static boolean isAppVisable() { + return INNER_LIFECYCLE_HANDLER != null && INNER_LIFECYCLE_HANDLER.started > INNER_LIFECYCLE_HANDLER.stopped; + } + + public static boolean isAppBackground() { + return INNER_LIFECYCLE_HANDLER != null && INNER_LIFECYCLE_HANDLER.resumed <= INNER_LIFECYCLE_HANDLER.stopped; + } + + private static void checkAppContext() { + if (sApplication == null) + throw new IllegalStateException("app reference is null"); + } + + public static Application getAppInstance() { + checkAppContext(); + return sApplication; + } + + public static Context getAppContext() { + checkAppContext(); + return sApplication.getApplicationContext(); + } + + public static Resources getResources() { + checkAppContext(); + return sApplication.getResources(); + } + + public static boolean isMainThread() { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + + + private static class InnerLifecycleHandler implements Application.ActivityLifecycleCallbacks { + private int created; + private int resumed; + private int paused; + private int started; + private int stopped; + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + ++created; + + } + + @Override + public void onActivityStarted(Activity activity) { + ++started; + + } + + @Override + public void onActivityResumed(Activity activity) { + ++resumed; + + } + + @Override + public void onActivityPaused(Activity activity) { + ++paused; + + } + + @Override + public void onActivityStopped(Activity activity) { + ++stopped; + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + + } + } + +} diff --git a/lib/src/main/java/com/razerdp/github/lib/base/BaseLibActivity.java b/lib/src/main/java/com/razerdp/github/lib/base/BaseLibActivity.java new file mode 100644 index 0000000..6c1bf17 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/base/BaseLibActivity.java @@ -0,0 +1,153 @@ +package com.razerdp.github.lib.base; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.razerdp.github.lib.api.AppContext; +import com.razerdp.github.lib.helper.PermissionHelper; +import com.razerdp.github.lib.interfaces.IPermission; +import com.razerdp.github.lib.interfaces.OnPermissionGrantListener; +import com.razerdp.github.lib.utils.KLog; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + + +/** + * Created by 大灯泡 on 2017/3/22. + *

+ * BaseLibActivity + */ + +public abstract class BaseLibActivity extends AppCompatActivity implements IPermission { + + private PermissionHelper mPermissionHelper; + + protected boolean isAppInBackground = false; + + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + KLog.i("当前打开 : " + this.getClass().getSimpleName()); + if (mPermissionHelper == null) { + mPermissionHelper = new PermissionHelper(this); + } + onHandleIntent(getIntent()); + } + + public void requestPermission(OnPermissionGrantListener listener, PermissionHelper.Permission... permissions) { + getPermissionHelper().requestPermission(listener, permissions); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (mPermissionHelper != null) { + mPermissionHelper.handlePermissionsResult(requestCode, permissions, grantResults); + } + } + + + @Override + public PermissionHelper getPermissionHelper() { + if (mPermissionHelper == null) { + mPermissionHelper = new PermissionHelper(this); + } + return mPermissionHelper; + } + + + @Override + protected void onStop() { + super.onStop(); + if (AppContext.isAppBackground()) { + isAppInBackground = true; + } + } + + @Override + protected void onResume() { + super.onResume(); + if (isAppInBackground) { + isAppInBackground = false; + } + } + + /** + * run in {@link BaseLibActivity#onCreate(Bundle)} but before {@link AppCompatActivity#setContentView(int)} + *

+ *

+ * 如果有intent,则需要处理这个intent(该方法在onCreate里面执行,但在setContentView之前调用) + * + * @param intent + * @return false:关闭activity + */ + public abstract void onHandleIntent(Intent intent); + + protected T findView(@IdRes int id) { + return (T) super.findViewById(id); + } + + + public Activity getActivity() { + return BaseLibActivity.this; + } + + /** + * 隐藏状态栏 + *

+ * 在setContentView前调用 + */ + protected void hideStatusBar() { + final int sdkVer = Build.VERSION.SDK_INT; + if (sdkVer < 16) { + //4.0及一下 + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } else { + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + decorView.setSystemUiVisibility(uiOptions); + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + } + } + + protected void showStatusBar() { + final int sdkVer = Build.VERSION.SDK_INT; + if (sdkVer < 16) { + //4.0及一下 + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } else { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.show(); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPermissionHelper != null) { + mPermissionHelper.handleDestroy(); + } + mPermissionHelper = null; + } + +} diff --git a/lib/src/main/java/com/razerdp/github/lib/base/BaseLibFragment.java b/lib/src/main/java/com/razerdp/github/lib/base/BaseLibFragment.java new file mode 100644 index 0000000..a85e6ce --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/base/BaseLibFragment.java @@ -0,0 +1,140 @@ +package com.razerdp.github.lib.base; + +import android.app.Activity; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.razerdp.github.lib.helper.PermissionHelper; +import com.razerdp.github.lib.interfaces.IPermission; +import com.razerdp.github.lib.interfaces.OnPermissionGrantListener; +import com.razerdp.github.lib.utils.KeyBoardUtil; + +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +/** + * Created by 大灯泡 on 2017/3/29. + *

+ * basefragment + */ + +public abstract class BaseLibFragment extends Fragment implements IPermission { + private View rootView; + protected Activity mActivity; + private PermissionHelper mPermissionHelper; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (mActivity == null) { + mActivity = getActivity(); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (mActivity == null) { + mActivity = getActivity(); + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (mActivity == null) { + mActivity = getActivity(); + } + if (mPermissionHelper == null) { + mPermissionHelper = new PermissionHelper(this); + } + rootView = inflater.inflate(getLayoutResId(), container, false); + if (rootView != null) { + onPreInitView(rootView); + onInitData(); + onInitView(rootView); + afterInitView(); + return rootView; + } else { + return super.onCreateView(inflater, container, savedInstanceState); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + onVisibleChange(isVisibleToUser); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (mPermissionHelper != null) { + mPermissionHelper.handlePermissionsResult(requestCode, permissions, grantResults); + } + } + + @Override + public PermissionHelper getPermissionHelper() { + if (mPermissionHelper == null) { + mPermissionHelper = new PermissionHelper(this); + } + return mPermissionHelper; + } + + public void requestPermission(OnPermissionGrantListener listener, PermissionHelper.Permission... permissions) { + getPermissionHelper().requestPermission(listener, permissions); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPermissionHelper != null) { + mPermissionHelper.handleDestroy(); + } + mPermissionHelper = null; + } + + @LayoutRes + public abstract int getLayoutResId(); + + protected void onPreInitView(View rootView) { + } + + + protected abstract void onInitData(); + + protected abstract void onInitView(View rootView); + + protected void afterInitView() { + } + + protected void onVisibleChange(boolean isVisibleToUser) { + } + + protected View getRootView() { + return rootView; + } + + protected T findView(@IdRes int id) { + if (rootView != null) { + return (T) rootView.findViewById(id); + } else { + return null; + } + } + + protected void back() { + KeyBoardUtil.close(getActivity()); + if (getFragmentManager().getBackStackEntryCount() > 0) { + getFragmentManager().popBackStack(); + } else { + getActivity().finish(); + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/helper/AppFileHelper.java b/lib/src/main/java/com/razerdp/github/lib/helper/AppFileHelper.java new file mode 100644 index 0000000..87b8108 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/helper/AppFileHelper.java @@ -0,0 +1,192 @@ +package com.razerdp.github.lib.helper; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Environment; +import android.text.TextUtils; + +import com.razerdp.github.lib.api.AppContext; +import com.razerdp.github.lib.interfaces.IPermission; +import com.razerdp.github.lib.interfaces.OnPermissionGrantListener; +import com.razerdp.github.lib.utils.FileUtil; +import com.razerdp.github.lib.utils.KLog; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import androidx.core.app.ActivityCompat; + + +/** + * Created by 大灯泡 on 2017/3/23. + *

+ * app文件helper,针对7.0,需要留意path与filepaths一致 + */ + +public class AppFileHelper { + private static boolean OVERM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + private static final String TAG = "AppFileHelper"; + + public static final String[] INTERNAL_STORAGE_PATHS = new String[]{"/mnt/", "/emmc/"}; + public static final String ROOT_PATH = "razerdp/github/friendcircle/"; + public static final String DATA_PATH = ROOT_PATH + "data/"; + public static final String CACHE_PATH = ROOT_PATH + "cache/"; + public static final String PIC_PATH = ROOT_PATH + "pic/"; + public static final String CAMERA_PATH = ROOT_PATH + "pic/camera/"; + public static final String LOG_PATH = ROOT_PATH + "log/"; + public static final String DOWNLOAD_PATH = ROOT_PATH + "download/"; + public static final String TEMP_PATH = ROOT_PATH + "temp/"; + + private static String storagePath; + + public static void initStroagePath(Activity activity) { + if (!TextUtils.isEmpty(storagePath)) return; + if (OVERM) { + if (activity instanceof IPermission) { + ((IPermission) activity).getPermissionHelper().requestPermission(new OnPermissionGrantListener() { + @Override + public void onPermissionGranted(PermissionHelper.Permission... grantedPermissions) { + initStroagePathInternal(); + } + + @Override + public void onPermissionsDenied(PermissionHelper.Permission... deniedPermissions) { + + } + }, PermissionHelper.Permission.WRITE_EXTERNAL_STORAGE, PermissionHelper.Permission.READ_EXTERNAL_STORAGE); + } else { + int permission1 = ActivityCompat.checkSelfPermission(activity, + Manifest.permission.WRITE_EXTERNAL_STORAGE); + int permission2 = ActivityCompat.checkSelfPermission(activity, + Manifest.permission.READ_EXTERNAL_STORAGE); + + if (permission1 != PackageManager.PERMISSION_GRANTED || + permission2 != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + activity, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, + 1 + ); + } else { + initStroagePathInternal(); + } + } + } else { + initStroagePathInternal(); + } + } + + + private static void initStroagePathInternal() { + if (TextUtils.isEmpty(storagePath)) { + //M开始用的filePorider + if (!OVERM) { + storagePath = FileUtil.getStoragePath(AppContext.getAppContext(), false); + } + if (TextUtils.isEmpty(storagePath)) { + //没有路径就使用getExternalStorageDirectory + storagePath = Environment.getExternalStorageDirectory().getAbsolutePath(); + if (TextUtils.isEmpty(storagePath)) { + //依然没法创建路径则使用私有的 + storagePath = AppContext.getAppContext().getFilesDir().getAbsolutePath(); + } + } + } + + storagePath = FileUtil.checkFileSeparator(storagePath); + KLog.i(TAG, "storagepath >> " + storagePath); + + File rootDir = new File(storagePath.concat(ROOT_PATH)); + checkAndMakeDir(rootDir); + + File dataDir = new File(storagePath.concat(DATA_PATH)); + checkAndMakeDir(dataDir); + + File cacheDir = new File(storagePath.concat(CACHE_PATH)); + checkAndMakeDir(cacheDir); + + File picDir = new File(storagePath.concat(PIC_PATH)); + checkAndMakeDir(picDir); + + File cameraDir = new File(storagePath.concat(CAMERA_PATH)); + checkAndMakeDir(cameraDir); + + File logDir = new File(storagePath.concat(LOG_PATH)); + checkAndMakeDir(logDir); + + File downLoadDir = new File(storagePath.concat(DOWNLOAD_PATH)); + checkAndMakeDir(downLoadDir); + + File tempDir = new File(storagePath.concat(TEMP_PATH)); + checkAndMakeDir(tempDir); + } + + private static void checkAndMakeDir(File file) { + if (!file.exists()) { + boolean result = file.mkdirs(); + KLog.i(TAG, "mkdirs >>> " + file.getAbsolutePath() + " success >> " + result); + } + } + + public static String getAppStoragePath() { + return storagePath; + } + + public static String getAppDataPath() { + return storagePath.concat(DATA_PATH); + } + + + public static String getAppCachePath() { + return storagePath.concat(CACHE_PATH); + } + + public static String getAppPicPath() { + return storagePath.concat(PIC_PATH); + } + + public static String getCameraPath() { + return storagePath.concat(CAMERA_PATH); + } + + public static String getAppLogPath() { + return storagePath.concat(LOG_PATH); + } + + public static String getAppTempPath() { + return storagePath.concat(TEMP_PATH); + } + + public static String createShareImageName() { + return createImageName(false); + } + + public static String createImageName(boolean isJpg) { + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat dateFormat = new SimpleDateFormat( + "yyyyMMdd_HHmmss", Locale.US); + return dateFormat.format(date) + (isJpg ? ".jpg" : ".png"); + } + + public static String createCropImageName() { + Date date = new Date(System.currentTimeMillis()); + SimpleDateFormat dateFormat = new SimpleDateFormat( + "yyyyMMdd_HHmmss", Locale.US); + return dateFormat.format(date) + "_crop.png"; + } + + public static String getDownloadDir() { + return storagePath.concat(DOWNLOAD_PATH); + } + + public static String getDownloadPath(String url) { + String name = FileUtil.getFileName(url); + return getDownloadDir() + name; + } + +} diff --git a/lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelper.java b/lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelper.java new file mode 100644 index 0000000..34dc0af --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelper.java @@ -0,0 +1,333 @@ +package com.razerdp.github.lib.helper; + +import android.Manifest; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; + +import com.razerdp.github.lib.interfaces.OnPermissionGrantListener; +import com.razerdp.github.lib.utils.KLog; +import com.razerdp.github.lib.utils.ToolUtil; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + + +/** + * 权限帮助类 + */ + +public class PermissionHelper { + private static final String TAG = "PermissionHelper"; + private WeakReference mWeakReference; + private OnPermissionGrantListener mOnPermissionGrantListener; + private PermissionHelperCompat mHelperCompat; + + private static final int REQUEST_CODE_REQUEST_PERMISSION = 6666; + + public enum Permission { + RECORD_AUDIO(Manifest.permission.RECORD_AUDIO, "录音"), + GET_ACCOUNTS(Manifest.permission.GET_ACCOUNTS, "访问账户Gmail列表"), + READ_PHONE_STATE(Manifest.permission.READ_PHONE_STATE, "读取电话状态"), + CALL_PHONE(Manifest.permission.CALL_PHONE, "拨打电话"), + CAMERA(Manifest.permission.CAMERA, "拍照权限"), + ACCESS_FINE_LOCATION(Manifest.permission.ACCESS_FINE_LOCATION, "获取精确位置"), + ACCESS_COARSE_LOCATION(Manifest.permission.ACCESS_COARSE_LOCATION, "获取粗略位置"), + READ_EXTERNAL_STORAGE(Manifest.permission.READ_EXTERNAL_STORAGE, "读外部存储"), + WRITE_EXTERNAL_STORAGE(Manifest.permission.WRITE_EXTERNAL_STORAGE, "读写外部存储"); + + private final String permission; + private String desc; + + Permission(String permission, String desc) { + this.permission = permission; + this.desc = desc; + } + + public String getTips() { + return desc; + } + + public Permission setTips(String tips) { + this.desc = tips; + return this; + } + + public static Permission[] permissionsStrToPermission(String[] permission) { + if (permission.length == 0 || permission == null) return null; + List permissions = new ArrayList<>(); + for (String s : permission) { + for (Permission permission1 : Permission.values()) { + if (TextUtils.equals(permission1.permission, s)) { + permissions.add(permission1); + } + } + } + return permissions.toArray(new Permission[permissions.size()]); + } + } + + public PermissionHelper(Activity act) { + mWeakReference = new WeakReference(act); + mHelperCompat = new PermissionHelperCompat(); + } + + public PermissionHelper(Fragment frag) { + mWeakReference = new WeakReference(frag); + mHelperCompat = new PermissionHelperCompat(); + } + + public void requestPermission(OnPermissionGrantListener listener, Permission... permissions) { + setOnPermissionGrantListener(listener); + if (!needDoNext(permissions)) return; + mHelperCompat.onRequestPermission(listener, permissions); + //保存相关信息 + //检查是否已经拥有权限 + List noGrantedPermission = checkHasGrantedAndRetrunNoGranted(permissions); + if (ToolUtil.isListEmpty(noGrantedPermission)) { + callPermissionGranted(permissions); + return; + } + + log("申请权限:\n", noGrantedPermission); + //针对没拥有的权限进行请求权限 + requestPermissionsInternal(noGrantedPermission.toArray(new String[noGrantedPermission.size()]), REQUEST_CODE_REQUEST_PERMISSION); + } + + private void requestPermissionsInternal(final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) { + if (getActivity() != null) { + ActivityCompat.requestPermissions(getActivity(), permissions, requestCode); + } else if (getFragment() != null) { + getFragment().requestPermissions(permissions, requestCode); + } + } + + + private static void shoMessageDialog(final Context context, String title, String message, String buttonText, final DialogInterface.OnClickListener + dialogButtonClickListener) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(buttonText, dialogButtonClickListener) + .create() + .show(); + } + + public void handlePermissionsResult(final int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (!checkTargetValided()) return; + if (requestCode == REQUEST_CODE_REQUEST_PERMISSION) { + mHelperCompat.onHandlePermissionResult(requestCode, permissions, grantResults); + List deniedPermissions = new ArrayList<>(); + for (int i = 0; i < grantResults.length; i++) { + int result = grantResults[i]; + if (result != PackageManager.PERMISSION_GRANTED) { + deniedPermissions.add(permissions[i]); + } + } + if (deniedPermissions.size() <= 0) { + callPermissionGranted(Permission.permissionsStrToPermission(permissions)); + return; + } + log("权限被拒绝:\n", deniedPermissions); + callPermissionDenied(Permission.permissionsStrToPermission(deniedPermissions.toArray(new String[deniedPermissions.size()]))); + //是否勾选不再询问 + List dontAskPermission = new ArrayList<>(); + for (String deniedPermission : deniedPermissions) { + if (!shouldShowRequestPermissionRationale(deniedPermission)) { + dontAskPermission.add(deniedPermission); + } + } + log("权限被永久拒绝:\n", dontAskPermission); + boolean doNext = mHelperCompat.onDontAskPermission(dontAskPermission); + if (!doNext) return; + if (dontAskPermission.size() > 0) { + openSettingActivity(getContext(), Permission.permissionsStrToPermission(dontAskPermission.toArray(new String[dontAskPermission.size()]))); + } + + } + + } + + private static void openSettingActivity(final Context context, Permission... permissions) { + StringBuffer message = new StringBuffer(); + if (permissions != null) { + for (Permission permission : permissions) { + message.append(permission.desc) + .append("\n"); + } + } + Dialog dialog = new AlertDialog.Builder(context) + .setTitle("授权被禁止,需要手动授权") + .setMessage(message) + .setPositiveButton("前往设置", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", context.getPackageName(), null); + intent.setData(uri); + context.startActivity(intent); + dialog.dismiss(); + } + }) + .setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setCancelable(false) + .create(); + dialog.show(); + } + + public Context getContext() { + Activity activity = getActivity(); + Fragment fragment = getFragment(); + if (activity == null && fragment == null) { + return null; + } + if (activity == null) { + return fragment.getContext(); + } + if (fragment == null) { + return activity; + } + return null; + } + + private Fragment getFragment() { + Object object = mWeakReference == null ? null : mWeakReference.get(); + return object instanceof Fragment ? ((Fragment) object) : null; + } + + private Activity getActivity() { + Object object = mWeakReference == null ? null : mWeakReference.get(); + return object instanceof Activity ? ((Activity) object) : null; + } + + + private void callPermissionGranted(Permission... permissions) { + boolean canCall = mHelperCompat.onBeforeCallGrantResult(permissions); + if (mOnPermissionGrantListener != null) { + if (canCall) { + mOnPermissionGrantListener.onPermissionGranted(permissions); + } + } + } + + private void callPermissionDenied(Permission... permissions) { + boolean canCall = mHelperCompat.onBeforeCallDeniedResult(permissions); + if (mOnPermissionGrantListener != null) { + if (canCall) { + mOnPermissionGrantListener.onPermissionsDenied(permissions); + } + } + } + + public void handleDestroy() { + if (mWeakReference != null) { + mWeakReference.clear(); + } + mOnPermissionGrantListener = null; + mWeakReference = null; + } + + public OnPermissionGrantListener getOnPermissionGrantListener() { + return mOnPermissionGrantListener; + } + + public void setOnPermissionGrantListener(OnPermissionGrantListener onPermissionGrantListener) { + mOnPermissionGrantListener = onPermissionGrantListener; + } + + //-----------------------------------------tools----------------------------------------- + + private boolean needDoNext(Permission[] permissions) { + //低于23的直接返回 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + callPermissionGranted(permissions); + return false; + } + + //预检查 + if (!checkTargetValided()) return false; + if (permissions == null || permissions.length <= 0) { + callPermissionGranted(permissions); + return false; + } + return true; + } + + /** + * 检查是否拥有权限 + * + * @return 没授权的权限 + */ + private List checkHasGrantedAndRetrunNoGranted(Permission... permissions) { + List result = new ArrayList<>(); + for (Permission permissionWrap : permissions) { + try { + if (ContextCompat.checkSelfPermission(getContext(), permissionWrap.permission) != PackageManager.PERMISSION_GRANTED) { + result.add(permissionWrap.permission); + } else { + log("已经拥有权限:\n", permissionWrap); + } + } catch (RuntimeException e) { + e.printStackTrace(); + result.add(permissionWrap.permission); + } + } + return result; + } + + private boolean shouldShowRequestPermissionRationale(@NonNull String permission) { + if (getActivity() != null) { + return ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission); + } else if (getFragment() != null) { + return getFragment().shouldShowRequestPermissionRationale(permission); + } + return false; + } + + //检查是否符合activity或者fragment + private boolean checkTargetValided() { + return mWeakReference != null && mWeakReference.get() != null && (mWeakReference.get() instanceof Activity || mWeakReference.get() instanceof Fragment); + } + + private void log(String desc, List data) { + String logText = null; + if (!ToolUtil.isListEmpty(data)) { + for (String datum : data) { + logText = datum + "\n"; + } + } + KLog.i(TAG, desc + logText); + } + + private void log(String desc, Permission... permissions) { + String logText = null; + if (permissions != null) { + for (Permission permission : permissions) { + logText = permission.permission + "(" + permission.desc + ")" + "\n"; + } + } + KLog.i(TAG, desc + logText); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelperCompat.java b/lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelperCompat.java new file mode 100644 index 0000000..dec6d7a --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/helper/PermissionHelperCompat.java @@ -0,0 +1,46 @@ +package com.razerdp.github.lib.helper; + + +import com.razerdp.github.lib.interfaces.OnPermissionGrantListener; + +import java.util.List; + +import androidx.annotation.NonNull; + + +/** + * Created by 大灯泡 on 2018/5/7. + *

+ * 针对权限与不同厂商适配 + *

+ * 本类所有返回boolean的方法都有如下规定: + *

+ * 当返回true,外部{@link PermissionHelper}的对应方法才能继续执行 + * 当返回false,外部{@link PermissionHelper}的对应方法中断 + */ +class PermissionHelperCompat { + + + void onRequestPermission(OnPermissionGrantListener listener, PermissionHelper.Permission... permissions) { + + } + + void onHandlePermissionResult(final int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + + } + + boolean onDontAskPermission(List dontAskPermission) { + + return true; + } + + boolean onBeforeCallGrantResult(PermissionHelper.Permission... permissions) { + + return true; + } + + boolean onBeforeCallDeniedResult(PermissionHelper.Permission... permissions) { + + return true; + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/helper/SharePreferenceHelper.java b/lib/src/main/java/com/razerdp/github/lib/helper/SharePreferenceHelper.java new file mode 100644 index 0000000..06d6084 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/helper/SharePreferenceHelper.java @@ -0,0 +1,120 @@ +package com.razerdp.github.lib.helper; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.razerdp.github.lib.api.AppContext; + +import java.util.Map; + + +/** + * Created by 大灯泡 on 2016/10/28. + *

+ * preference单例 + */ + +public class SharePreferenceHelper { + + public static final String HAS_LOGIN = "haslogin"; + public static final String HOST_NAME = "hostName"; + public static final String HOST_AVATAR = "hostAvatar"; + public static final String HOST_NICK = "hostNick"; + public static final String HOST_ID = "hostId"; + public static final String HOST_COVER = "cover"; + public static final String CHECK_REGISTER = "check_register"; + public static final String APP_HAS_SCAN_IMG = "has_scan_img"; + public static final String APP_LAST_SCAN_IMG_TIME = "last_scan_image_time"; + + + private static final String PERFERENCE_NAME = "FriendCircleData"; + private static SharedPreferences sharedPreferences = AppContext.getAppContext().getSharedPreferences(PERFERENCE_NAME, Context.MODE_PRIVATE); + + static { + sharedPreferences = AppContext.getAppContext().getSharedPreferences(PERFERENCE_NAME, Context.MODE_PRIVATE); + } + + public static String loadStringPreferenceByKey(String key, String defaultValue) { + createSharedPreferencesIfNotExist(); + return sharedPreferences.getString(key, defaultValue); + } + + public static boolean loadBooleanPreferenceByKey(String key, boolean defaultValue) { + createSharedPreferencesIfNotExist(); + return sharedPreferences.getBoolean(key, defaultValue); + } + + public static float loadFloatPreferenceByKey(String key, float defaultValue) { + createSharedPreferencesIfNotExist(); + return sharedPreferences.getFloat(key, defaultValue); + } + + public static long loadLongPreferenceByKey(String key, long defaultValue) { + createSharedPreferencesIfNotExist(); + return sharedPreferences.getLong(key, defaultValue); + } + + public static Map loadAllPreferenceByKey() { + createSharedPreferencesIfNotExist(); + return sharedPreferences.getAll(); + } + + public static int loadIntegerPreferenceByKey(String key, int defaultValue) { + createSharedPreferencesIfNotExist(); + return sharedPreferences.getInt(key, defaultValue); + } + + public static void saveStringPreferenceByKey(String key, String value) { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(key, value); + editor.apply(); + } + + public static void saveBooleanPreferenceByKey(String key, boolean value) { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(key, value); + editor.apply(); + } + + public static void saveIntegerPreferenceByKey(String key, int value) { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(key, value); + editor.apply(); + } + + public static void saveFloatPreferenceByKey(String key, float value) { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putFloat(key, value); + editor.apply(); + } + + public static void saveLongPreferenceByKey(String key, long value) { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putLong(key, value); + editor.apply(); + } + + public static void removePreferenceByKey(String key) { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(key); + editor.apply(); + } + + public static void clearAllPreference() { + createSharedPreferencesIfNotExist(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear().apply(); + } + + private static void createSharedPreferencesIfNotExist() { + if (sharedPreferences == null) { + sharedPreferences = AppContext.getAppContext().getSharedPreferences(PERFERENCE_NAME, Context.MODE_PRIVATE); + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/ClearMemoryObject.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/ClearMemoryObject.java new file mode 100644 index 0000000..b7165ea --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/ClearMemoryObject.java @@ -0,0 +1,12 @@ +package com.razerdp.github.lib.interfaces; + +/** + * Created by 大灯泡 on 2017/4/1. + *

+ * 标记清除用的obj + */ + +public interface ClearMemoryObject { + + void clearMemroy(boolean setNull); +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/ExtSimpleCallback.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/ExtSimpleCallback.java new file mode 100644 index 0000000..a5a0360 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/ExtSimpleCallback.java @@ -0,0 +1,17 @@ +package com.razerdp.github.lib.interfaces; + +/** + * Created by 大灯泡 on 2018/6/13. + * 升级版simpleCallback + */ +public abstract class ExtSimpleCallback implements SimpleCallback { + + public void onStart() { + } + + public void onError(int code,String errorMessage) { + } + + public void onFinish() { + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/IPermission.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/IPermission.java new file mode 100644 index 0000000..61c6a90 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/IPermission.java @@ -0,0 +1,11 @@ +package com.razerdp.github.lib.interfaces; + + +import com.razerdp.github.lib.helper.PermissionHelper; + +/** + * Created by 大灯泡 on 2018/3/5. + */ +public interface IPermission { + PermissionHelper getPermissionHelper(); +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/MultiClickListener.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/MultiClickListener.java new file mode 100644 index 0000000..0d65c96 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/MultiClickListener.java @@ -0,0 +1,44 @@ +package com.razerdp.github.lib.interfaces; + +import android.view.View; + +import com.razerdp.github.lib.utils.WeakHandler; + + +/** + * Created by 大灯泡 on 2016/6/21. + *

+ * 单双击事件监听 + */ +public abstract class MultiClickListener implements View.OnClickListener { + private static final int DelayedTime = 250; + private boolean isDouble = false; + private WeakHandler handler = new WeakHandler(); + + + private final Runnable runnable = new Runnable() { + @Override + public void run() { + isDouble = false; + handler.removeCallbacks(this); + onSingleClick(); + } + }; + + @Override + public final void onClick(View v) { + if (isDouble) { + isDouble = false; + handler.removeCallbacks(runnable); + onDoubleClick(); + } else { + isDouble = true; + handler.postDelayed(runnable, DelayedTime); + } + } + + public abstract void onSingleClick(); + + public abstract void onDoubleClick(); + +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/OnPermissionGrantListener.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/OnPermissionGrantListener.java new file mode 100644 index 0000000..b606b1d --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/OnPermissionGrantListener.java @@ -0,0 +1,25 @@ +package com.razerdp.github.lib.interfaces; + + +import com.razerdp.github.lib.helper.PermissionHelper; + +/** + * Created by 大灯泡 on 2018/5/7. + */ +public interface OnPermissionGrantListener { + void onPermissionGranted(PermissionHelper.Permission... grantedPermissions); + + void onPermissionsDenied(PermissionHelper.Permission... deniedPermissions); + + abstract class OnPermissionGrantListenerAdapter implements OnPermissionGrantListener { + @Override + public void onPermissionGranted(PermissionHelper.Permission... grantedPermissions) { + + } + + @Override + public void onPermissionsDenied(PermissionHelper.Permission... deniedPermissions) { + + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/SimpleCallback.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/SimpleCallback.java new file mode 100644 index 0000000..488da7c --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/SimpleCallback.java @@ -0,0 +1,8 @@ +package com.razerdp.github.lib.interfaces; + +/** + * Created by 大灯泡 on 2018/4/4. + */ +public interface SimpleCallback { + void onCall(T data); +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/SingleClickListener.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/SingleClickListener.java new file mode 100644 index 0000000..4ec949a --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/SingleClickListener.java @@ -0,0 +1,33 @@ +package com.razerdp.github.lib.interfaces; + +import android.view.View; + +/** + * Created by 大灯泡 on 2017/04/01. + *

+ * 防止短时间内触发多次点击事件 + */ +public abstract class SingleClickListener implements View.OnClickListener { + private static final String TAG = "SingleClickListener"; + + private int MIN_CLICK_DELAY_TIME = 500; + private long lastClickTime = 0; + + public SingleClickListener() { + } + + public SingleClickListener(int MIN_CLICK_DELAY_TIME) { + this.MIN_CLICK_DELAY_TIME = MIN_CLICK_DELAY_TIME; + } + + @Override + public final void onClick(View v) { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) { + lastClickTime = currentTime; + onSingleClick(v); + } + } + + public abstract void onSingleClick(View v); +} diff --git a/lib/src/main/java/com/razerdp/github/lib/interfaces/adapter/TextWatcherAdapter.java b/lib/src/main/java/com/razerdp/github/lib/interfaces/adapter/TextWatcherAdapter.java new file mode 100644 index 0000000..d14bf10 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/interfaces/adapter/TextWatcherAdapter.java @@ -0,0 +1,25 @@ +package com.razerdp.github.lib.interfaces.adapter; + +import android.text.Editable; +import android.text.TextWatcher; + +/** + * Created by 大灯泡 on 2017/10/12. + */ + +public abstract class TextWatcherAdapter implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/KeyboardControlMnanager.java b/lib/src/main/java/com/razerdp/github/lib/manager/KeyboardControlMnanager.java new file mode 100644 index 0000000..5598959 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/KeyboardControlMnanager.java @@ -0,0 +1,79 @@ +package com.razerdp.github.lib.manager; + +import android.app.Activity; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewTreeObserver; + +import java.lang.ref.WeakReference; + +/** + * Created by 大灯泡 on 2016/12/13. + * + * 键盘管理 + */ + +public class KeyboardControlMnanager { + + + private OnKeyboardStateChangeListener onKeyboardStateChangeListener; + private WeakReference act; + + private KeyboardControlMnanager(Activity act, OnKeyboardStateChangeListener onKeyboardStateChangeListener) { + this.act = new WeakReference(act); + this.onKeyboardStateChangeListener = onKeyboardStateChangeListener; + } + + + public void observerKeyboardVisibleChangeInternal() { + if (onKeyboardStateChangeListener == null) return; + Activity activity = act.get(); + if (activity == null) return; + setOnKeyboardStateChangeListener(onKeyboardStateChangeListener); + final View decorView = activity.getWindow().getDecorView(); + + decorView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + int preKeyboardHeight = -1; + Rect rect = new Rect(); + boolean preVisible = false; + + @Override + public void onGlobalLayout() { + rect.setEmpty(); + decorView.getWindowVisibleDisplayFrame(rect); + int displayHeight = rect.height(); + int windowHeight = decorView.getHeight(); + int keyboardHeight = windowHeight - displayHeight; + if (preKeyboardHeight != keyboardHeight) { + //判定可见区域与原来的window区域占比是否小于0.75,小于意味着键盘弹出来了。 + boolean isVisible = (displayHeight * 1.0f / windowHeight * 1.0f) < 0.75f; + if (isVisible != preVisible) { + onKeyboardStateChangeListener.onKeyboardChange(keyboardHeight, isVisible); + preVisible = isVisible; + } + } + preKeyboardHeight = keyboardHeight; + } + }); + + + } + + public static void observerKeyboardVisibleChange(Activity act, OnKeyboardStateChangeListener onKeyboardStateChangeListener) { + new KeyboardControlMnanager(act, onKeyboardStateChangeListener).observerKeyboardVisibleChangeInternal(); + + } + + public OnKeyboardStateChangeListener getOnKeyboardStateChangeListener() { + return onKeyboardStateChangeListener; + } + + public void setOnKeyboardStateChangeListener(OnKeyboardStateChangeListener onKeyboardStateChangeListener) { + this.onKeyboardStateChangeListener = onKeyboardStateChangeListener; + } + + //=============================================================interface + public interface OnKeyboardStateChangeListener { + void onKeyboardChange(int keyboardHeight, boolean isVisible); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/ThreadPoolManager.java b/lib/src/main/java/com/razerdp/github/lib/manager/ThreadPoolManager.java new file mode 100644 index 0000000..a293e3e --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/ThreadPoolManager.java @@ -0,0 +1,69 @@ +package com.razerdp.github.lib.manager; + +import android.os.AsyncTask; +import android.os.Build; + +import com.razerdp.github.lib.utils.KLog; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Created by 大灯泡 on 2016/10/28. + *

+ * 线程池封装 + */ +public class ThreadPoolManager { + + private static ExecutorService threadPool = null; + private static ExecutorService uploadThreadPool = null; + + static { + /** + corePoolSize 线程池中核心线程的数量 + maximumPoolSize 线程池中最大线程数量 + keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长 + unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等 + workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。 + threadFactory 为线程池提供创建新线程的功能,这个我们一般使用默认即可 + handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。 + */ + + int processorNum = Runtime.getRuntime().availableProcessors(); //CPU数量 + threadPool = new ThreadPoolExecutor(processorNum, processorNum * 2 + 1, 20, TimeUnit.SECONDS, new SynchronousQueue()); + uploadThreadPool = new ThreadPoolExecutor(processorNum, 9, 20, TimeUnit.SECONDS, new SynchronousQueue()); + } + + public static void execute(Runnable runnable) { + try { + threadPool.execute(runnable); + } catch (Exception e) { + e.printStackTrace(); + KLog.e(e.toString()); + } + } + + public static void executeOnUploadPool(Runnable runnable) { + try { + uploadThreadPool.execute(runnable); + } catch (Exception e) { + e.printStackTrace(); + KLog.e(e.toString()); + } + } + + /** + * 针对不同api的 asynctask处理 + * 3.0以后的asynctask被改为默认串行,使用自己的线程池实现并行 + */ + public static void executeOnExecutor(AsyncTask task, Params... + params) { + if (Build.VERSION.SDK_INT >= 11) { + task.executeOnExecutor(threadPool, params); + } else { + task.execute(params); + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/BaseCompressTaskHelper.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/BaseCompressTaskHelper.java new file mode 100644 index 0000000..d5e1a97 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/BaseCompressTaskHelper.java @@ -0,0 +1,88 @@ +package com.razerdp.github.lib.manager.compress; + +import android.content.Context; +import android.os.Looper; + +import com.razerdp.github.lib.api.AppContext; +import com.razerdp.github.lib.utils.WeakHandler; + +import java.util.List; + + +/** + * Created by 大灯泡 on 2018/1/10. + */ +abstract class BaseCompressTaskHelper { + protected String TAG = this.getClass().getSimpleName(); + protected OnCompressListener mOnCompressListener; + protected T data; + protected Context mContext; + WeakHandler mWeakHandler; + + public BaseCompressTaskHelper(Context context, T options, OnCompressListener onCompressListener) { + mContext = context; + data = options; + mOnCompressListener = onCompressListener; + mWeakHandler = new WeakHandler(Looper.getMainLooper()); + } + + public abstract void start(); + + + void callSuccess(final List imagePath) { + + if (mOnCompressListener == null) return; + if (AppContext.isMainThread()) { + mOnCompressListener.onSuccess(imagePath); + } else { + mWeakHandler.post(new Runnable() { + @Override + public void run() { + mOnCompressListener.onSuccess(imagePath); + } + }); + } + } + + void callError(final String message) { + if (mOnCompressListener == null) return; + if (AppContext.isMainThread()) { + mOnCompressListener.onError(message); + } else { + mWeakHandler.post(new Runnable() { + @Override + public void run() { + mOnCompressListener.onError(message); + } + }); + } + } + + void callCompress(final int cur, final int target) { + if (mOnCompressListener == null) return; + if (AppContext.isMainThread()) { + mOnCompressListener.onCompress(cur, target); + } else { + mWeakHandler.post(new Runnable() { + @Override + public void run() { + mOnCompressListener.onCompress(cur, target); + } + }); + } + } + + void callRotated(final int picIndex, final int width, final int height) { + if (mOnCompressListener == null) return; + if (AppContext.isMainThread()) { + mOnCompressListener.onRotate(picIndex, width, height); + } else { + mWeakHandler.post(new Runnable() { + @Override + public void run() { + mOnCompressListener.onRotate(picIndex, width, height); + } + }); + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressManager.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressManager.java new file mode 100644 index 0000000..7168ecf --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressManager.java @@ -0,0 +1,92 @@ +package com.razerdp.github.lib.manager.compress; + +import android.content.Context; + +import com.razerdp.github.lib.utils.KLog; +import com.razerdp.github.lib.utils.ToolUtil; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.IntDef; +import androidx.annotation.StringDef; + + +/** + * Created by 大灯泡 on 2018/1/8. + */ +public class CompressManager { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({SIZE, SCALE, BOTH}) + public @interface CompressType { + + } + + public static final int SIZE = 0x10;//质量压缩 + public static final int SCALE = 0x11;//分辨率压缩 + public static final int BOTH = 0x12;//上面两个。。。 + + + @Retention(RetentionPolicy.SOURCE) + @StringDef({JPG, PNG}) + public @interface ImageType { + } + + public static final String JPG = ".jpg"; + public static final String PNG = ".png"; + + + WeakReference mWeakReference; + + private List mOptions; + + private CompressManager(Context context) { + mWeakReference = new WeakReference(context); + mOptions = new ArrayList<>(); + } + + public static CompressManager create(Context context) { + return new CompressManager(context); + } + + + Context getContext() { + return mWeakReference == null ? null : mWeakReference.get(); + } + + public CompressOption addTask() { + return addTaskInternal(null); + } + + + CompressOption addTaskInternal(CompressOption compressOption) { + CompressOption option = new CompressOption(this); + if (compressOption != null) { + mOptions.add(compressOption); + } else { + mOptions.add(option); + } + return option; + } + + public void start() { + start(null); + } + + public void start(OnCompressListener listener) { + BaseCompressTaskHelper helper; + if (getContext() == null) { + KLog.e("context为空"); + return; + } + if (ToolUtil.isListEmpty(mOptions)) { + KLog.e("配置为空"); + return; + } + new CompressTaskQueue(getContext(), mOptions, listener).start(); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressOption.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressOption.java new file mode 100644 index 0000000..58c876e --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressOption.java @@ -0,0 +1,112 @@ +package com.razerdp.github.lib.manager.compress; + +import android.text.TextUtils; + +import com.razerdp.github.lib.helper.AppFileHelper; +import com.razerdp.github.lib.utils.EncryUtil; + +import java.io.Serializable; +import java.lang.ref.WeakReference; + +import static com.razerdp.github.lib.manager.compress.CompressManager.BOTH; + + +/** + * Created by 大灯泡 on 2018/1/9. + * 压缩配置 + */ +public class CompressOption implements Serializable { + private WeakReference mManagerWeakReference; + + @CompressManager.CompressType + int compressType = BOTH; + int sizeTarget = 5 * 1024;//默认最大5M + int maxWidth = 720;//默认最大720p + int maxHeight = 1280; + + boolean autoRotate = true; + + @CompressManager.ImageType + String suffix = CompressManager.JPG; + String originalImagePath; + String saveCompressImagePath; + + OnCompressListener mOnCompressListener; + + public CompressOption(CompressManager manager) { + mManagerWeakReference = new WeakReference(manager); + } + + CompressManager getManager() { + return mManagerWeakReference == null ? null : mManagerWeakReference.get(); + } + + + public CompressOption setCompressType(@CompressManager.CompressType int compressType) { + this.compressType = compressType; + return this; + } + + + public CompressOption setSizeTarget(int sizeTarget) { + this.sizeTarget = sizeTarget; + return this; + } + + public CompressOption setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + return this; + } + + public CompressOption setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + return this; + } + + public CompressOption setAutoRotate(boolean autoRotate) { + this.autoRotate = autoRotate; + return this; + } + + + public CompressOption setOriginalImagePath(String originalImagePath) { + this.originalImagePath = originalImagePath; + if (TextUtils.isEmpty(saveCompressImagePath)) { + saveCompressImagePath = AppFileHelper.getAppTempPath().concat(EncryUtil.MD5(originalImagePath) + suffix); + } + return this; + } + + public CompressOption setSaveCompressImagePath(String saveCompressImagePath) { + this.saveCompressImagePath = saveCompressImagePath; + return this; + } + + public CompressOption setOnCompressListener(OnCompressListener onCompressListener) { + mOnCompressListener = onCompressListener; + return this; + } + + public CompressOption setSuffix(@CompressManager.ImageType String suffix) { + this.suffix = suffix; + return this; + } + + public CompressOption addTask() { + if (getManager() == null) { + throw new NullPointerException("CompressManager must not be null"); + } + return getManager().addTaskInternal(this); + } + + public void start() { + start(null); + } + + public void start(OnCompressListener listener) { + if (getManager() == null) { + throw new NullPointerException("CompressManager must not be null"); + } + getManager().start(listener); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressResult.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressResult.java new file mode 100644 index 0000000..989dcf7 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressResult.java @@ -0,0 +1,97 @@ +package com.razerdp.github.lib.manager.compress; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Created by 大灯泡 on 2018/10/26. + */ +public class CompressResult implements Parcelable { + private String compressedFilePath; + private float originWidth; + private float originHeight; + private float compressedWidth; + private float compressedHeight; + + public CompressResult() { + } + + protected CompressResult(Parcel in) { + compressedFilePath = in.readString(); + originWidth = in.readFloat(); + originHeight = in.readFloat(); + compressedWidth = in.readFloat(); + compressedHeight = in.readFloat(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(compressedFilePath); + dest.writeFloat(originWidth); + dest.writeFloat(originHeight); + dest.writeFloat(compressedWidth); + dest.writeFloat(compressedHeight); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public CompressResult createFromParcel(Parcel in) { + return new CompressResult(in); + } + + @Override + public CompressResult[] newArray(int size) { + return new CompressResult[size]; + } + }; + + public String getCompressedFilePath() { + return compressedFilePath; + } + + public CompressResult setCompressedFilePath(String compressedFilePath) { + this.compressedFilePath = compressedFilePath; + return this; + } + + public float getOriginWidth() { + return originWidth; + } + + public CompressResult setOriginWidth(float originWidth) { + this.originWidth = originWidth; + return this; + } + + public float getOriginHeight() { + return originHeight; + } + + public CompressResult setOriginHeight(float originHeight) { + this.originHeight = originHeight; + return this; + } + + public float getCompressedWidth() { + return compressedWidth; + } + + public CompressResult setCompressedWidth(float compressedWidth) { + this.compressedWidth = compressedWidth; + return this; + } + + public float getCompressedHeight() { + return compressedHeight; + } + + public CompressResult setCompressedHeight(float compressedHeight) { + this.compressedHeight = compressedHeight; + return this; + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskHelper.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskHelper.java new file mode 100644 index 0000000..f64a8ae --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskHelper.java @@ -0,0 +1,146 @@ +package com.razerdp.github.lib.manager.compress; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.razerdp.github.lib.helper.AppFileHelper; +import com.razerdp.github.lib.manager.ThreadPoolManager; +import com.razerdp.github.lib.utils.BitmapUtil; +import com.razerdp.github.lib.utils.EncryUtil; +import com.razerdp.github.lib.utils.FileUtil; +import com.razerdp.github.lib.utils.KLog; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + + +/** + * Created by 大灯泡 on 2018/1/10. + */ +class CompressTaskHelper extends BaseCompressTaskHelper { + + private List resultBucket; + + public CompressTaskHelper(Context context, CompressOption options, OnCompressListener compressListener) { + super(context, options, compressListener); + resultBucket = new ArrayList<>(); + } + + @Override + public void start() { + if (data == null) { + callError("配置为空"); + return; + } + CompressOption option = data; + int[] imageSize = BitmapUtil.getImageSize(option.originalImagePath); + if (imageSize[0] <= -1 || imageSize[1] <= -1) { + callError(">>>>>无法获取图片大小<<<<<"); + return; + } + File imageFile = new File(option.originalImagePath); + if (!imageFile.exists()) { + callError("图片不存在。。。退出"); + return; + } + long fileSize = 0; + try { + fileSize = FileUtil.getFileSize(new File(option.originalImagePath)); + KLog.i(TAG, "文件路径 >>> " + option.originalImagePath + "\n文件大小 >>> " + FileUtil.fileLengthFormat(fileSize)); + } catch (Exception e) { + e.printStackTrace(); + } + + if (fileSize <= 0 && option.compressType != CompressManager.SCALE) { + callError("获取图片文件大小失败。。。。"); + return; + } + + startTask(imageSize, fileSize, option); + } + + private void startTask(final int[] imageSize, final long fileSize, final CompressOption option) { + ThreadPoolManager.executeOnUploadPool(new Runnable() { + @Override + public void run() { + try { + callCompress(0, 1); + startInternal(imageSize, fileSize, option); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + private void startInternal(int[] imageSize, long fileSize, CompressOption option) throws Exception { + CompressResult result = null; + switch (option.compressType) { + case CompressManager.SCALE: + result = compressOnScale(option.originalImagePath, imageSize, option); + break; + case CompressManager.SIZE: + result = compressOnSize(option.originalImagePath, fileSize, option); + break; + case CompressManager.BOTH: + String savePath = option.saveCompressImagePath; + option.setSaveCompressImagePath(AppFileHelper.getAppTempPath().concat(EncryUtil.MD5(savePath) + option.suffix)); + CompressResult scaleResult = compressOnScale(option.originalImagePath, imageSize, option); + if (scaleResult != null) { + option.setSaveCompressImagePath(savePath); + result = compressOnSize(scaleResult.getCompressedFilePath(), fileSize, option); + } + break; + } + if (result != null) { + KLog.i(TAG, "压缩成功,图片地址 >> " + result.getCompressedFilePath()); + callCompress(1, 1); + resultBucket.add(result); + callSuccess(resultBucket); + } else { + callError("压缩失败"); + } + } + + private CompressResult compressOnScale(String imagePath, int[] imageSize, CompressOption option) throws Exception { + CompressResult result = new CompressResult(); + result.setOriginWidth(imageSize[0]) + .setOriginHeight(imageSize[1]); + KLog.i(TAG, "压缩前分辨率 >> 【" + imageSize[0] + "x" + imageSize[1] + "】"); + Bitmap bp = BitmapUtil.loadBitmap(mContext, option.originalImagePath, option.maxWidth, option.maxHeight); + boolean success = BitmapUtil.saveBitmap(option.saveCompressImagePath, bp, option.suffix); + bp.recycle(); + int[] resultSize = BitmapUtil.getImageSize(option.saveCompressImagePath); + KLog.i(TAG, "压缩后分辨率 >> 【" + resultSize[0] + "x" + resultSize[1] + "】"); + result.setCompressedFilePath(option.saveCompressImagePath) + .setCompressedWidth(resultSize[0]) + .setCompressedHeight(resultSize[1]); + return success ? result : null; + } + + private CompressResult compressOnSize(String originalImagePath, long fileSize, CompressOption option) { + CompressResult result = new CompressResult(); + long sizeInKB = fileSize >> 10; + int[] size = BitmapUtil.getImageSize(option.originalImagePath); + result.setOriginWidth(size[0]) + .setOriginHeight(size[1]); + KLog.i(TAG, "压缩前的文件大小 >> " + FileUtil.fileLengthFormat(fileSize) + "\n当前设置最大文件大小 >> " + option.sizeTarget + "kb"); + if (option.sizeTarget >= sizeInKB) { + KLog.i(TAG, "小于理论大小,放弃压缩"); + FileUtil.moveFile(originalImagePath, option.saveCompressImagePath); + result.setCompressedFilePath(option.saveCompressImagePath) + .setCompressedWidth(size[0]) + .setCompressedHeight(size[1]); + return result; + } + boolean success = BitmapUtil.compressBmpToFile(BitmapUtil.loadBitmap(mContext, originalImagePath), + new File(option.saveCompressImagePath), + option.sizeTarget, + option.suffix); + size = BitmapUtil.getImageSize(option.saveCompressImagePath); + result.setCompressedWidth(size[0]) + .setCompressedHeight(size[1]); + return success ? result : null; + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskQueue.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskQueue.java new file mode 100644 index 0000000..fe9863e --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/CompressTaskQueue.java @@ -0,0 +1,93 @@ +package com.razerdp.github.lib.manager.compress; + +import android.content.Context; + +import com.razerdp.github.lib.utils.ToolUtil; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Created by 大灯泡 on 2018/1/10. + */ +public class CompressTaskQueue extends BaseCompressTaskHelper> { + + private List mTaskHelpers; + private List result; + private int taskSize; + private volatile boolean abort = false; + + CompressTaskQueue(Context context, List options, OnCompressListener onCompressListener) { + super(context, options, onCompressListener); + } + + + @Override + public void start() { + if (ToolUtil.isListEmpty(data)) { + callError("配置为空"); + return; + } + if (mTaskHelpers == null) { + prepare(); + } + startInternal(); + } + + private void prepare() { + mTaskHelpers = new ArrayList<>(); + result = new ArrayList<>(); + for (CompressOption option : data) { + //把每个单张图片的task的listener切换为本类的listener + mTaskHelpers.add(new CompressTaskHelper(mContext, option, mOnCompressListener)); + } + taskSize = mTaskHelpers.size(); + } + + private void startInternal() { + if (ToolUtil.isListEmpty(mTaskHelpers) && result.size() == taskSize) { + //如果已经全部完成,并检查之后,则意味着已经success了 + return; + } + if (abort) { + mTaskHelpers.clear(); + return; + } + //否则移除第一个并开始执行 + mTaskHelpers.remove(0).start(); + callCompress(taskSize - mTaskHelpers.size(), taskSize); + } + + + //-----------------------------------------single listener----------------------------------------- + private OnCompressListener mOnCompressListener = new OnCompressListener() { + + @Override + public void onRotate(int picIndex, int width, int height) { + callRotated(taskSize - mTaskHelpers.size(), width, height); + } + + + @Override + public void onSuccess(List imagePath) { + result.add(imagePath.get(0)); + if (result.size() == taskSize) { + callSuccess(result); + return; + } + startInternal(); + } + + @Override + public void onCompress(long current, long target) { + + } + + @Override + public void onError(String tag) { + abort = true; + callError(tag); + } + }; +} diff --git a/lib/src/main/java/com/razerdp/github/lib/manager/compress/OnCompressListener.java b/lib/src/main/java/com/razerdp/github/lib/manager/compress/OnCompressListener.java new file mode 100644 index 0000000..53b5451 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/manager/compress/OnCompressListener.java @@ -0,0 +1,34 @@ +package com.razerdp.github.lib.manager.compress; + +import java.util.List; + +/** + * Created by 大灯泡 on 2018/1/8. + */ +public interface OnCompressListener { + + void onRotate(int picIndex, int width, int height); + + void onSuccess(List imagePath); + + void onCompress(long current, long target); + + void onError(String tag); + + public static abstract class OnCompressListenerAdapter implements OnCompressListener { + @Override + public void onRotate(int picIndex, int width, int height) { + + } + + @Override + public void onCompress(long current, long target) { + + } + + @Override + public void onError(String tag) { + + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/BitmapUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/BitmapUtil.java new file mode 100644 index 0000000..1e28e4f --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/BitmapUtil.java @@ -0,0 +1,225 @@ +package com.razerdp.github.lib.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.text.TextUtils; + +import com.razerdp.github.lib.api.AppContext; +import com.razerdp.github.lib.manager.compress.CompressManager; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; + + +/** + * Created by 大灯泡 on 2018/1/10. + */ +public class BitmapUtil { + + private static final String TAG = "BitmapUtil"; + + private static final int s_defaultPicWidth = 720; + private static final int s_defaultPicHeight = 1080; + + private static int s_ScreenWidth; + private static int s_ScreenHeight; + + public static int getScreenWidthPix() { + if (s_ScreenWidth == 0) { + s_ScreenWidth = AppContext.getResources().getDisplayMetrics().widthPixels; + } + return s_ScreenWidth; + } + + public static int getScreenHeightPix() { + if (s_ScreenHeight == 0) { + s_ScreenHeight = AppContext.getResources().getDisplayMetrics().heightPixels; + } + return s_ScreenHeight; + } + + + /** + * 获取图像的宽高 + **/ + public static int[] getImageSize(String path) { + int[] size = {-1, -1}; + if (path == null) { + return size; + } + File file = new File(path); + if (file.exists() && !file.isDirectory()) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + InputStream is = new FileInputStream(path); + BitmapFactory.decodeStream(is, null, options); + size[0] = options.outWidth; + size[1] = options.outHeight; + } catch (Exception e) { + e.printStackTrace(); + } + } + return size; + } + + public static int calculateSampleSize(BitmapFactory.Options options, int width, int height) { + int w = options.outWidth; + int h = options.outHeight; + int result = 1; + + if (w > width || h > height) { + + final int halfWidth = w / 2; + final int halfHeight = h / 2; + + while ((halfHeight / result) >= width && (halfWidth / result) >= height) { + result *= 2; + } + } + + return result; + } + + public static Bitmap loadBitmap(Context c, String filePath) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 1; + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, options); + options.inJustDecodeBounds = false; + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeFile(filePath, options); + } catch (OutOfMemoryError e) { + return BitmapFactory.decodeFile(filePath); + } + } + + /** + * 指定宽高加载bitmap + */ + public static Bitmap loadBitmap(Context c, String filePath, int width, int height) { + boolean needCalculateSampleSize = width > -1 && height > -1; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 1; + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filePath, options); + if (needCalculateSampleSize) { + options.inSampleSize = calculateSampleSize(options, width, height); + } + options.inJustDecodeBounds = false; + options.inDither = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeFile(filePath, options); + } catch (OutOfMemoryError e) { + //oom,加载尝试加载一半 + if (needCalculateSampleSize) { + return loadBitmap(c, filePath, (int) (width * 0.5), (int) (height * 0.5)); + } else { + return loadBitmap(c, filePath); + } + } + } + + + /** + * 保存bitmap图片 + * + * @return 是否保存成功 + */ + public static boolean saveBitmap(String bitName, Bitmap bitmap, String suffix) { + try { + File temp = File.createTempFile("temp", suffix, new File(FileUtil.getNameDelLastPath(bitName))); + FileOutputStream fOut = null; + fOut = new FileOutputStream(temp); + if (suffix.equals(CompressManager.PNG)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); + } else { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fOut); + } + fOut.flush(); + fOut.close(); + if (temp.exists()) { + File f = new File(bitName); + if (f.exists()) { + f.delete(); + } + FileUtil.moveFile(temp.getAbsolutePath(), bitName); + } + return true; + } catch (Exception e) { + e.printStackTrace(); + } + + return false; + } + + + public static boolean compressBmpToFile(Bitmap bmp, File file, int sizeKB, String suffix) { + if (bmp == null) { + return false; + } + int compressCount = 0; + boolean isJpg = TextUtils.equals(suffix, CompressManager.JPG); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int options = 100; + bmp.compress(isJpg ? Bitmap.CompressFormat.JPEG : Bitmap.CompressFormat.PNG, options, baos); + long sizeInKb = baos.toByteArray().length >> 10; + KLog.d(TAG, "执行第1次压缩,压缩前质量 >> " + sizeInKb + "kb"); + while (sizeInKb > sizeKB && (options - 5) > 0) { + compressCount++; + baos.reset(); + options -= 5; + bmp.compress(isJpg ? Bitmap.CompressFormat.JPEG : Bitmap.CompressFormat.PNG, options, baos); + sizeInKb = baos.toByteArray().length >> 10; + KLog.d(TAG, "第" + compressCount + "次压缩完成,压缩后质量 >> " + sizeInKb + "kb"); + } + try { + FileOutputStream fos = new FileOutputStream(file); + fos.write(baos.toByteArray()); + fos.flush(); + fos.close(); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } catch (OutOfMemoryError e) { + System.gc(); + return false; + } + } + + + public static Bitmap compressBmpWithScale(Bitmap bm) { + if (bm == null) return null; + int bitmapWidth = bm.getWidth(); + int bitmapHeight = bm.getHeight(); + boolean smallerThenScreen = true; + + float ratio = bitmapWidth / bitmapHeight; + + int width = Math.min(getScreenWidthPix(), s_defaultPicWidth); + int height = Math.min(getScreenHeightPix(), s_defaultPicHeight); + float scaleWidth; + float scaleHeight; + if (bitmapWidth > width) { + scaleWidth = (float) width / bitmapWidth; + scaleHeight = width * bitmapHeight / bitmapWidth; + } else { + scaleHeight = (float) height / bitmapHeight; + scaleWidth = bitmapWidth * height / bitmapHeight; + } + + Matrix matrix = new Matrix(); + matrix.postScale(scaleWidth, scaleHeight); + // 产生缩放后的Bitmap对象 + return Bitmap.createBitmap(bm, 0, 0, bitmapWidth, bitmapHeight, matrix, true); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/EncodingUtils.java b/lib/src/main/java/com/razerdp/github/lib/utils/EncodingUtils.java new file mode 100644 index 0000000..6481feb --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/EncodingUtils.java @@ -0,0 +1,116 @@ +package com.razerdp.github.lib.utils; + +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/util/EncodingUtils.java $ + * $Revision: 503413 $ + * $Date: 2007-02-04 06:22:14 -0800 (Sun, 04 Feb 2007) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +import java.io.UnsupportedEncodingException; + +/** + * The home for utility methods that handle various encoding tasks. + * + * @author Michael Becke + * @author Oleg Kalnichevski + * + * @since 4.0 + */ +public final class EncodingUtils { + /** + * Converts the byte array of HTTP content characters to a string. If + * the specified charset is not supported, default system encoding + * is used. + * + * @param data the byte array to be encoded + * @param offset the index of the first byte to encode + * @param length the number of bytes to encode + * @param charset the desired character encoding + * @return The result of the conversion. + */ + public static String getString( + final byte[] data, + int offset, + int length, + String charset + ) { + if (data == null) { + throw new IllegalArgumentException("Parameter may not be null"); + } + if (charset == null || charset.length() == 0) { + throw new IllegalArgumentException("charset may not be null or empty"); + } + try { + return new String(data, offset, length, charset); + } catch (UnsupportedEncodingException e) { + return new String(data, offset, length); + } + } + /** + * Converts the byte array of HTTP content characters to a string. If + * the specified charset is not supported, default system encoding + * is used. + * + * @param data the byte array to be encoded + * @param charset the desired character encoding + * @return The result of the conversion. + */ + public static String getString(final byte[] data, final String charset) { + if (data == null) { + throw new IllegalArgumentException("Parameter may not be null"); + } + return getString(data, 0, data.length, charset); + } + /** + * Converts the specified string to a byte array. If the charset is not supported the + * default system charset is used. + * + * @param data the string to be encoded + * @param charset the desired character encoding + * @return The resulting byte array. + */ + public static byte[] getBytes(final String data, final String charset) { + if (data == null) { + throw new IllegalArgumentException("data may not be null"); + } + if (charset == null || charset.length() == 0) { + throw new IllegalArgumentException("charset may not be null or empty"); + } + try { + return data.getBytes(charset); + } catch (UnsupportedEncodingException e) { + return data.getBytes(); + } + } + + /** + * This class should not be instantiated. + */ + private EncodingUtils() { + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/EncryUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/EncryUtil.java new file mode 100644 index 0000000..d9cf267 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/EncryUtil.java @@ -0,0 +1,76 @@ +package com.razerdp.github.lib.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.security.MessageDigest; + +/** + * Created by 大灯泡 on 2016/9/26. + */ + +public class EncryUtil { + + public final static String MD5(String s) { + char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f' }; + try { + byte[] btInput = s.getBytes(); + // 获得MD5摘要算法的 MessageDigest 对象 + MessageDigest mdInst = MessageDigest.getInstance("MD5"); + // 使用指定的字节更新摘要 + mdInst.update(btInput); + // 获得密文 + byte[] md = mdInst.digest(); + // 把密文转换成十六进制的字符串形式 + int j = md.length; + char str[] = new char[j * 2]; + int k = 0; + for (int i = 0; i < j; i++) { + byte byte0 = md[i]; + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + return null; + } + } + + public static String getFileMD5(File file) { + if (!file.isFile()) { + return null; + } + MessageDigest digest = null; + FileInputStream in = null; + byte buffer[] = new byte[1024]; + int len; + try { + digest = MessageDigest.getInstance("MD5"); + in = new FileInputStream(file); + while ((len = in.read(buffer, 0, 1024)) != -1) { + digest.update(buffer, 0, len); + } + in.close(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return bytesToHexString(digest.digest()); + } + + public static String bytesToHexString(byte[] src) { + StringBuilder stringBuilder = new StringBuilder(""); + if (src == null || src.length <= 0) { + return null; + } + for (int i = 0; i < src.length; i++) { + int v = src[i] & 0xFF; + String hv = Integer.toHexString(v); + if (hv.length() < 2) { + stringBuilder.append(0); + } + stringBuilder.append(hv); + } + return stringBuilder.toString(); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/FileUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/FileUtil.java new file mode 100644 index 0000000..cfac4d1 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/FileUtil.java @@ -0,0 +1,586 @@ +package com.razerdp.github.lib.utils; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; +import android.os.storage.StorageManager; +import android.text.TextUtils; + +import com.razerdp.github.lib.api.AppContext; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.text.DecimalFormat; + + +public class FileUtil { + + private static final int IO_BUFFER_SIZE = 1024; + + public static boolean hasSDCard() { + return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + } + + /** + * 判断文件是否可读可写 + */ + public static boolean isFileCanReadAndWrite(String filePath) { + if (null != filePath && filePath.length() > 0) { + File f = new File(filePath); + if (null != f && f.exists()) { + return f.canRead() && f.canWrite(); + } + } + return false; + } + + public static void copy(InputStream in, OutputStream out) throws IOException { + byte[] b = new byte[IO_BUFFER_SIZE]; + int read; + while ((read = in.read(b)) != -1) { + out.write(b, 0, read); + } + } + + public static String getFileName(String path) { + int start = path.lastIndexOf("/"); + if (start != -1) { + return path.substring(start + 1); + } else { + return null; + } + } + + /** + * 得到除去文件名部分的路径 实际上就是路径中的最后一个路径分隔符前的部分。 + */ + public static String getNameDelLastPath(String fileName) { + int point = getPathLastIndex(fileName); + if (point == -1) { + return fileName; + } else { + return fileName.substring(0, point); + } + } + + /** + * 得到路径分隔符在文件路径中最后出现的位置。 对于DOS或者UNIX风格的分隔符都可以。 + * + * @param fileName 文件路径 + * @return 路径分隔符在路径中最后出现的位置,没有出现时返回-1。 + * @since 0.5 + */ + public static int getPathLastIndex(String fileName) { + int point = fileName.lastIndexOf('/'); + if (point == -1) { + point = fileName.lastIndexOf('\\'); + } + return point; + } + + /** + * 如果文件末尾有了"/"则判断是否有多个"/",是则保留一个,没有则添加 + * + * @param path + * @return + */ + public static String checkFileSeparator(String path) { + if (!TextUtils.isEmpty(path)) { + if (!path.endsWith(File.separator)) { + return path.concat(File.separator); + } else { + final int sourceStringLength = path.length(); + int index = sourceStringLength; + if (index >= 0) { + while (index >= 0) { + index--; + if (path.charAt(index) != File.separatorChar) { + break; + } + } + } + if (index < sourceStringLength) { + path = path.substring(0, index + 1); + return path.concat(File.separator); + } + } + } + return path; + } + + /** + * 复制单个文件 + * + * @param oldPath String 原文件路径 + * @param newPath String 复制后路径 + * @return boolean + */ + public static boolean copyFile(String oldPath, String newPath) { + try { + int byteread = 0; + File oldfile = new File(oldPath); + if (oldfile.exists()) { // 文件存在时 + InputStream inStream = new FileInputStream(oldPath); // 读入原文件 + FileOutputStream fs = new FileOutputStream(newPath); + byte[] buffer = new byte[IO_BUFFER_SIZE]; + + while ((byteread = inStream.read(buffer)) != -1) { + fs.write(buffer, 0, byteread); + } + inStream.close(); + fs.close(); + } + } catch (Exception e) { + KLog.e(e); + return false; + } + return true; + } + + /** + * 写入文件 + * + * @param strFileName 文件名 + * @param ins 流 + */ + public static void writeToFile(String strFileName, InputStream ins) { + try { + File file = new File(strFileName); + + FileOutputStream fouts = new FileOutputStream(file); + int len; + int maxSize = 1024 * 1024; + byte buf[] = new byte[maxSize]; + while ((len = ins.read(buf, 0, maxSize)) != -1) { + fouts.write(buf, 0, len); + fouts.flush(); + } + + fouts.close(); + } catch (IOException e) { + KLog.e(e); + } + } + + /** + * 写入文件 + * + * @param strFileName 文件名 + * @param bytes bytes + */ + public static boolean writeToFile(String strFileName, byte[] bytes) { + try { + File file = new File(strFileName); + + FileOutputStream fouts = new FileOutputStream(file); + fouts.write(bytes, 0, bytes.length); + fouts.flush(); + fouts.close(); + return true; + } catch (IOException e) { + KLog.e(e); + } + return false; + } + + /** + * Prints some data to a file using a BufferedWriter + */ + public static boolean writeToFile(String filename, String data) { + BufferedWriter bufferedWriter = null; + try { + // Construct the BufferedWriter object + bufferedWriter = new BufferedWriter(new FileWriter(filename)); + // Start writing to the output stream + bufferedWriter.write(data); + return true; + } catch (FileNotFoundException e) { + KLog.e(e); + } catch (IOException e) { + KLog.e(e); + } finally { + // Close the BufferedWriter + try { + if (bufferedWriter != null) { + bufferedWriter.flush(); + bufferedWriter.close(); + } + } catch (IOException e) { + KLog.e(e); + } + } + return false; + } + + public static String Read(String fileName) { + String res = ""; + try { + FileInputStream fin = new FileInputStream(fileName); + int length = fin.available(); + byte[] buffer = new byte[length]; + fin.read(buffer); + res = EncodingUtils.getString(buffer, "UTF-8"); + fin.close(); + } catch (Exception e) { + KLog.e(e); + } + return res; + } + + public static void Write(String fileName, String message) { + + try { + FileOutputStream outSTr = null; + try { + outSTr = new FileOutputStream(new File(fileName)); + } catch (FileNotFoundException e) { + KLog.e(e); + } + BufferedOutputStream Buff = new BufferedOutputStream(outSTr); + byte[] bs = message.getBytes(); + Buff.write(bs); + Buff.flush(); + Buff.close(); + } catch (MalformedURLException e) { + KLog.e(e); + } catch (IOException e) { + KLog.e(e); + } + } + + public static void Write(String fileName, String message, boolean append) { + try { + FileOutputStream outSTr = null; + try { + outSTr = new FileOutputStream(new File(fileName), append); + } catch (FileNotFoundException e) { + KLog.e(e); + } + BufferedOutputStream Buff = new BufferedOutputStream(outSTr); + byte[] bs = message.getBytes(); + Buff.write(bs); + Buff.flush(); + Buff.close(); + } catch (MalformedURLException e) { + KLog.e(e); + } catch (IOException e) { + KLog.e(e); + } + } + + /** + * 删除文件 删除文件夹里面的所有文件 + * + * @param path String 路径 + */ + public static void deleteFile(String path) { + File file = new File(path); + if (!file.exists()) { + return; + } + if (!file.isDirectory()) {// 如果是文件,则删除文件 + file.delete(); + return; + } + File files[] = file.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteFile(files[i].getAbsolutePath());// 先删除文件夹里面的文件 + } + files[i].delete(); + } + file.delete(); + } + + /** + * 删除文件 删除文件夹里面的所有文件 + *

+ * (此方法和deleteFile(String path)这个方法总体是一样的,只是删除代码部分用的是先改名再删除的方法删除的,为了避免EBUSY (Device or resource busy)的错误) + * + * @param path String 路径 + */ + public static void deleteFileSafely(String path) { + File file = new File(path); + if (!file.exists()) { + return; + } + if (!file.isDirectory()) {// 如果是文件,则删除文件 + safelyDelete(file); + return; + } + File files[] = file.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteFileSafely(files[i].getAbsolutePath());// 先删除文件夹里面的文件 + } + safelyDelete(files[i]); + } + safelyDelete(file); + } + + /** + * 先改名,在删除(为了避免EBUSY (Device or resource busy)的错误) + */ + private static void safelyDelete(File file) { + if (file == null || !file.exists()) return; + try { + final File to = new File(file.getAbsolutePath() + System.currentTimeMillis()); + file.renameTo(to); + to.delete(); + } catch (Exception e) { + KLog.e(e); + } + } + + /** + * 文件大小 + * + * @throws Exception + */ + public static long getFileSize(File file) throws Exception { + long size = 0; + if (!file.exists()) { + return size; + } + if (!file.isDirectory()) { + size = file.length(); + } else { + File[] fileList = file.listFiles(); + for (int i = 0; i < fileList.length; i++) { + if (fileList[i].isDirectory()) { + size = size + getFileSize(fileList[i]); + } else { + size = size + fileList[i].length(); + } + } + } + return size; + } + + /** + * @return 文件的大小,带单位(MB、KB等) + */ + public static String getFileLength(String filePath) { + try { + File file = new File(filePath); + return fileLengthFormat(getFileSize(file)); + } catch (Exception e) { + KLog.e(e); + return ""; + } + } + + /** + * @return 文件的大小,带单位(MB、KB等) + */ + public static String fileLengthFormat(long length) { + String lenStr = ""; + DecimalFormat formater = new DecimalFormat("#0.##"); + if (length > 0 && length < 1024) { + lenStr = formater.format(length) + " Byte"; + } else if (length < 1024 * 1024) { + lenStr = formater.format(length / 1024.0f) + " KB"; + } else if (length < 1024 * 1024 * 1024) { + lenStr = formater.format(length / (1024 * 1024.0f)) + " MB"; + } else { + lenStr = formater.format(length / (1024 * 1024 * 1024.0f)) + " GB"; + } + return lenStr; + } + + /** + * 得到文件的类型。 实际上就是得到文件名中最后一个“.”后面的部分。 + * + * @param fileName 文件名 + * @return 文件名中的类型部分 + */ + public static String pathExtension(String fileName) { + int point = fileName.lastIndexOf('.'); + int length = fileName.length(); + if (point == -1 || point == length - 1) { + return ""; + } else { + return fileName.substring(point, length); + } + } + + /** + * 调用系统打开文件 + */ + public static boolean openFile(File file, Context context) { + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 设置intent的Action属性 + intent.setAction(Intent.ACTION_VIEW); + // 获取文件file的MIME类型 + String type = getMIMEType(file); + // 设置intent的data和Type属性。 + intent.setDataAndType(Uri.fromFile(file), type); + // 跳转 + try { + ((Activity) context).startActivity(intent); + } catch (ActivityNotFoundException e) { + KLog.e(e); + return false; + } + return true; + } + + /** + * 根据文件后缀名获得对应的MIME类型。 + */ + @SuppressLint("DefaultLocale") + public static String getMIMEType(File file) { + + String type = "*/*"; + String fName = file.getName(); + // 获取后缀名前的分隔符"."在fName中的位置。 + int dotIndex = fName.lastIndexOf("."); + if (dotIndex < 0) { + return type; + } + /* 获取文件的后缀名 */ + String end = fName.substring(dotIndex, fName.length()).toLowerCase(); + if (end == "") { + return type; + } + // 在MIME和文件类型的匹配表中找到对应的MIME类型。 + for (int i = 0; i < MIME_MapTable.length; i++) { + if (end.equals(MIME_MapTable[i][0])) { + type = MIME_MapTable[i][1]; + } + } + return type; + } + + public static void moveFile(String oldPath, String newPath) { + copyFile(oldPath, newPath); + delFile(oldPath); + } + + public static void delFile(String filePathAndName) { + try { + File myDelFile = new File(filePathAndName); + myDelFile.delete(); + } catch (Exception e) { + System.out.println("删除文件操作出错"); + e.printStackTrace(); + } + } + + private static final String[][] MIME_MapTable = { + // {后缀名, MIME类型} + {".doc", "application/msword"}, {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".xls", "application/vnd.ms-excel"}, {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".pdf", "application/pdf"}, {".ppt", "application/vnd.ms-powerpoint"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, {".txt", "text/plain"}, + {".wps", "application/vnd.ms-works"}, {"", "*/*"} + }; + + + /** + * 删除指定目录下文件及目录 + */ + public static void deleteFolderFile(String filePath, boolean deleteThisPath) throws IOException { + if (!TextUtils.isEmpty(filePath)) { + File file = new File(filePath); + + if (file.isDirectory()) { + File files[] = file.listFiles(); + for (int i = 0; i < files.length; i++) { + deleteFolderFile(files[i].getAbsolutePath(), true); + } + } + if (deleteThisPath) { + if (!file.isDirectory()) { + file.delete(); + } else { + if (file.listFiles().length == 0) { + file.delete(); + } + } + } + } + } + + public static String getFromAssets(String fileName) { + try { + InputStreamReader inputReader = + new InputStreamReader(AppContext.getResources().getAssets().open(fileName), "UTF-8"); + BufferedReader bufReader = new BufferedReader(inputReader); + String line = ""; + String Result = ""; + while ((line = bufReader.readLine()) != null) Result += line; + return Result; + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + public static InputStream getAssetsInputStream(String fileName) { + try { + return AppContext.getResources().getAssets().open(fileName); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 得到内置或外置SD卡的路径 + * + * @param mContext + * @param isExSD true=外置SD卡 + * @return + */ + public static String getStoragePath(Context mContext, boolean isExSD) { + StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); + Class storageVolumeClazz = null; + try { + storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); + Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); + Method getPath = storageVolumeClazz.getMethod("getPath"); + Method isRemovable = storageVolumeClazz.getMethod("isRemovable"); + Object result = getVolumeList.invoke(mStorageManager); + final int length = Array.getLength(result); + for (int i = 0; i < length; i++) { + Object storageVolumeElement = Array.get(result, i); + String path = (String) getPath.invoke(storageVolumeElement); + boolean removable = (Boolean) isRemovable.invoke(storageVolumeElement); + if (isExSD == removable) { + return path; + } + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/GsonUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/GsonUtil.java new file mode 100644 index 0000000..cf9ac48 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/GsonUtil.java @@ -0,0 +1,196 @@ +package com.razerdp.github.lib.utils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; +import com.google.gson.internal.$Gson$Types; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * Created by 大灯泡 on 2016/10/28. + *

+ * gson工具类 + */ + +public enum GsonUtil { + INSTANCE; + + Gson gson = new GsonBuilder() + .serializeNulls() + .registerTypeAdapter(Integer.class, new IntegerDefaultAdapter()) + .registerTypeAdapter(int.class, new IntegerDefaultAdapter()) + .registerTypeAdapter(Double.class, new DoubleDefaultAdapter()) + .registerTypeAdapter(double.class, new DoubleDefaultAdapter()) + .registerTypeAdapter(Long.class, new LongDefaultAdapter()) + .registerTypeAdapter(long.class, new LongDefaultAdapter()) + .create(); + + public String toString(Object obj) { + return getGson().toJson(obj); + } + + public T toObject(String json, Class clazz) { + return getGson().fromJson(json, clazz); + } + + @SuppressWarnings("unchecked") + public List toList(String json, Class clazz) { + return getGson().fromJson(json, TypeList(clazz)); + } + + @SuppressWarnings("unchecked") + public Set toSet(String json, Class clazz) { + return (Set) getGson().fromJson(json, TypeSet(clazz)); + } + + @SuppressWarnings("unchecked") + public HashMap toHashMap(String json, Class keyClazz, Class valueClazz) { + return getGson().fromJson(json, TypeHashMap(keyClazz, valueClazz)); + } + + @SuppressWarnings("unchecked") + public HashMap toLinkHashMap(String json, Class keyClazz, Class valueClazz) { + return getGson().fromJson(json, TypeLinkHashMap(keyClazz, valueClazz)); + } + + @SuppressWarnings("unchecked") + public LinkedHashMap toLinkHashMap(String json, Type type) { + Gson gson = new Gson(); + return gson.fromJson(json, type); + } + + public Gson getGson() { + if (gson == null) { + gson = new GsonBuilder() + .serializeNulls() + .registerTypeAdapter(Integer.class, new IntegerDefaultAdapter()) + .registerTypeAdapter(int.class, new IntegerDefaultAdapter()) + .registerTypeAdapter(Double.class, new DoubleDefaultAdapter()) + .registerTypeAdapter(double.class, new DoubleDefaultAdapter()) + .registerTypeAdapter(Long.class, new LongDefaultAdapter()) + .registerTypeAdapter(long.class, new LongDefaultAdapter()) + .create(); + } + return gson; + } + + + public static Type TypeList(Type type) { + return $Gson$Types.newParameterizedTypeWithOwner(null, List.class, type); + } + + public static Type TypeSet(Type type) { + return $Gson$Types.newParameterizedTypeWithOwner(null, Set.class, type); + } + + public static Type TypeHashMap(Type type, Type type2) { + return $Gson$Types.newParameterizedTypeWithOwner(null, HashMap.class, type, type2); + } + + public static Type TypeLinkHashMap(Type type, Type type2) { + return $Gson$Types.newParameterizedTypeWithOwner(null, LinkedHashMap.class, type, type2); + } + + public static Type TypeMap(Type type, Type type2) { + return $Gson$Types.newParameterizedTypeWithOwner(null, Map.class, type, type2); + } + + public static Type TypeParameterized(Type ownerType, Type rawType, Type... typeArguments) { + return $Gson$Types.newParameterizedTypeWithOwner(ownerType, rawType, typeArguments); + } + + public static Type TypeArray(Type type) { + return $Gson$Types.arrayOf(type); + } + + public static Type TypeSubtypeOf(Type type) { + return $Gson$Types.subtypeOf(type); + } + + public static Type TypeSupertypeOf(Type type) { + return $Gson$Types.supertypeOf(type); + } + + + private static class DoubleDefaultAdapter implements JsonSerializer, JsonDeserializer { + @Override + public Double deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + try { + if (json.getAsString().equals("") || json.getAsString().equals("null")) {//定义为double类型,如果后台返回""或者null,则返回0.00 + return 0.0; + } + } catch (Exception ignore) { + } + try { + return json.getAsDouble(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public JsonElement serialize(Double aDouble, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(aDouble); + } + } + + private static class IntegerDefaultAdapter implements JsonSerializer, JsonDeserializer { + @Override + public Integer deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + try { + //定义为int类型,如果后台返回""或者null,则返回0 + if (json.getAsString().equals("") || json.getAsString().equals("null")) { + return 0; + } + } catch (Exception ignore) { + } + try { + return json.getAsInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public JsonElement serialize(Integer integer, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(integer); + } + } + + private static class LongDefaultAdapter implements JsonSerializer, JsonDeserializer { + @Override + public Long deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + try { + //定义为long类型,如果后台返回""或者null,则返回0 + if (json.getAsString().equals("") || json.getAsString().equals("null")) { + return 0L; + } + } catch (Exception ignore) { + } + try { + return json.getAsLong(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public JsonElement serialize(Long aLong, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(aLong); + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/ImageSelectUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/ImageSelectUtil.java new file mode 100644 index 0000000..598e62f --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/ImageSelectUtil.java @@ -0,0 +1,173 @@ +package com.razerdp.github.lib.utils; + +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +/** + * Created by 向强 on 2015/1/30. + */ +public class ImageSelectUtil { + public static String getPath(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + public static String selectImage(Context context, Intent data) { + Uri selectedImage = data.getData(); +// Log.e(TAG, selectedImage.toString()); + if (selectedImage != null) { + String uriStr = selectedImage.toString(); + String path = uriStr.substring(10, uriStr.length()); + if (path.startsWith("com.sec.android.gallery3d")) { + return null; + } + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + String picturePath = null; + Cursor cursor = context.getContentResolver().query(selectedImage, filePathColumn, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + picturePath = cursor.getString(columnIndex); + cursor.close(); + } + return picturePath; + } else { + return null; + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/KLog.java b/lib/src/main/java/com/razerdp/github/lib/utils/KLog.java new file mode 100644 index 0000000..0bf3e0c --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/KLog.java @@ -0,0 +1,48 @@ +package com.razerdp.github.lib.utils; + +/** + * Created by 大灯泡 on 2019/7/30. + */ +public class KLog { + + public static void i(Object... objects) { + + } + + + public static void i(String s, Object... objects) { + + } + + public static void d(Object... objects) { + + } + + public static void d(String s, Object... objects) { + + } + + public static void w(Object... objects) { + + } + + public static void w(String s, Object... objects) { + + } + + public static void e(Object... objects) { + + } + + public static void e(String s, Object... objects) { + + } + + public static void v(Object... objects) { + + } + + public static void v(String s, Object... objects) { + + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/KeyBoardUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/KeyBoardUtil.java new file mode 100644 index 0000000..52a0a0f --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/KeyBoardUtil.java @@ -0,0 +1,77 @@ +package com.razerdp.github.lib.utils; + +import android.app.Activity; +import android.content.Context; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + + +/** + * Created by 大灯泡 on 2014/10/11. + */ +public class KeyBoardUtil { + public static void open(final EditText primaryTextField) { + new WeakHandler().postDelayed(new Runnable() { + public void run() { + primaryTextField.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, 0, 0, 0)); + primaryTextField.dispatchTouchEvent(MotionEvent.obtain( + SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, 0, 0, 0)); + if (primaryTextField.getText() != null) { + String text = primaryTextField.getText().toString(); + if (!TextUtils.isEmpty(text)) { + primaryTextField.setSelection(text.length()); + } + } + } + }, 100); + } + + /** + * 隐藏软键盘 + */ + public static void close(Activity activity) { + if (activity == null) { + return; + } + View view = activity.getWindow().getDecorView().getRootView(); + try { + InputMethodManager imm = (InputMethodManager) view.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 隐藏软键盘 + */ + public static void close(View view) { + try { + InputMethodManager imm = (InputMethodManager) view.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void close(final View view, long delayMillis) { + new WeakHandler().postDelayed(new Runnable() { + @Override + public void run() { + close(view); + } + }, delayMillis); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/OSUtils.java b/lib/src/main/java/com/razerdp/github/lib/utils/OSUtils.java new file mode 100644 index 0000000..5824662 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/OSUtils.java @@ -0,0 +1,121 @@ +package com.razerdp.github.lib.utils; + +import android.os.Build; +import android.text.TextUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Created by 大灯泡 on 2019/1/16. + */ +public class OSUtils { + public static final String ROM_MIUI = "MIUI"; + public static final String ROM_EMUI = "EMUI"; + public static final String ROM_FLYME = "FLYME"; + public static final String ROM_OPPO = "OPPO"; + public static final String ROM_SMARTISAN = "SMARTISAN"; + public static final String ROM_VIVO = "VIVO"; + public static final String ROM_QIKU = "QIKU"; + + private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name"; + private static final String KEY_VERSION_EMUI = "ro.build.version.emui"; + private static final String KEY_VERSION_OPPO = "ro.build.version.opporom"; + private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version"; + private static final String KEY_VERSION_VIVO = "ro.vivo.os.version"; + + private static String sName; + private static String sVersion; + + public static boolean isEmui() { + return check(ROM_EMUI); + } + + public static boolean isMiui() { + return check(ROM_MIUI); + } + + public static boolean isVivo() { + return check(ROM_VIVO); + } + + public static boolean isOppo() { + return check(ROM_OPPO); + } + + public static boolean isFlyme() { + return check(ROM_FLYME); + } + + public static boolean is360() { + return check(ROM_QIKU) || check("360"); + } + + public static boolean isSmartisan() { + return check(ROM_SMARTISAN); + } + + public static String getName() { + if (sName == null) { + check(""); + } + return sName; + } + + public static String getVersion() { + if (sVersion == null) { + check(""); + } + return sVersion; + } + + public static boolean check(String rom) { + if (sName != null) { + return sName.equals(rom); + } + + if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) { + sName = ROM_MIUI; + } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) { + sName = ROM_EMUI; + } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) { + sName = ROM_OPPO; + } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) { + sName = ROM_VIVO; + } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) { + sName = ROM_SMARTISAN; + } else { + sVersion = Build.DISPLAY; + if (sVersion.toUpperCase().contains(ROM_FLYME)) { + sName = ROM_FLYME; + } else { + sVersion = Build.UNKNOWN; + sName = Build.MANUFACTURER.toUpperCase(); + } + } + return sName.equals(rom); + } + + public static String getProp(String name) { + String line = null; + BufferedReader input = null; + try { + Process p = Runtime.getRuntime().exec("getprop " + name); + input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024); + line = input.readLine(); + input.close(); + } catch (IOException ex) { + return null; + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return line; + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/SimpleObjectPool.java b/lib/src/main/java/com/razerdp/github/lib/utils/SimpleObjectPool.java new file mode 100644 index 0000000..14da3bc --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/SimpleObjectPool.java @@ -0,0 +1,49 @@ +package com.razerdp.github.lib.utils; + +import java.lang.reflect.Array; + +/** + * Created by 大灯泡 on 2017/05/5. + *

+ * 简单的对象池 + */ + +public class SimpleObjectPool { + + protected T[] objsPool; + protected int size; + protected int curPointer = -1; + + public SimpleObjectPool(Class type) { + this(type, 8); + } + + public SimpleObjectPool(Class type, int size) { + this.size = size; + objsPool = (T[]) Array.newInstance(type, size); + } + + public synchronized T get() { + if (curPointer == -1 || curPointer > objsPool.length) return null; + T obj = objsPool[curPointer]; + objsPool[curPointer] = null; + curPointer--; + return obj; + } + + public synchronized boolean put(T t) { + if (curPointer == -1 || curPointer < objsPool.length - 1) { + curPointer++; + objsPool[curPointer] = t; + return true; + } + return false; + } + + public void clearPool() { + for (int i = 0; i < objsPool.length; i++) { + objsPool[i] = null; + } + curPointer = -1; + } +} \ No newline at end of file diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/StringUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/StringUtil.java new file mode 100644 index 0000000..2588660 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/StringUtil.java @@ -0,0 +1,55 @@ +package com.razerdp.github.lib.utils; + +import android.text.TextUtils; + +import com.razerdp.github.lib.api.AppContext; + +import java.util.Locale; + + +/** + * Created by 大灯泡 on 2016/10/28. + *

+ * 字符串工具类 + */ + +public class StringUtil { + + public static boolean noEmpty(String originStr) { + return !TextUtils.isEmpty(originStr); + } + + + public static boolean noEmpty(String... originStr) { + boolean noEmpty = true; + for (String s : originStr) { + if (TextUtils.isEmpty(s)) { + noEmpty = false; + break; + } + } + return noEmpty; + } + + /** + * 从资源文件拿到文字 + */ + public static String getResourceString(int strId) { + String result = ""; + if (strId > 0) { + result = AppContext.getResources().getString(strId); + } + return result; + } + + /** + * 从资源文件得到文字并format + */ + public static String getResourceStringAndFormat(int strId, Object... objs) { + String result = ""; + if (strId > 0) { + result = String.format(Locale.getDefault(), AppContext.getResources().getString(strId), objs); + } + return result; + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/TimeUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/TimeUtil.java new file mode 100644 index 0000000..75592d5 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/TimeUtil.java @@ -0,0 +1,337 @@ +package com.razerdp.github.lib.utils; + +import android.text.TextUtils; +import android.util.Log; + +import com.razerdp.github.lib.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import androidx.annotation.StringDef; + +/** + * Created by 大灯泡 on 2016/11/1. + *

+ * 时间工具 + */ + +public class TimeUtil { + + @Retention(RetentionPolicy.SOURCE) + @StringDef({YYYYMMDD, YYYYMM, HHMM, YYYYMD, YYYYMMDDHHMM, YYYYMMDDHHMMSS, YYYYMMDDHHMM_EX, MMDDHHMM_CHINESE_EX, YYYYMMLNDDHHMM, MMDDHHMM_CHINESE, YYYYMMDD_SLASH, YYYYMM_SLASH}) + public @interface FormateType { + } + + public final static String YYYYMMDD = "yyyy-MM-dd"; + public final static String YYYYMMDDHHMM_EX = "yyyy_MM_dd_hh_mm"; + public final static String YYYYMD = "yyyy-M-d"; + public final static String YYYYMM = "yyyy-MM"; + public final static String HHMM = "hh:mm"; + public final static String YYYYMMDDHHMM = "yyyy-MM-dd hh:mm"; + public final static String YYYYMMLNDDHHMM = "yyyy-MM-dd\nhh:mm"; + public final static String YYYYMMDDHHMMSS = "yyyy-MM-dd hh:mm:ss"; + public final static String MMDDHHMM_CHINESE = "MM月dd日_hh时mm分"; + public final static String MMDDHHMM_CHINESE_EX = "MM月dd日_hh_mm"; + public final static String YYYYMMDD_SLASH = "yyyy/MM/dd"; + public final static String YYYYMM_SLASH = "yyyy/MM"; + + private static SimpleDateFormat sdf = new SimpleDateFormat(); + public static final int YEAR = 365 * 24 * 60 * 60;// 年 + public static final int MONTH = 30 * 24 * 60 * 60;// 月 + public static final int DAY = 24 * 60 * 60;// 天 + public static final int HOUR = 60 * 60;// 小时 + public static final int MINUTE = 60;// 分钟 + + public static String getTimeString(long timestamp) { + long currentTime = System.currentTimeMillis(); + long timeGap = (currentTime - timestamp) / 1000;// 与现在时间相差秒数 + String timeStr = null; + if (timeGap > YEAR) { + timeStr = formatTimeFromResource(R.string.format_time_year, (int) (timeGap / YEAR)); + } else if (timeGap > MONTH) { + timeStr = formatTimeFromResource(R.string.format_time_month, (int) (timeGap / MONTH)); + + } else if (timeGap > DAY) {// 1天以上 + timeStr = formatTimeFromResource(R.string.format_time_day, (int) (timeGap / DAY)); + } else if (timeGap > HOUR) {// 1小时-24小时 + timeStr = formatTimeFromResource(R.string.format_time_hour, (int) (timeGap / HOUR)); + } else if (timeGap > MINUTE) {// 1分钟-59分钟 + timeStr = formatTimeFromResource(R.string.format_time_minute, (int) (timeGap / MINUTE)); + } else {// 1秒钟-59秒钟 + timeStr = StringUtil.getResourceString(R.string.format_time_sec); + } + return timeStr; + } + + public static String getTimeStringFromBmob(String time) { + //格式:2016-10-28 18:48:23 + try { + Date date = dataFormate.parse(time); + return getTimeString(date.getTime()); + } catch (ParseException e) { + e.printStackTrace(); + return getTimeString(System.currentTimeMillis()); + } + } + + private static String formatTimeFromResource(int resource, int time) { + return StringUtil.getResourceStringAndFormat(resource, time); + } + + + private static SimpleDateFormat dataFormate = new SimpleDateFormat(YYYYMMDDHHMMSS, Locale.getDefault()); + private static SimpleDateFormat yyyymmddFormate = new SimpleDateFormat(YYYYMMDD, Locale.getDefault()); + + /** + * 获取未来 第 past 天的日期 + * + * @param past + * @return + */ + public static String getFetureDate(int past) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) + past); + Date today = calendar.getTime(); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + String result = format.format(today); + Log.e(null, result); + return result; + } + + /** + * 获取未来 第 past 天的日期 + * + * @param past + * @return + */ + public static String getFetureDate(int past, String dateFormat) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) + past); + Date today = calendar.getTime(); + SimpleDateFormat format = new SimpleDateFormat(dateFormat); + String result = format.format(today); + return result; + } + + public static String getWhichDay(int past) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) + past); + Date today = calendar.getTime(); + SimpleDateFormat format = new SimpleDateFormat("EEEE"); + String result = format.format(today); + return result; + } + + public static String formatDate(String date, String dateFormat) throws ParseException { + SimpleDateFormat inputSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date dateTime = inputSdf.parse(date); + SimpleDateFormat ouputSdf = new SimpleDateFormat(dateFormat); + return ouputSdf.format(dateTime); + } + + /** + * 获取未来 第 past 天的日期 + * + * @param date 20170412 + * @return 04-12 + */ + public static String formatDate(String date) { + if (!TextUtils.isEmpty(date) && date.length() == 8) { + return new StringBuilder(date.substring(4)).insert(2, "-").toString(); + } + return date; + } + + /** + * 时间戳转格式化日期 + * + * @param timestamp 单位毫秒 + * @param format 日期格式 + * @return + */ + public static String longToTimeStr(long timestamp, @FormateType String format) { + return transferLongToDate(timestamp, format); + } + + /** + * 时间戳转格式化日期 + * + * @param timestamp 单位毫秒 + * @param format 日期格式 + * @return + */ + private static String transferLongToDate(long timestamp, String format) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(format); + Date date = new Date(timestamp); + return sdf.format(date); + } catch (Exception e) { + KLog.e(e); + return "null"; + } + } + + public static String dateToString(Date date, String format) { + SimpleDateFormat sdf = new SimpleDateFormat(format); + return sdf.format(date); + } + + /** + * 格式化日期转时间戳 + * + * @param format 日期格式 + * @return + */ + public static long strToTimestamp(String date, String format) { + long timestamp = 0; + try { + timestamp = new SimpleDateFormat(format).parse(date).getTime() / 1000; + + } catch (ParseException e) { + e.printStackTrace(); + } + return timestamp; + } + + /** + * 比较timeTwo是否比TimeOne晚超过distanceMinute分钟 + * + * @param TimeOne 单位 unixtime + * @param timeTwo 单位 unixtime + * @param distanceMinute 单位分钟 + * @return + */ + public static boolean isGreaterThan(long TimeOne, long timeTwo, int distanceMinute) { + long timeDifference = timeTwo - TimeOne;// 两者相距的秒数 + long oneMinute = 60; + if (timeDifference > distanceMinute * oneMinute) { + return true; + } else { + return false; + } + } + + public static String transferDateFromate(String originalDate, @FormateType String oldFormate, @FormateType String newFormate) { + long time = stringToTimeStamp(originalDate, oldFormate); + return transferDateFromate(time, newFormate); + } + + public static String transferDateFromate(long originalDate, @FormateType String newFormate) { + return longToTimeStr(originalDate, newFormate); + } + + + public static String formatLongToTimeStr(Long l) { + String str = ""; + int hour = 0; + int minute = 0; + int second = 0; + second = l.intValue() / 1000; + if (second > 60) { + minute = second / 60; + second = second % 60; + } + + if (minute > 60) { + hour = minute / 60; + minute = minute % 60; + } + + String strtime = ""; + if (hour != 0) { + strtime = hour + ":" + minute + ":" + second; + } else { + strtime = minute + ":" + (second >= 10 ? second : "0" + second); + } + return strtime; + } + + public static long stringToTimeStamp(String timeString, @FormateType String format) { + SimpleDateFormat sdf = new SimpleDateFormat(format); + try { + return sdf.parse(timeString).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + return 0; + } catch (NullPointerException e) { + e.printStackTrace(); + return 0; + } + } + + public static long stringToTimeStamp(String timeString, @FormateType String format, TimeZone timeZone) { + SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.ENGLISH); + sdf.setTimeZone(timeZone); + try { + return sdf.parse(timeString).getTime(); + } catch (ParseException e) { + e.printStackTrace(); + return 0; + } catch (NullPointerException e) { + e.printStackTrace(); + return 0; + } + } + + public static boolean isToday(long timeInMilis) { + Calendar time = Calendar.getInstance(); + time.setTimeInMillis(timeInMilis); + Calendar now = Calendar.getInstance(); + if (now.get(Calendar.DATE) == time.get(Calendar.DATE)) { + return true; + } else { + return false; + } + } + + public static int getSubDay(long start, long end) { + int result = Math.round((end - start) / (1000 * DAY)); + return result < 0 ? -1 : result; + } + + + + public static String getWeek(String pTime) { + String week = ""; + Calendar c = Calendar.getInstance(); + try { + c.setTime(yyyymmddFormate.parse(pTime)); + } catch (ParseException e) { + e.printStackTrace(); + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) { + week = "周日"; + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY) { + week = "周一"; + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.TUESDAY) { + week = "周二"; + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.WEDNESDAY) { + week = "周三"; + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.THURSDAY) { + week = "周四"; + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) { + week = "周五"; + } + if (c.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) { + week = "周六"; + } + return week; + } + + public static long getNextTime(long curTime, long time) { + return ((curTime / 1000) + time) * 1000; + } + +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/ToolUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/ToolUtil.java new file mode 100644 index 0000000..0402ee5 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/ToolUtil.java @@ -0,0 +1,76 @@ +package com.razerdp.github.lib.utils; + +import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.widget.Toast; + +import com.razerdp.github.lib.api.AppContext; + +import java.io.File; +import java.util.List; + +import androidx.core.content.FileProvider; + +/** + * Created by 大灯泡 on 2016/10/27. + */ + +public class ToolUtil { + + public static boolean isListEmpty(List datas) { + return datas == null || datas.size() <= 0; + } + + //复制到剪切板 + @SuppressLint({"NewApi", "ServiceCast"}) + public static void copyToClipboard(String szContent) { + Context context = AppContext.getAppContext(); + + String sourceText = szContent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setText(sourceText); + } else { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Copied Text", sourceText); + clipboard.setPrimaryClip(clip); + } + } + + //复制到剪切板 + @SuppressLint({"NewApi", "ServiceCast"}) + public static void copyToClipboardAndToast(Context context, String szContent) { + + String sourceText = szContent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setText(sourceText); + Toast.makeText(context, "copy success", Toast.LENGTH_SHORT).show(); + } else { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Copied Text", sourceText); + clipboard.setPrimaryClip(clip); + Toast.makeText(context, "copy success", Toast.LENGTH_SHORT).show(); + } + } + + public static void install(Context context, String apkPath) { + Intent intent = new Intent(Intent.ACTION_VIEW); + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { + File file = (new File(apkPath)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri apkUri = FileProvider.getUriForFile(context, "github.razerdp.friendcircle", file); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); + } else { + intent.setDataAndType(Uri.fromFile(new File(apkPath)), + "application/vnd.android.package-archive"); + } + context.startActivity(intent); + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/VersionUtil.java b/lib/src/main/java/com/razerdp/github/lib/utils/VersionUtil.java new file mode 100644 index 0000000..bae3391 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/VersionUtil.java @@ -0,0 +1,41 @@ +package com.razerdp.github.lib.utils; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import com.razerdp.github.lib.api.AppContext; + + +public class VersionUtil { + + public static String getAppVersionName() { + try { + String versionName = ""; + Context context = AppContext.getAppContext(); + PackageManager packageManager = context.getPackageManager(); + String packageName = context.getPackageName(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_CONFIGURATIONS); + versionName = packageInfo.versionName; + return versionName; + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + public static int getAppVersionCode() { + try { + Context context = AppContext.getAppContext(); + PackageManager packageManager = context.getPackageManager(); + String packageName = context.getPackageName(); + int versionCode = 0; + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_CONFIGURATIONS); + versionCode = packageInfo.versionCode; + return versionCode; + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } +} diff --git a/lib/src/main/java/com/razerdp/github/lib/utils/WeakHandler.java b/lib/src/main/java/com/razerdp/github/lib/utils/WeakHandler.java new file mode 100644 index 0000000..9ec89d1 --- /dev/null +++ b/lib/src/main/java/com/razerdp/github/lib/utils/WeakHandler.java @@ -0,0 +1,503 @@ +/* + * Copyright (c) 2014 Badoo Trading Limited + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Portions of documentation in this code are modifications based on work created and + * shared by Android Open Source Project and used according to terms described in the + * Apache License, Version 2.0 + */ +package com.razerdp.github.lib.utils; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.lang.ref.WeakReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Memory safer implementation of android.os.Handler + *

+ * Original implementation of Handlers always keeps hard reference to handler in queue of execution. + * If you create anonymous handler and post delayed message into it, it will keep all parent class + * for that time in memory even if it could be cleaned. + *

+ * This implementation is trickier, it will keep WeakReferences to runnables and messages, + * and GC could collect them once WeakHandler instance is not referenced any more + *

+ * + * @see Handler + *

+ * Created by Dmytro Voronkevych on 17/06/2014. + *

+ *

+ * git: + * https://github.com/badoo/android-weak-handler/blob/master/src/main/java/com/badoo/mobile/util/WeakHandler.java + */ +@SuppressWarnings("unused") +public class WeakHandler { + private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory + private final ExecHandler mExec; + private Lock mLock = new ReentrantLock(); + @SuppressWarnings("ConstantConditions") + final ChainedRef mRunnables = new ChainedRef(mLock, null); + + /** + * Default constructor associates this handler with the {@link Looper} for the + * current thread. + *

+ * If this thread does not have a looper, this handler won't be able to receive messages + * so an exception is thrown. + */ + public WeakHandler() { + mCallback = null; + mExec = new ExecHandler(); + } + + /** + * Constructor associates this handler with the {@link Looper} for the + * current thread and takes a callback interface in which you can handle + * messages. + *

+ * If this thread does not have a looper, this handler won't be able to receive messages + * so an exception is thrown. + * + * @param callback The callback interface in which to handle messages, or null. + */ + public WeakHandler(Handler.Callback callback) { + mCallback = callback; // Hard referencing body + mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler + } + + /** + * Use the provided {@link Looper} instead of the default one. + * + * @param looper The looper, must not be null. + */ + public WeakHandler(Looper looper) { + mCallback = null; + mExec = new ExecHandler(looper); + } + + /** + * Use the provided {@link Looper} instead of the default one and take a callback + * interface in which to handle messages. + * + * @param looper The looper, must not be null. + * @param callback The callback interface in which to handle messages, or null. + */ + public WeakHandler(Looper looper, Handler.Callback callback) { + mCallback = callback; + mExec = new ExecHandler(looper, new WeakReference<>(callback)); + } + + /** + * Causes the Runnable r to be added to the message queue. + * The runnable will be run on the thread to which this handler is + * attached. + * + * @param r The Runnable that will be executed. + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean post(Runnable r) { + return mExec.post(wrapRunnable(r)); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * at a specific time given by uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * The runnable will be run on the thread to which this handler is attached. + * + * @param r The Runnable that will be executed. + * @param uptimeMillis The absolute time at which the callback should run, + * using the {@link android.os.SystemClock#uptimeMillis} time-base. + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean postAtTime(Runnable r, long uptimeMillis) { + return mExec.postAtTime(wrapRunnable(r), uptimeMillis); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * at a specific time given by uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * The runnable will be run on the thread to which this handler is attached. + * + * @param r The Runnable that will be executed. + * @param uptimeMillis The absolute time at which the callback should run, + * using the {@link android.os.SystemClock#uptimeMillis} time-base. + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + * @see android.os.SystemClock#uptimeMillis + */ + public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { + return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * after the specified amount of time elapses. + * The runnable will be run on the thread to which this handler + * is attached. + * + * @param r The Runnable that will be executed. + * @param delayMillis The delay (in milliseconds) until the Runnable + * will be executed. + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- + * if the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean postDelayed(Runnable r, long delayMillis) { + return mExec.postDelayed(wrapRunnable(r), delayMillis); + } + + /** + * Posts a message to an object that implements Runnable. + * Causes the Runnable r to executed on the next iteration through the + * message queue. The runnable will be run on the thread to which this + * handler is attached. + * This method is only for use in very special circumstances -- it + * can easily starve the message queue, cause ordering problems, or have + * other unexpected side-effects. + * + * @param r The Runnable that will be executed. + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean postAtFrontOfQueue(Runnable r) { + return mExec.postAtFrontOfQueue(wrapRunnable(r)); + } + + /** + * Remove any pending posts of Runnable r that are in the message queue. + */ + public final void removeCallbacks(Runnable r) { + final WeakRunnable runnable = mRunnables.remove(r); + if (runnable != null) { + mExec.removeCallbacks(runnable); + } + } + + /** + * Remove any pending posts of Runnable r with Object + * token that are in the message queue. If token is null, + * all callbacks will be removed. + */ + public final void removeCallbacks(Runnable r, Object token) { + final WeakRunnable runnable = mRunnables.remove(r); + if (runnable != null) { + mExec.removeCallbacks(runnable, token); + } + } + + /** + * Pushes a message onto the end of the message queue after all pending messages + * before the current time. It will be received in callback, + * in the thread attached to this handler. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendMessage(Message msg) { + return mExec.sendMessage(msg); + } + + /** + * Sends a Message containing only the what value. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendEmptyMessage(int what) { + return mExec.sendEmptyMessage(what); + } + + /** + * Sends a Message containing only the what value, to be delivered + * after the specified amount of time elapses. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + * @see #sendMessageDelayed(Message, long) + */ + public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { + return mExec.sendEmptyMessageDelayed(what, delayMillis); + } + + /** + * Sends a Message containing only the what value, to be delivered + * at a specific time. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + * @see #sendMessageAtTime(Message, long) + */ + public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { + return mExec.sendEmptyMessageAtTime(what, uptimeMillis); + } + + /** + * Enqueue a message into the message queue after all pending messages + * before (current time + delayMillis). You will receive it in + * callback, in the thread attached to this handler. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the message will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean sendMessageDelayed(Message msg, long delayMillis) { + return mExec.sendMessageDelayed(msg, delayMillis); + } + + /** + * Enqueue a message into the message queue after all pending messages + * before the absolute time (in milliseconds) uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * You will receive it in callback, in the thread attached + * to this handler. + * + * @param uptimeMillis The absolute time at which the message should be + * delivered, using the + * {@link android.os.SystemClock#uptimeMillis} time-base. + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the message will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + return mExec.sendMessageAtTime(msg, uptimeMillis); + } + + /** + * Enqueue a message at the front of the message queue, to be processed on + * the next iteration of the message loop. You will receive it in + * callback, in the thread attached to this handler. + * This method is only for use in very special circumstances -- it + * can easily starve the message queue, cause ordering problems, or have + * other unexpected side-effects. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendMessageAtFrontOfQueue(Message msg) { + return mExec.sendMessageAtFrontOfQueue(msg); + } + + /** + * Remove any pending posts of messages with code 'what' that are in the + * message queue. + */ + public final void removeMessages(int what) { + mExec.removeMessages(what); + } + + /** + * Remove any pending posts of messages with code 'what' and whose obj is + * 'object' that are in the message queue. If object is null, + * all messages will be removed. + */ + public final void removeMessages(int what, Object object) { + mExec.removeMessages(what, object); + } + + /** + * Remove any pending posts of callbacks and sent messages whose + * obj is token. If token is null, + * all callbacks and messages will be removed. + */ + public final void removeCallbacksAndMessages(Object token) { + mExec.removeCallbacksAndMessages(token); + } + + /** + * Check if there are any pending posts of messages with code 'what' in + * the message queue. + */ + public final boolean hasMessages(int what) { + return mExec.hasMessages(what); + } + + /** + * Check if there are any pending posts of messages with code 'what' and + * whose obj is 'object' in the message queue. + */ + public final boolean hasMessages(int what, Object object) { + return mExec.hasMessages(what, object); + } + + public final Looper getLooper() { + return mExec.getLooper(); + } + + private WeakRunnable wrapRunnable(Runnable r) { + //noinspection ConstantConditions + if (r == null) { + throw new NullPointerException("Runnable can't be null"); + } + final ChainedRef hardRef = new ChainedRef(mLock, r); + mRunnables.insertAfter(hardRef); + return hardRef.wrapper; + } + + private static class ExecHandler extends Handler { + private final WeakReference mCallback; + + ExecHandler() { + mCallback = null; + } + + ExecHandler(WeakReference callback) { + mCallback = callback; + } + + ExecHandler(Looper looper) { + super(looper); + mCallback = null; + } + + ExecHandler(Looper looper, WeakReference callback) { + super(looper); + mCallback = callback; + } + + @Override + public void handleMessage(Message msg) { + if (mCallback == null) { + return; + } + final Callback callback = mCallback.get(); + if (callback == null) { // Already disposed + return; + } + callback.handleMessage(msg); + } + } + + static class WeakRunnable implements Runnable { + private final WeakReference mDelegate; + private final WeakReference mReference; + + WeakRunnable(WeakReference delegate, WeakReference reference) { + mDelegate = delegate; + mReference = reference; + } + + @Override + public void run() { + final Runnable delegate = mDelegate.get(); + final ChainedRef reference = mReference.get(); + if (reference != null) { + reference.remove(); + } + if (delegate != null) { + delegate.run(); + } + } + } + + static class ChainedRef { + ChainedRef next; + ChainedRef prev; + final Runnable runnable; + final WeakRunnable wrapper; + + Lock lock; + + public ChainedRef(Lock lock, Runnable r) { + this.runnable = r; + this.lock = lock; + this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this)); + } + + public WeakRunnable remove() { + lock.lock(); + try { + if (prev != null) { + prev.next = next; + } + if (next != null) { + next.prev = prev; + } + prev = null; + next = null; + } finally { + lock.unlock(); + } + return wrapper; + } + + public void insertAfter(ChainedRef candidate) { + lock.lock(); + try { + if (this.next != null) { + this.next.prev = candidate; + } + + candidate.next = this.next; + this.next = candidate; + candidate.prev = this; + } finally { + lock.unlock(); + } + } + + public WeakRunnable remove(Runnable obj) { + lock.lock(); + try { + ChainedRef curr = this.next; // Skipping head + while (curr != null) { + if (curr.runnable == obj) { // We do comparison exactly how Handler does inside + return curr.remove(); + } + curr = curr.next; + } + } finally { + lock.unlock(); + } + return null; + } + } +} diff --git a/lib/src/main/res/values/colors.xml b/lib/src/main/res/values/colors.xml new file mode 100644 index 0000000..0d2c4cc --- /dev/null +++ b/lib/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/src/main/res/values/strings.xml b/lib/src/main/res/values/strings.xml new file mode 100644 index 0000000..82181a4 --- /dev/null +++ b/lib/src/main/res/values/strings.xml @@ -0,0 +1,35 @@ + + BaseLibrary + + + %s年前 + %s个月前 + %s天前 + %s小时前 + %s分钟前 + 刚刚 + + + + + @string/permission_recode_audio_hint + @string/permission_get_accounts_hint + @string/permission_read_phone_hint + @string/permission_call_phone_hint + @string/permission_camera_hint + @string/permission_access_fine_location_hint + @string/permission_access_coarse_location_hint + @string/permission_read_external_hint + @string/permission_white_external_hint + + + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_GET_ACCOUNTS】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_READ_PHONE_STATE】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_CALL_PHONE】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_CAMERA】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_ACCESS_FINE_LOCATION】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_ACCESS_COARSE_LOCATION】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_READ_EXTERNAL_STORAGE】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_WRITE_EXTERNAL_STORAGE】 + 没有此权限,无法开启这个功能。\n请手动开启权限:\n【PERMISSION_RECORD_AUDIO】 + diff --git a/lib/src/test/java/com/razerdp/github/lib/ExampleUnitTest.java b/lib/src/test/java/com/razerdp/github/lib/ExampleUnitTest.java new file mode 100644 index 0000000..133ac80 --- /dev/null +++ b/lib/src/test/java/com/razerdp/github/lib/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.github.lib; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/module_main/.gitignore b/module_main/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/module_main/.gitignore @@ -0,0 +1 @@ +/build diff --git a/module_main/build.gradle b/module_main/build.gradle new file mode 100644 index 0000000..a3bf0ae --- /dev/null +++ b/module_main/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'com.android.library' + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar']) + api project(':router') +} diff --git a/module_main/proguard-rules.pro b/module_main/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/module_main/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 diff --git a/module_main/src/androidTest/java/com/razerdp/github/module/main/ExampleInstrumentedTest.java b/module_main/src/androidTest/java/com/razerdp/github/module/main/ExampleInstrumentedTest.java new file mode 100644 index 0000000..2c92774 --- /dev/null +++ b/module_main/src/androidTest/java/com/razerdp/github/module/main/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.github.module.main; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.github.module.main.test", appContext.getPackageName()); + } +} diff --git a/module_main/src/main/AndroidManifest.xml b/module_main/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8b4156d --- /dev/null +++ b/module_main/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/module_main/src/main/res/values/strings.xml b/module_main/src/main/res/values/strings.xml new file mode 100644 index 0000000..3ab34a7 --- /dev/null +++ b/module_main/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + module_main + diff --git a/module_main/src/test/java/com/razerdp/github/module/main/ExampleUnitTest.java b/module_main/src/test/java/com/razerdp/github/module/main/ExampleUnitTest.java new file mode 100644 index 0000000..7ac8f60 --- /dev/null +++ b/module_main/src/test/java/com/razerdp/github/module/main/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.github.module.main; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/module_main_impl/.gitignore b/module_main_impl/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/module_main_impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/module_main_impl/build.gradle b/module_main_impl/build.gradle new file mode 100644 index 0000000..472c28c --- /dev/null +++ b/module_main_impl/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'com.android.application' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':module_main') +} diff --git a/module_main_impl/proguard-rules.pro b/module_main_impl/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/module_main_impl/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 diff --git a/module_main_impl/src/androidTest/java/com/razerdp/module/impl/main/ExampleInstrumentedTest.java b/module_main_impl/src/androidTest/java/com/razerdp/module/impl/main/ExampleInstrumentedTest.java new file mode 100644 index 0000000..b995c60 --- /dev/null +++ b/module_main_impl/src/androidTest/java/com/razerdp/module/impl/main/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.module.impl.main; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.module.impl.main.test", appContext.getPackageName()); + } +} diff --git a/module_main_impl/src/main/AndroidManifest.xml b/module_main_impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7c22052 --- /dev/null +++ b/module_main_impl/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module_main_impl/src/main/java/com/razerdp/module/impl/main/MainApplication.java b/module_main_impl/src/main/java/com/razerdp/module/impl/main/MainApplication.java new file mode 100644 index 0000000..98c86f0 --- /dev/null +++ b/module_main_impl/src/main/java/com/razerdp/module/impl/main/MainApplication.java @@ -0,0 +1,10 @@ +package com.razerdp.module.impl.main; + + +import com.razerdp.github.common.modules.base.BaseModuleApplication; + +/** + * Created by 大灯泡 on 2019/7/30. + */ +public class MainApplication extends BaseModuleApplication { +} diff --git a/module_main_impl/src/main/java/com/razerdp/module/impl/main/ui/MainActivity.java b/module_main_impl/src/main/java/com/razerdp/module/impl/main/ui/MainActivity.java new file mode 100644 index 0000000..e7ef52a --- /dev/null +++ b/module_main_impl/src/main/java/com/razerdp/module/impl/main/ui/MainActivity.java @@ -0,0 +1,16 @@ +package com.razerdp.module.impl.main.ui; + +import android.os.Bundle; + +import com.razerdp.module.impl.main.R; + +import androidx.appcompat.app.AppCompatActivity; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } +} diff --git a/module_main_impl/src/main/res/layout/activity_main.xml b/module_main_impl/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..8cf1fed --- /dev/null +++ b/module_main_impl/src/main/res/layout/activity_main.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/module_main_impl/src/main/res/values/strings.xml b/module_main_impl/src/main/res/values/strings.xml new file mode 100644 index 0000000..f419879 --- /dev/null +++ b/module_main_impl/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + module_main_impl + diff --git a/module_main_impl/src/test/java/com/razerdp/module/impl/main/ExampleUnitTest.java b/module_main_impl/src/test/java/com/razerdp/module/impl/main/ExampleUnitTest.java new file mode 100644 index 0000000..b75da38 --- /dev/null +++ b/module_main_impl/src/test/java/com/razerdp/module/impl/main/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.module.impl.main; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/network/.gitignore b/network/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/network/.gitignore @@ -0,0 +1 @@ +/build diff --git a/network/build.gradle b/network/build.gradle new file mode 100644 index 0000000..1e03b18 --- /dev/null +++ b/network/build.gradle @@ -0,0 +1,5 @@ +apply plugin: 'com.android.library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/network/proguard-rules.pro b/network/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/network/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 diff --git a/network/src/androidTest/java/com/razerdp/github/network/ExampleInstrumentedTest.java b/network/src/androidTest/java/com/razerdp/github/network/ExampleInstrumentedTest.java new file mode 100644 index 0000000..eeea6d1 --- /dev/null +++ b/network/src/androidTest/java/com/razerdp/github/network/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.github.network; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.github.network.test", appContext.getPackageName()); + } +} diff --git a/network/src/main/AndroidManifest.xml b/network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..362f9a2 --- /dev/null +++ b/network/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/network/src/main/res/values/strings.xml b/network/src/main/res/values/strings.xml new file mode 100644 index 0000000..3259206 --- /dev/null +++ b/network/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + network + diff --git a/network/src/test/java/com/razerdp/github/network/ExampleUnitTest.java b/network/src/test/java/com/razerdp/github/network/ExampleUnitTest.java new file mode 100644 index 0000000..c4fc290 --- /dev/null +++ b/network/src/test/java/com/razerdp/github/network/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.github.network; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/router/.gitignore b/router/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/router/.gitignore @@ -0,0 +1 @@ +/build diff --git a/router/build.gradle b/router/build.gradle new file mode 100644 index 0000000..a33baab --- /dev/null +++ b/router/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'com.android.library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + api project(':common') +} diff --git a/router/proguard-rules.pro b/router/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/router/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 diff --git a/router/src/androidTest/java/com/razerdp/github/router/ExampleInstrumentedTest.java b/router/src/androidTest/java/com/razerdp/github/router/ExampleInstrumentedTest.java new file mode 100644 index 0000000..1c232d3 --- /dev/null +++ b/router/src/androidTest/java/com/razerdp/github/router/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.github.router; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.github.router.test", appContext.getPackageName()); + } +} diff --git a/router/src/main/AndroidManifest.xml b/router/src/main/AndroidManifest.xml new file mode 100644 index 0000000..23e662e --- /dev/null +++ b/router/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/router/src/main/res/values/strings.xml b/router/src/main/res/values/strings.xml new file mode 100644 index 0000000..9287916 --- /dev/null +++ b/router/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + router + diff --git a/router/src/test/java/com/razerdp/github/router/ExampleUnitTest.java b/router/src/test/java/com/razerdp/github/router/ExampleUnitTest.java new file mode 100644 index 0000000..b7061bf --- /dev/null +++ b/router/src/test/java/com/razerdp/github/router/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.github.router; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a2d1445..c1d9689 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,11 @@ -include ':app', ':circle_base_library', ':circle_base_ui', ':circle_photoview', ':circle_photoselect', ':circle_publish', ':circle_common' +include ':app' +include ':network' +include ':lib' +include ':uilib' + +include ':common' +include ':router' +//modules +include ':module_main' +//modules - impl(private) +include ':module_main_impl' diff --git a/uilib/.gitignore b/uilib/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/uilib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/uilib/build.gradle b/uilib/build.gradle new file mode 100644 index 0000000..c7b106d --- /dev/null +++ b/uilib/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'com.android.library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + api project(':lib') + +} diff --git a/uilib/proguard-rules.pro b/uilib/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/uilib/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 diff --git a/uilib/src/androidTest/java/com/razerdp/github/uilib/ExampleInstrumentedTest.java b/uilib/src/androidTest/java/com/razerdp/github/uilib/ExampleInstrumentedTest.java new file mode 100644 index 0000000..35f0bd8 --- /dev/null +++ b/uilib/src/androidTest/java/com/razerdp/github/uilib/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.razerdp.github.uilib; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.razerdp.github.uilib.test", appContext.getPackageName()); + } +} diff --git a/uilib/src/main/AndroidManifest.xml b/uilib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4a8636d --- /dev/null +++ b/uilib/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/uilib/src/main/res/values/colors.xml b/uilib/src/main/res/values/colors.xml new file mode 100644 index 0000000..9bc7e41 --- /dev/null +++ b/uilib/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #232426 + #232426 + #10d21c + + #00000000 + + \ No newline at end of file diff --git a/uilib/src/main/res/values/strings.xml b/uilib/src/main/res/values/strings.xml new file mode 100644 index 0000000..bdbef66 --- /dev/null +++ b/uilib/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + uilib + diff --git a/uilib/src/test/java/com/razerdp/github/uilib/ExampleUnitTest.java b/uilib/src/test/java/com/razerdp/github/uilib/ExampleUnitTest.java new file mode 100644 index 0000000..f8d953a --- /dev/null +++ b/uilib/src/test/java/com/razerdp/github/uilib/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.razerdp.github.uilib; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file