From 1d738befab5c70f9fc07ffe6aaf0f70e663b101d Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 19 May 2019 23:19:29 +0800 Subject: [PATCH 001/100] Convert code to Kotlin --- android_project_common.gradle | 10 +- .../me/ycdev/android/arch/ArchConstants.java | 28 -- .../me/ycdev/android/arch/ArchConstants.kt | 21 + .../arch/activity/AppCompatBaseActivity.java | 26 -- .../arch/activity/AppCompatBaseActivity.kt | 22 ++ .../android/arch/activity/BaseActivity.java | 10 - .../android/arch/activity/BaseActivity.kt | 8 + .../arch/activity/PreferenceBaseActivity.java | 11 - .../arch/activity/PreferenceBaseActivity.kt | 9 + .../ycdev/android/arch/utils/AppLogger.java | 77 ---- .../android/arch/wrapper/ToastHelper.java | 29 -- .../ycdev/android/arch/wrapper/ToastHelper.kt | 20 + archLintRulesAAR/build.gradle | 56 --- archLintRulesAAR/proguard-rules.pro | 17 - .../arch/lint/aar/ApplicationTest.java | 13 - archLintRulesAAR/src/main/AndroidManifest.xml | 4 - archLintRulesTestDemo/build.gradle | 5 +- .../arch/demo/activity/LintGood2Activity.java | 12 - .../arch/demo/activity/LintGood2Activity.kt | 11 + .../arch/demo/activity/LintGood3Activity.java | 5 - .../arch/demo/activity/LintGood3Activity.kt | 4 + .../arch/demo/activity/LintGoodActivity.java | 32 -- .../arch/demo/activity/LintGoodActivity.kt | 28 ++ .../demo/activity/LintViolation2Activity.java | 12 - .../demo/activity/LintViolation2Activity.kt | 11 + .../demo/activity/LintViolationActivity.java | 52 --- .../demo/activity/LintViolationActivity.kt | 49 +++ .../demo/wrapper/BroadcastHelperLintCase.java | 58 --- .../demo/wrapper/BroadcastHelperLintCase.kt | 66 ++++ .../demo/wrapper/IntentHelperLintCase.java | 48 --- .../arch/demo/wrapper/IntentHelperLintCase.kt | 48 +++ .../demo/wrapper/ToastHelperLintCase.java | 40 -- .../arch/demo/wrapper/ToastHelperLintCase.kt | 40 ++ .../lib/common/async/AsyncTaskQueueTest.kt | 12 +- .../lib/common/async/TaskSchedulerTest.kt | 38 +- .../demo/service/LocalServiceConnector.kt | 4 +- .../demo/service/RemoteServiceConnector.kt | 4 +- .../android/app/ActivityManagerIATest.kt | 2 +- .../android/os/PowerManagerIATest.kt | 10 +- .../internalapi/android/os/ProcessIATest.kt | 8 +- .../android/os/ServiceManagerIATest.kt | 8 +- .../android/os/UserHandleIATest.kt | 2 +- .../lib/common/ipc/ServiceClientBaseTest.kt | 59 +-- .../lib/common/ipc/ServiceConnectorTest.kt | 131 ++++--- .../lib/common/net/NetworkUtilsTest.kt | 14 +- .../lib/common/annotation/GuardedBy.java | 20 - .../lib/common/annotation/Immutable.java | 18 - .../android/lib/common/apps/AppInfo.java | 116 ------ .../ycdev/android/lib/common/apps/AppInfo.kt | 102 +++++ .../lib/common/apps/AppsLoadConfig.java | 14 - .../android/lib/common/apps/AppsLoadConfig.kt | 12 + .../lib/common/apps/AppsLoadFilter.java | 32 -- .../android/lib/common/apps/AppsLoadFilter.kt | 31 ++ ...sLoadListener.java => AppsLoadListener.kt} | 11 +- .../android/lib/common/apps/AppsLoader.java | 147 ------- .../android/lib/common/apps/AppsLoader.kt | 154 ++++++++ .../lib/common/async/AsyncTaskQueue.java | 158 -------- .../lib/common/async/AsyncTaskQueue.kt | 140 +++++++ .../lib/common/async/HandlerExecutor.java | 32 -- .../lib/common/async/HandlerExecutor.kt | 31 ++ .../common/async/HandlerThreadExecutor.java | 21 - .../lib/common/async/ITaskExecutor.java | 19 - .../android/lib/common/async/ITaskExecutor.kt | 16 + .../lib/common/async/TaskScheduler.java | 359 ------------------ .../android/lib/common/async/TaskScheduler.kt | 353 +++++++++++++++++ .../android/lib/common/base/ICallback.java | 7 - .../android/lib/common/base/ICallback.kt | 6 + .../lib/common/compat/PowerManagerCompat.java | 17 - .../lib/common/compat/ViewsCompat.java | 26 -- .../lib/common/dbmgr/SQLiteDbCreator.java | 9 - .../lib/common/dbmgr/SQLiteDbCreator.kt | 8 + .../android/lib/common/dbmgr/SQLiteDbMgr.java | 92 ----- .../android/lib/common/dbmgr/SQLiteDbMgr.kt | 92 +++++ .../android/app/ActivityManagerIA.java | 142 ------- .../android/app/ActivityManagerIA.kt | 133 +++++++ .../internalapi/android/os/EnvironmentIA.java | 140 ------- .../internalapi/android/os/EnvironmentIA.kt | 140 +++++++ .../android/os/PowerManagerIA.java | 304 --------------- .../internalapi/android/os/PowerManagerIA.kt | 310 +++++++++++++++ .../internalapi/android/os/ProcessIA.java | 228 ----------- .../internalapi/android/os/ProcessIA.kt | 226 +++++++++++ .../android/os/ServiceManagerIA.java | 216 ----------- .../android/os/ServiceManagerIA.kt | 211 ++++++++++ .../android/os/SystemPropertiesIA.java | 92 ----- .../android/os/SystemPropertiesIA.kt | 110 ++++++ .../internalapi/android/os/UserHandleIA.java | 72 ---- .../internalapi/android/os/UserHandleIA.kt | 44 +++ .../lib/common/ipc/ConnectStateListener.java | 5 - .../lib/common/ipc/ConnectStateListener.kt | 5 + .../android/lib/common/ipc/IpcHandler.java | 31 -- .../android/lib/common/ipc/IpcHandler.kt | 14 + .../android/lib/common/ipc/IpcOperation.java | 8 - .../android/lib/common/ipc/IpcOperation.kt | 8 + .../lib/common/ipc/ServiceClientBase.java | 162 -------- .../lib/common/ipc/ServiceClientBase.kt | 153 ++++++++ .../lib/common/ipc/ServiceConnector.java | 342 ----------------- .../lib/common/ipc/ServiceConnector.kt | 321 ++++++++++++++++ .../android/lib/common/net/HttpClient.java | 169 --------- .../android/lib/common/net/HttpClient.kt | 168 ++++++++ .../android/lib/common/net/NetworkUtils.java | 232 ----------- .../android/lib/common/net/NetworkUtils.kt | 199 ++++++++++ .../lib/common/perms/PermissionCallback.java | 7 - .../lib/common/perms/PermissionCallback.kt | 7 + .../common/perms/PermissionRequestParams.java | 29 -- .../common/perms/PermissionRequestParams.kt | 29 ++ .../lib/common/perms/PermissionUtils.java | 158 -------- .../lib/common/perms/PermissionUtils.kt | 170 +++++++++ .../lib/common/provider/InfoProvider.java | 120 ------ .../lib/common/provider/InfoProvider.kt | 116 ++++++ .../common/provider/InfoProviderClient.java | 150 -------- .../lib/common/provider/InfoProviderClient.kt | 151 ++++++++ .../common/tracker/BatteryInfoTracker.java | 126 ------ .../lib/common/tracker/BatteryInfoTracker.kt | 130 +++++++ .../tracker/InteractiveStateTracker.java | 114 ------ .../common/tracker/InteractiveStateTracker.kt | 110 ++++++ .../lib/common/tracker/WeakTracker.java | 18 - .../android/lib/common/tracker/WeakTracker.kt | 16 + .../lib/common/type/BooleanHolder.java | 9 - .../android/lib/common/type/BooleanHolder.kt | 3 + .../lib/common/type/IntegerHolder.java | 9 - .../android/lib/common/type/IntegerHolder.kt | 3 + .../android/lib/common/type/LongHolder.java | 9 - .../android/lib/common/type/LongHolder.kt | 3 + .../lib/common/utils/AndroidVersionUtils.java | 36 -- .../lib/common/utils/ApplicationUtils.java | 81 ---- .../lib/common/utils/ApplicationUtils.kt | 74 ++++ .../lib/common/utils/DateTimeUtils.java | 69 ---- .../android/lib/common/utils/DateTimeUtils.kt | 65 ++++ .../android/lib/common/utils/DebugUtils.java | 30 -- .../android/lib/common/utils/DebugUtils.kt | 25 ++ .../android/lib/common/utils/DigestUtils.java | 77 ---- .../android/lib/common/utils/DigestUtils.kt | 78 ++++ .../lib/common/utils/EncodingUtils.java | 75 ---- .../android/lib/common/utils/EncodingUtils.kt | 69 ++++ .../android/lib/common/utils/FileLogger.java | 141 ------- .../android/lib/common/utils/FileLogger.kt | 129 +++++++ .../android/lib/common/utils/GcHelper.java | 49 --- .../android/lib/common/utils/GcHelper.kt | 48 +++ .../android/lib/common/utils/GsonHelper.java | 50 --- .../android/lib/common/utils/GsonHelper.kt | 41 ++ .../utils/{ImageUtils.java => ImageUtils.kt} | 158 ++++---- .../android/lib/common/utils/IntentUtils.java | 17 - .../android/lib/common/utils/IntentUtils.kt | 16 + .../android/lib/common/utils/IoUtils.java | 191 ---------- .../ycdev/android/lib/common/utils/IoUtils.kt | 156 ++++++++ .../android/lib/common/utils/LibConfigs.java | 8 - .../android/lib/common/utils/LibLogger.java | 155 -------- .../android/lib/common/utils/LibLogger.kt | 148 ++++++++ .../android/lib/common/utils/MainHandler.java | 25 -- .../android/lib/common/utils/MainHandler.kt | 7 + .../android/lib/common/utils/MiscUtils.java | 7 - .../android/lib/common/utils/MiscUtils.kt | 7 + .../lib/common/utils/PackageUtils.java | 176 --------- .../android/lib/common/utils/PackageUtils.kt | 174 +++++++++ .../lib/common/utils/Preconditions.java | 29 -- .../android/lib/common/utils/Preconditions.kt | 28 ++ .../lib/common/utils/ReflectionUtils.java | 55 --- .../lib/common/utils/ReflectionUtils.kt | 62 +++ .../lib/common/utils/StorageUtils.java | 77 ---- .../android/lib/common/utils/StorageUtils.kt | 67 ++++ .../android/lib/common/utils/StringUtils.java | 34 -- .../android/lib/common/utils/StringUtils.kt | 31 ++ .../lib/common/utils/SystemServiceHelper.java | 94 ----- .../lib/common/utils/SystemServiceHelper.kt | 83 ++++ .../android/lib/common/utils/ThreadUtils.java | 30 -- .../android/lib/common/utils/ThreadUtils.kt | 26 ++ .../android/lib/common/utils/WeakHandler.java | 24 -- .../android/lib/common/utils/WeakHandler.kt | 16 + .../lib/common/utils/WeakListenerManager.java | 100 ----- .../lib/common/utils/WeakListenerManager.kt | 99 +++++ .../lib/common/wrapper/BroadcastHelper.java | 65 ---- .../lib/common/wrapper/BroadcastHelper.kt | 68 ++++ .../lib/common/wrapper/IntentHelper.java | 330 ---------------- .../lib/common/wrapper/IntentHelper.kt | 318 ++++++++++++++++ .../lib/common/net/NetworkUtilsTestBasic.kt | 6 +- .../common/packets/TinyPacketsWorkerTest.kt | 1 + .../lib/common/utils/EncodingUtilsTest.java | 62 --- .../lib/common/utils/EncodingUtilsTest.kt | 62 +++ .../lib/common/utils/ReflectionUtilsTest.kt | 14 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55190 bytes gradlew | 2 +- gradlew.bat | 2 +- .../lib/commonjni/FileStatusHelperTest.kt | 3 +- .../lib/commonjni/CommonJniLoader.java | 11 - .../android/lib/commonjni/CommonJniLoader.kt | 11 + .../lib/commonjni/FileStatusHelper.java | 17 - .../android/lib/commonjni/FileStatusHelper.kt | 11 + .../lib/commonjni/SysResourceLimitHelper.java | 25 -- .../lib/commonjni/SysResourceLimitHelper.kt | 23 ++ jniLibDemo/build.gradle | 1 + .../lib/commonjni/demo/MainActivity.java | 57 --- .../lib/commonjni/demo/MainActivity.kt | 52 +++ .../commonjni/demo/ResourceLimitActivity.java | 80 ---- .../commonjni/demo/ResourceLimitActivity.kt | 74 ++++ .../android/lib/test/ObjectLeakCheckerTest.kt | 7 +- .../android/lib/test/ObjectLeakChecker.java | 66 ---- .../android/lib/test/ObjectLeakChecker.kt | 62 +++ .../lib/test/base/NormalJUnitBase.java | 13 - .../android/lib/test/base/NormalJUnitBase.kt | 14 + .../lib/test/base/RobolectricBase.java | 11 - .../android/lib/test/base/RobolectricBase.kt | 14 + .../lib/test/log/AndroidLogHelper.java | 30 -- .../android/lib/test/log/AndroidLogHelper.kt | 24 ++ .../android/lib/test/log/TimberJvmTree.java | 29 -- .../android/lib/test/log/TimberJvmTree.kt | 28 ++ .../activity/GridEntriesActivity.java | 154 -------- .../commonui/activity/GridEntriesActivity.kt | 133 +++++++ .../lib/commonui/base/ListAdapterBase.java | 84 ---- .../lib/commonui/base/ListAdapterBase.kt | 74 ++++ .../commonui/base/LoadingAsyncTaskBase.java | 30 -- .../lib/commonui/base/LoadingAsyncTaskBase.kt | 27 ++ .../lib/commonui/base/ViewHolderBase.java | 15 - .../lib/commonui/base/ViewHolderBase.kt | 5 + .../commonui/base/WaitingAsyncTaskBase.java | 73 ---- .../lib/commonui/base/WaitingAsyncTaskBase.kt | 56 +++ .../lib/commonui/utils/WaitingAsyncTask.java | 36 -- .../lib/commonui/utils/WaitingAsyncTask.kt | 27 ++ 218 files changed, 6875 insertions(+), 7560 deletions(-) delete mode 100644 archLib/src/main/java/me/ycdev/android/arch/ArchConstants.java create mode 100644 archLib/src/main/java/me/ycdev/android/arch/ArchConstants.kt delete mode 100644 archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.java create mode 100644 archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.kt delete mode 100644 archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.java create mode 100644 archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.kt delete mode 100644 archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.java create mode 100644 archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.kt delete mode 100644 archLib/src/main/java/me/ycdev/android/arch/utils/AppLogger.java delete mode 100644 archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.java create mode 100644 archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.kt delete mode 100644 archLintRulesAAR/build.gradle delete mode 100644 archLintRulesAAR/proguard-rules.pro delete mode 100644 archLintRulesAAR/src/androidTest/java/me/ycdev/android/arch/lint/aar/ApplicationTest.java delete mode 100644 archLintRulesAAR/src/main/AndroidManifest.xml delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt delete mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java create mode 100644 archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/annotation/GuardedBy.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/annotation/Immutable.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.kt rename baseLib/src/main/java/me/ycdev/android/lib/common/apps/{AppsLoadListener.java => AppsLoadListener.kt} (61%) delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerThreadExecutor.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/compat/PowerManagerCompat.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/compat/ViewsCompat.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/AndroidVersionUtils.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.kt rename baseLib/src/main/java/me/ycdev/android/lib/common/utils/{ImageUtils.java => ImageUtils.kt} (58%) delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibConfigs.java delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt delete mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.java create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.kt delete mode 100644 baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.java create mode 100644 baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.kt delete mode 100644 jniLib/src/main/java/me/ycdev/android/lib/commonjni/CommonJniLoader.java create mode 100644 jniLib/src/main/java/me/ycdev/android/lib/commonjni/CommonJniLoader.kt delete mode 100644 jniLib/src/main/java/me/ycdev/android/lib/commonjni/FileStatusHelper.java create mode 100644 jniLib/src/main/java/me/ycdev/android/lib/commonjni/FileStatusHelper.kt delete mode 100644 jniLib/src/main/java/me/ycdev/android/lib/commonjni/SysResourceLimitHelper.java create mode 100644 jniLib/src/main/java/me/ycdev/android/lib/commonjni/SysResourceLimitHelper.kt delete mode 100644 jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/MainActivity.java create mode 100644 jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/MainActivity.kt delete mode 100644 jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.java create mode 100644 jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.kt delete mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.java create mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.kt delete mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.java create mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.kt delete mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.java create mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.kt delete mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.java create mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.kt delete mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.java create mode 100644 testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.java create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.java create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.java create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.java create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.java create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.java create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.kt diff --git a/android_project_common.gradle b/android_project_common.gradle index 3a3e828..0c34562 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -35,14 +35,14 @@ ext { 'compileSdk' : 28, // Android official support - 'kotlin' : "1.3.30", + 'kotlin' : "1.3.31", 'supportLib' : "28.0.0", 'multidexLib' : "2.0.1", 'constraintLayout' : "1.1.3", 'lintLib' : "26.4.0", - 'archCore' : "2.0.0-rc01", - 'lifecycle' : "2.0.0-rc01", - 'room' : "2.0.0-rc01", + 'archCore' : "2.0.0", + 'lifecycle' : "2.0.0", + 'room' : "2.0.0", // test 'runner' : "1.1.0", @@ -60,7 +60,7 @@ ext { 'wearableSupport' : "2.3.0", // infrastructure - 'butterknife' : "10.0.0", + 'butterknife' : "10.1.0", 'timber' : "4.7.1", 'guava' : "23.5-android", diff --git a/archLib/src/main/java/me/ycdev/android/arch/ArchConstants.java b/archLib/src/main/java/me/ycdev/android/arch/ArchConstants.java deleted file mode 100644 index 61510db..0000000 --- a/archLib/src/main/java/me/ycdev/android/arch/ArchConstants.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.ycdev.android.arch; - -import androidx.annotation.IntDef; -import android.widget.Toast; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import static me.ycdev.android.arch.ArchConstants.IntentType.INTENT_TYPE_ACTIVITY; -import static me.ycdev.android.arch.ArchConstants.IntentType.INTENT_TYPE_BROADCAST; -import static me.ycdev.android.arch.ArchConstants.IntentType.INTENT_TYPE_SERVICE; - -public class ArchConstants { - @IntDef({INTENT_TYPE_ACTIVITY, INTENT_TYPE_BROADCAST, INTENT_TYPE_SERVICE}) - @Retention(RetentionPolicy.SOURCE) - public @interface IntentType { - int INTENT_TYPE_ACTIVITY = 1; - int INTENT_TYPE_BROADCAST = 2; - int INTENT_TYPE_SERVICE = 3; - } - - /* - * Durations for toast - */ - @IntDef({Toast.LENGTH_SHORT, Toast.LENGTH_LONG}) - @Retention(RetentionPolicy.SOURCE) - public @interface ToastDuration {} -} diff --git a/archLib/src/main/java/me/ycdev/android/arch/ArchConstants.kt b/archLib/src/main/java/me/ycdev/android/arch/ArchConstants.kt new file mode 100644 index 0000000..630b734 --- /dev/null +++ b/archLib/src/main/java/me/ycdev/android/arch/ArchConstants.kt @@ -0,0 +1,21 @@ +package me.ycdev.android.arch + +import android.widget.Toast +import androidx.annotation.IntDef + +object ArchConstants { + const val INTENT_TYPE_ACTIVITY = 1 + const val INTENT_TYPE_BROADCAST = 2 + const val INTENT_TYPE_SERVICE = 3 + + @IntDef(INTENT_TYPE_ACTIVITY, INTENT_TYPE_BROADCAST, INTENT_TYPE_SERVICE) + @Retention(AnnotationRetention.SOURCE) + annotation class IntentType + + /* + * Durations for toast + */ + @IntDef(Toast.LENGTH_SHORT, Toast.LENGTH_LONG) + @Retention(AnnotationRetention.SOURCE) + annotation class ToastDuration +} diff --git a/archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.java b/archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.java deleted file mode 100644 index d0448c7..0000000 --- a/archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.ycdev.android.arch.activity; - -import android.os.Bundle; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; - -/** - * Base class for Activity which wants to inherit - * {@link androidx.appcompat.app.AppCompatActivity}. - */ -public abstract class AppCompatBaseActivity extends AppCompatActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (shouldSetDisplayHomeAsUpEnabled()) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - } - } - } - - protected boolean shouldSetDisplayHomeAsUpEnabled() { - return true; - } -} diff --git a/archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.kt b/archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.kt new file mode 100644 index 0000000..2cb0d94 --- /dev/null +++ b/archLib/src/main/java/me/ycdev/android/arch/activity/AppCompatBaseActivity.kt @@ -0,0 +1,22 @@ +package me.ycdev.android.arch.activity + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +/** + * Base class for Activity which wants to inherit + * [androidx.appcompat.app.AppCompatActivity]. + */ +abstract class AppCompatBaseActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (shouldSetDisplayHomeAsUpEnabled()) { + val actionBar = supportActionBar + actionBar?.setDisplayHomeAsUpEnabled(true) + } + } + + protected open fun shouldSetDisplayHomeAsUpEnabled(): Boolean { + return true + } +} diff --git a/archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.java b/archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.java deleted file mode 100644 index 7c99872..0000000 --- a/archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.java +++ /dev/null @@ -1,10 +0,0 @@ -package me.ycdev.android.arch.activity; - -import android.app.Activity; - -/** - * Base class for Activity which wants to inherit {@link android.app.Activity}. - */ -public abstract class BaseActivity extends Activity { - // nothing to do right now -} diff --git a/archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.kt b/archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.kt new file mode 100644 index 0000000..2a33f68 --- /dev/null +++ b/archLib/src/main/java/me/ycdev/android/arch/activity/BaseActivity.kt @@ -0,0 +1,8 @@ +package me.ycdev.android.arch.activity + +import android.app.Activity + +/** + * Base class for Activity which wants to inherit [android.app.Activity]. + */ +abstract class BaseActivity : Activity() // nothing to do right now diff --git a/archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.java b/archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.java deleted file mode 100644 index f641bd3..0000000 --- a/archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.java +++ /dev/null @@ -1,11 +0,0 @@ -package me.ycdev.android.arch.activity; - -import android.preference.PreferenceActivity; - -/** - * Base class for Activity which wants to inherit - * {@link android.preference.PreferenceActivity}. - */ -public abstract class PreferenceBaseActivity extends PreferenceActivity { - // nothing to do right now -} diff --git a/archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.kt b/archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.kt new file mode 100644 index 0000000..30cc0b8 --- /dev/null +++ b/archLib/src/main/java/me/ycdev/android/arch/activity/PreferenceBaseActivity.kt @@ -0,0 +1,9 @@ +package me.ycdev.android.arch.activity + +import android.preference.PreferenceActivity + +/** + * Base class for Activity which wants to inherit + * [android.preference.PreferenceActivity]. + */ +abstract class PreferenceBaseActivity : PreferenceActivity() // nothing to do right now diff --git a/archLib/src/main/java/me/ycdev/android/arch/utils/AppLogger.java b/archLib/src/main/java/me/ycdev/android/arch/utils/AppLogger.java deleted file mode 100644 index d3d5aec..0000000 --- a/archLib/src/main/java/me/ycdev/android/arch/utils/AppLogger.java +++ /dev/null @@ -1,77 +0,0 @@ -package me.ycdev.android.arch.utils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.Log; - -import me.ycdev.android.lib.common.utils.FileLogger; -import me.ycdev.android.lib.common.utils.LibLogger; - -/** - * A wrapper class as logger. - *

TODO To write custom lint rules to enforce only AppLogger used instead of android.util.Log.

- */ -public class AppLogger { - private AppLogger() { - // nothing to do - } - - public static void enableJvmLogger() { - LibLogger.enableJvmLogger(); - } - - public static void setFileLogger(FileLogger fileLogger) { - LibLogger.setFileLogger(fileLogger); - } - - /** - * Log enabled by default - */ - public static void setLogEnabled(boolean enabled) { - LibLogger.setLogEnabled(enabled); - } - - public static boolean isLogEnabled() { - return LibLogger.isLogEnabled(); - } - - public static void v(@NonNull String tag, @NonNull String msg, Object... args) { - LibLogger.log(Log.VERBOSE, tag, msg, null, args); - } - - public static void d(@NonNull String tag, @NonNull String msg, Object... args) { - LibLogger.log(Log.DEBUG, tag, msg, null, args); - } - - public static void i(@NonNull String tag, @NonNull String msg, Object... args) { - LibLogger.log(Log.INFO, tag, msg, null, args); - } - - public static void w(@NonNull String tag, @NonNull String msg, Object... args) { - LibLogger.log(Log.WARN, tag, msg, null, args); - } - - public static void w(@NonNull String tag, @NonNull String msg, @NonNull Throwable e, - Object... args) { - LibLogger.log(Log.WARN, tag, msg, e, args); - } - - public static void w(@NonNull String tag, @NonNull Throwable e, Object... args) { - LibLogger.log(Log.WARN, tag, null, e, args); - } - - public static void e(@NonNull String tag, @NonNull String msg, Object... args) { - LibLogger.log(Log.ERROR, tag, msg, null, args); - } - - public static void e(@NonNull String tag, @NonNull String msg, @NonNull Throwable e, - Object... args) { - LibLogger.log(Log.ERROR, tag, msg, e, args); - } - - public static void log(int level, @NonNull String tag, @Nullable String msg, - @Nullable Throwable tr, Object... args) { - LibLogger.log(level, tag, msg, tr, args); - } - -} diff --git a/archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.java b/archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.java deleted file mode 100644 index fb83114..0000000 --- a/archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.ycdev.android.arch.wrapper; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import android.widget.Toast; - -import static me.ycdev.android.arch.ArchConstants.ToastDuration; - -/** - * A wrapper class for Toast so that we can customize and unify the UI in future. - */ -@SuppressWarnings("unused") -public class ToastHelper { - private ToastHelper() { - // nothing to do - } - - public static void show(@NonNull Context cxt, @StringRes int msgResId, - @ToastDuration int duration) { - Toast.makeText(cxt, msgResId, duration).show(); - } - - public static void show(@NonNull Context cxt, @NonNull CharSequence msg, - @ToastDuration int duration) { - Toast.makeText(cxt, msg, duration).show(); - } - -} diff --git a/archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.kt b/archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.kt new file mode 100644 index 0000000..428d079 --- /dev/null +++ b/archLib/src/main/java/me/ycdev/android/arch/wrapper/ToastHelper.kt @@ -0,0 +1,20 @@ +package me.ycdev.android.arch.wrapper + +import android.content.Context +import android.widget.Toast +import androidx.annotation.StringRes +import me.ycdev.android.arch.ArchConstants.ToastDuration + +/** + * A wrapper class for Toast so that we can customize and unify the UI in future. + */ +object ToastHelper { + + fun show(cxt: Context, @StringRes msgResId: Int, @ToastDuration duration: Int) { + Toast.makeText(cxt, msgResId, duration).show() + } + + fun show(cxt: Context, msg: CharSequence, @ToastDuration duration: Int) { + Toast.makeText(cxt, msg, duration).show() + } +} diff --git a/archLintRulesAAR/build.gradle b/archLintRulesAAR/build.gradle deleted file mode 100644 index dbfad50..0000000 --- a/archLintRulesAAR/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -apply plugin: 'com.android.library' -project.archivesBaseName = 'common-arch-lint-rules' - -android { - compileSdkVersion rootProject.ext.compileSdkVersion - buildToolsVersion rootProject.ext.buildToolsVersion - - defaultConfig { - minSdkVersion rootProject.ext.minSdkVersion - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) -} - -/* - * rules for including "lint.jar" in aar - */ -configurations { - lintJarImport -} - -dependencies { - lintJarImport project(path: ":archLintRules", configuration: "lintJarOutput") -} - -task copyLintJar(type: Copy) { - from (configurations.lintJarImport) { - rename { - String fileName -> - 'lint.jar' - } - } - into 'build/intermediates/lint/' -} - -project.afterEvaluate { - def compileLintTask = project.tasks.find { it.name == 'compileLint' } - compileLintTask.dependsOn(copyLintJar) -} - -project.ext { - moduleName = 'me.ycdev.android.common-arch-lint-rules' - moduleDesc = 'Lint rules for common arch module in AndroidLib project' -} - -apply from: rootProject.file('bintray-install.gradle') -apply from: rootProject.file('bintray-upload.gradle') diff --git a/archLintRulesAAR/proguard-rules.pro b/archLintRulesAAR/proguard-rules.pro deleted file mode 100644 index 5d677e1..0000000 --- a/archLintRulesAAR/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Users/pub/tools/android-sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# 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 *; -#} diff --git a/archLintRulesAAR/src/androidTest/java/me/ycdev/android/arch/lint/aar/ApplicationTest.java b/archLintRulesAAR/src/androidTest/java/me/ycdev/android/arch/lint/aar/ApplicationTest.java deleted file mode 100644 index 065ebd0..0000000 --- a/archLintRulesAAR/src/androidTest/java/me/ycdev/android/arch/lint/aar/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package me.ycdev.android.arch.lint.aar; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/archLintRulesAAR/src/main/AndroidManifest.xml b/archLintRulesAAR/src/main/AndroidManifest.xml deleted file mode 100644 index 2c28be4..0000000 --- a/archLintRulesAAR/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/archLintRulesTestDemo/build.gradle b/archLintRulesTestDemo/build.gradle index fbb4069..b6383c3 100644 --- a/archLintRulesTestDemo/build.gradle +++ b/archLintRulesTestDemo/build.gradle @@ -5,7 +5,7 @@ apply from: "${androidModuleCommon}" android { defaultConfig { applicationId "me.ycdev.android.arch.demo" - minSdkVersion 18 // for 'uiautomator' + minSdkVersion versions.minSdk targetSdkVersion 28 versionCode 1 versionName "1.0" @@ -41,6 +41,9 @@ dependencies { implementation "androidx.multidex:multidex:${versions.multidexLib}" implementation "androidx.annotation:annotation:1.0.2" implementation "androidx.constraintlayout:constraintlayout:${versions.constraintLayout}" + implementation "androidx.arch.core:core-common:${versions.archCore}" + implementation "androidx.lifecycle:lifecycle-common:${versions.lifecycle}" + implementation "androidx.room:room-common:${versions.room}" implementation ("com.google.android.gms:play-services-auth:${versions.gms}", { exclude group: 'com.android.support' diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.java deleted file mode 100644 index 6be8426..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.java +++ /dev/null @@ -1,12 +0,0 @@ -package me.ycdev.android.arch.demo.activity; - -import android.os.Bundle; - -import me.ycdev.android.arch.activity.BaseActivity; - -public class LintGood2Activity extends BaseActivity { // lint good - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.kt new file mode 100644 index 0000000..6d9f3ed --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood2Activity.kt @@ -0,0 +1,11 @@ +package me.ycdev.android.arch.demo.activity + +import android.os.Bundle + +import me.ycdev.android.arch.activity.BaseActivity + +open class LintGood2Activity : BaseActivity() { // lint good + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.java deleted file mode 100644 index 520ea71..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.java +++ /dev/null @@ -1,5 +0,0 @@ -package me.ycdev.android.arch.demo.activity; - -public class LintGood3Activity extends LintGood2Activity { // lint good - // nothing to do -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.kt new file mode 100644 index 0000000..e8fb68c --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGood3Activity.kt @@ -0,0 +1,4 @@ +package me.ycdev.android.arch.demo.activity + +class LintGood3Activity : LintGood2Activity() // lint good +// nothing to do diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.java deleted file mode 100644 index 2f895aa..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.java +++ /dev/null @@ -1,32 +0,0 @@ -package me.ycdev.android.arch.demo.activity; - -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; - -import me.ycdev.android.arch.activity.AppCompatBaseActivity; - - -public class LintGoodActivity extends AppCompatBaseActivity { // lint good - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - return super.onOptionsItemSelected(item); - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.kt new file mode 100644 index 0000000..b2cfd86 --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintGoodActivity.kt @@ -0,0 +1,28 @@ +package me.ycdev.android.arch.demo.activity + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem + +import me.ycdev.android.arch.activity.AppCompatBaseActivity + +class LintGoodActivity : AppCompatBaseActivity() { // lint good + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item.itemId + + return super.onOptionsItemSelected(item) + } +} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.java deleted file mode 100644 index 205b25f..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.java +++ /dev/null @@ -1,12 +0,0 @@ -package me.ycdev.android.arch.demo.activity; - -import android.app.Activity; -import android.os.Bundle; - -// class comment for test -public class LintViolation2Activity extends Activity { // lint violation - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.kt new file mode 100644 index 0000000..1398228 --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolation2Activity.kt @@ -0,0 +1,11 @@ +package me.ycdev.android.arch.demo.activity + +import android.app.Activity +import android.os.Bundle + +// class comment for test +class LintViolation2Activity : Activity() { // lint violation + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.java deleted file mode 100644 index 18df21e..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.java +++ /dev/null @@ -1,52 +0,0 @@ -package me.ycdev.android.arch.demo.activity; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.view.MenuItem; - - -/** - * Class doc for test - */ -public class LintViolationActivity extends AppCompatActivity { // lint violation - private static final String TEST_ACTION = "action.test"; - - private BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // nothing to do - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - - IntentFilter filter = new IntentFilter(); - filter.addAction(TEST_ACTION); - registerReceiver(mReceiver, filter); // lint violation - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - sendBroadcast(new Intent(TEST_ACTION)); // lint violation - - return super.onOptionsItemSelected(item); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - unregisterReceiver(mReceiver); - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt new file mode 100644 index 0000000..a98bec2 --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt @@ -0,0 +1,49 @@ +package me.ycdev.android.arch.demo.activity + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.view.MenuItem + +/** + * Class doc for test + */ +class LintViolationActivity : AppCompatActivity() { // lint violation + + private val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + // nothing to do + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val filter = IntentFilter() + filter.addAction(TEST_ACTION) + registerReceiver(receiver, filter) // lint violation + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item.itemId + + sendBroadcast(Intent(TEST_ACTION)) // lint violation + + return super.onOptionsItemSelected(item) + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(receiver) + } + + companion object { + private val TEST_ACTION = "action.test" + } +} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java deleted file mode 100644 index 62d063d..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java +++ /dev/null @@ -1,58 +0,0 @@ -package me.ycdev.android.arch.demo.wrapper; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; - -import me.ycdev.android.lib.common.wrapper.BroadcastHelper; - -public class BroadcastHelperLintCase { - private static class Foo { - public void registerReceiver() { // lint good - } - - public void sendBroadcast() { // lint good - } - } - - public static void registerReceiver() { // lint good - new Foo().registerReceiver(); - } - - public static void sendBroadcast() { // lint good - new Foo().sendBroadcast(); - } - - public static Intent registerGood(Context cxt, BroadcastReceiver receiver, IntentFilter filter) { - return BroadcastHelper.registerForInternal(cxt, receiver, filter); // lint good - } - - public static void sendToInternalGood(Context cxt, Intent intent) { - BroadcastHelper.sendToInternal(cxt, intent); // lint good - } - - public static void sendToExternalGood(Context cxt, Intent intent, String perm) { - BroadcastHelper.sendToExternal(cxt, intent, perm); // lint good - } - - public static void sendToExternal(Context cxt, Intent intent) { - BroadcastHelper.sendToExternal(cxt, intent); // lint good - } - - public static Intent registerViolation(Context cxt, BroadcastReceiver receiver, IntentFilter filter) { - return cxt.registerReceiver(receiver, filter); // lint violation - } - - public static Intent registerViolation2(Context cxt, BroadcastReceiver receiver, IntentFilter filter) { - return cxt.registerReceiver(receiver, filter, null, null); // lint violation - } - - public static void sendViolation(Context cxt, Intent intent, String perm) { - cxt.sendBroadcast(intent, perm); // lint violation - } - - public static void sendViolation2(Context cxt, Intent intent) { - cxt.sendBroadcast(intent); // lint violation - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt new file mode 100644 index 0000000..9721222 --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt @@ -0,0 +1,66 @@ +package me.ycdev.android.arch.demo.wrapper + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter + +import me.ycdev.android.lib.common.wrapper.BroadcastHelper + +object BroadcastHelperLintCase { + private class Foo { + fun registerReceiver() { // lint good + } + + fun sendBroadcast() { // lint good + } + } + + fun registerReceiver() { // lint good + Foo().registerReceiver() + } + + fun sendBroadcast() { // lint good + Foo().sendBroadcast() + } + + fun registerGood(cxt: Context, receiver: BroadcastReceiver, filter: IntentFilter): Intent? { + return BroadcastHelper.registerForInternal(cxt, receiver, filter) // lint good + } + + fun sendToInternalGood(cxt: Context, intent: Intent) { + BroadcastHelper.sendToInternal(cxt, intent) // lint good + } + + fun sendToExternalGood(cxt: Context, intent: Intent, perm: String) { + BroadcastHelper.sendToExternal(cxt, intent, perm) // lint good + } + + fun sendToExternal(cxt: Context, intent: Intent) { + BroadcastHelper.sendToExternal(cxt, intent) // lint good + } + + fun registerViolation( + cxt: Context, + receiver: BroadcastReceiver, + filter: IntentFilter + ): Intent? { + return cxt.registerReceiver(receiver, filter) // lint violation + } + + fun registerViolation2( + cxt: Context, + receiver: BroadcastReceiver, + filter: IntentFilter + ): Intent? { + return cxt.registerReceiver(receiver, filter, null, null) // lint violation + } + + fun sendViolation(cxt: Context, intent: Intent, perm: String) { + cxt.sendBroadcast(intent, perm) // lint violation + } + + fun sendViolation2(cxt: Context, intent: Intent) { + cxt.sendBroadcast(intent) // lint violation + } +} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java deleted file mode 100644 index 295458a..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java +++ /dev/null @@ -1,48 +0,0 @@ -package me.ycdev.android.arch.demo.wrapper; - -import android.content.Intent; -import android.os.Bundle; - -import me.ycdev.android.lib.common.wrapper.IntentHelper; - -public class IntentHelperLintCase { - private static class Foo { - public void hasExtra() { // lint good - } - - public void getBundleExtra() { // lint good - } - } - - public static void hasExtra() { // lint good - new Foo().hasExtra(); - } - - public static void getBundleExtra() { // lint good - new Foo().getBundleExtra(); - } - - public static boolean hasExtraGood(Intent intent, String key) { - return IntentHelper.hasExtra(intent, key); // lint good - } - - public static boolean getBooleanExtraGood(Intent intent, String key, boolean defValue) { - return IntentHelper.getBooleanExtra(intent, key, defValue); // lint good - } - - public static Bundle getBundleExtraGood(Intent intent, String key) { - return IntentHelper.getBundleExtra(intent, key); // lint good - } - - public static boolean hasExtraBad(Intent intent, String key) { - return intent.hasExtra(key); // lint violation - } - - public static boolean getBooleanExtraBad(Intent intent, String key, boolean defValue) { - return intent.getBooleanExtra(key, defValue); // lint violation - } - - public static Bundle getBundleExtraBad(Intent intent, String key) { - return intent.getBundleExtra(key); // lint violation - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt new file mode 100644 index 0000000..d71b0ab --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt @@ -0,0 +1,48 @@ +package me.ycdev.android.arch.demo.wrapper + +import android.content.Intent +import android.os.Bundle + +import me.ycdev.android.lib.common.wrapper.IntentHelper + +object IntentHelperLintCase { + private class Foo { + fun hasExtra() { // lint good + } + + fun getBundleExtra() { // lint good + } + } + + fun hasExtra() { // lint good + Foo().hasExtra() + } + + fun getBundleExtra() { // lint good + Foo().getBundleExtra() + } + + fun hasExtraGood(intent: Intent, key: String): Boolean { + return IntentHelper.hasExtra(intent, key) // lint good + } + + fun getBooleanExtraGood(intent: Intent, key: String, defValue: Boolean): Boolean { + return IntentHelper.getBooleanExtra(intent, key, defValue) // lint good + } + + fun getBundleExtraGood(intent: Intent, key: String): Bundle? { + return IntentHelper.getBundleExtra(intent, key) // lint good + } + + fun hasExtraBad(intent: Intent, key: String): Boolean { + return intent.hasExtra(key) // lint violation + } + + fun getBooleanExtraBad(intent: Intent, key: String, defValue: Boolean): Boolean { + return intent.getBooleanExtra(key, defValue) // lint violation + } + + fun getBundleExtraBad(intent: Intent, key: String): Bundle { + return intent.getBundleExtra(key) // lint violation + } +} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java deleted file mode 100644 index 11d862d..0000000 --- a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java +++ /dev/null @@ -1,40 +0,0 @@ -package me.ycdev.android.arch.demo.wrapper; - -import android.content.Context; -import android.widget.Toast; - -import me.ycdev.android.arch.wrapper.ToastHelper; - -public class ToastHelperLintCase { - private static class Foo { - public void show() { // lint good - } - - public void makeText() { // lint good - } - } - - public static void show() { // lint good - new Foo().show(); - } - - public static void makeText() { // lint good - new Foo().makeText(); - } - - public static void showGood(Context cxt, int msgResId, int duration) { - ToastHelper.show(cxt, msgResId, duration); // lint good - } - - public static void showGood(Context cxt, CharSequence msg, int duration) { - ToastHelper.show(cxt, msg, duration); // lint good - } - - public static void showViolation(Context cxt, int msgResId, int duration) { - Toast.makeText(cxt, msgResId, duration).show(); // lint violation - } - - public static void showViolation(Context cxt, CharSequence msg, int duration) { - Toast.makeText(cxt, msg, duration).show(); // lint violation - } -} diff --git a/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.kt b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.kt new file mode 100644 index 0000000..b523879 --- /dev/null +++ b/archLintRulesTestDemo/src/main/java/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.kt @@ -0,0 +1,40 @@ +package me.ycdev.android.arch.demo.wrapper + +import android.content.Context +import android.widget.Toast + +import me.ycdev.android.arch.wrapper.ToastHelper + +object ToastHelperLintCase { + private class Foo { + fun show() { // lint good + } + + fun makeText() { // lint good + } + } + + fun show() { // lint good + Foo().show() + } + + fun makeText() { // lint good + Foo().makeText() + } + + fun showGood(cxt: Context, msgResId: Int, duration: Int) { + ToastHelper.show(cxt, msgResId, duration) // lint good + } + + fun showGood(cxt: Context, msg: CharSequence, duration: Int) { + ToastHelper.show(cxt, msg, duration) // lint good + } + + fun showViolation(cxt: Context, msgResId: Int, duration: Int) { + Toast.makeText(cxt, msgResId, duration).show() // lint violation + } + + fun showViolation(cxt: Context, msg: CharSequence, duration: Int) { + Toast.makeText(cxt, msg, duration).show() // lint violation + } +} diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/AsyncTaskQueueTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/AsyncTaskQueueTest.kt index 988d2ff..17adcba 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/AsyncTaskQueueTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/AsyncTaskQueueTest.kt @@ -29,13 +29,13 @@ class AsyncTaskQueueTest { assertThat(taskQueue.taskHandler).isNull() val latch = CountDownLatch(2) - val task1 = { + val task1 = Runnable { Timber.tag(TAG).d("Executing task1 BEGIN") SystemClock.sleep(1000) latch.countDown() Timber.tag(TAG).d("Executing task1 END") } - val task2 = { + val task2 = Runnable { Timber.tag(TAG).d("Executing task2 BEGIN") // Task1 must be done assertThat(latch.count).isEqualTo(1) @@ -52,7 +52,7 @@ class AsyncTaskQueueTest { SystemClock.sleep(100) val taskHandler = taskQueue.taskHandler assertThat(taskHandler).isNotNull() - val taskThread = taskHandler.looper.thread + val taskThread = taskHandler!!.looper.thread val taskTid = taskThread.id assertThat(taskThread.name).isEqualTo(TAG) assertThat(taskThread.isAlive).isTrue() @@ -179,7 +179,7 @@ class AsyncTaskQueueTest { taskQueue.setWorkerThreadAutoQuitDelay(autoQuitDelay) val latch = CountDownLatch(1) - taskQueue.addTask({ latch.countDown() }, 100) + taskQueue.addTask(Runnable { latch.countDown() }, 100) latch.await() // check if task thread already quited @@ -196,7 +196,7 @@ class AsyncTaskQueueTest { taskQueue.setWorkerThreadAutoQuitDelay(0) val latch = CountDownLatch(1) - taskQueue.addTask({ latch.countDown() }, 100) + taskQueue.addTask(Runnable { latch.countDown() }, 100) latch.await() // check if task thread already quited @@ -275,7 +275,7 @@ class AsyncTaskQueueTest { taskTidHolder: LongArray ) { for (i in 0 until taskCount) { - val task = { + val task = Runnable { // check tid (should only one task thread created) val curTid = Thread.currentThread().id if (taskTidHolder[0] != -1L) { diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/TaskSchedulerTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/TaskSchedulerTest.kt index 9dcb260..6751e2d 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/TaskSchedulerTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/async/TaskSchedulerTest.kt @@ -12,7 +12,9 @@ import java.util.concurrent.TimeUnit @LargeTest class TaskSchedulerTest { private fun createScheduler(mainThread: Boolean): TaskScheduler { - val taskExecutor = if (mainThread) HandlerExecutor.withMainLooper() else HandlerThreadExecutor("test") + val taskExecutor = + if (mainThread) HandlerExecutor.withMainLooper() + else HandlerExecutor.withHandlerThread("test") val taskScheduler = TaskScheduler(taskExecutor, "test") taskScheduler.enableDebugLogs(mainThread) return taskScheduler @@ -28,7 +30,7 @@ class TaskSchedulerTest { val taskScheduler = createScheduler(mainThread) val latch = CountDownLatch(1) val startTime = SystemClock.elapsedRealtime() - taskScheduler.scheduleAt({ + taskScheduler.scheduleAt(Runnable { assertThat(SystemClock.elapsedRealtime() - startTime).isAtLeast(500) latch.countDown() }, 500) @@ -51,7 +53,7 @@ class TaskSchedulerTest { latch.countDown() } taskScheduler.scheduleAt(task, 500) - taskScheduler.scheduleAt(task, 500, TaskScheduler.SchedulePolicy.NO_CHECK) + taskScheduler.scheduleAt(task, 500, TaskScheduler.SCHEDULE_POLICY_NO_CHECK) taskScheduler.scheduleAt(task, 500) latch.await(1, TimeUnit.SECONDS) assertThat(latch.count).isEqualTo(0) @@ -67,11 +69,11 @@ class TaskSchedulerTest { val taskScheduler = createScheduler(mainThread) val latch = CountDownLatch(3) taskScheduler.scheduleAt(SameTaskWrapper(Runnable { latch.countDown() }, 101), - 500, TaskScheduler.SchedulePolicy.IGNORE) + 500, TaskScheduler.SCHEDULE_POLICY_IGNORE) taskScheduler.scheduleAt(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, TaskScheduler.SchedulePolicy.IGNORE) + 500, TaskScheduler.SCHEDULE_POLICY_IGNORE) taskScheduler.scheduleAt(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, TaskScheduler.SchedulePolicy.IGNORE) + 500, TaskScheduler.SCHEDULE_POLICY_IGNORE) latch.await(1, TimeUnit.SECONDS) // will timeout assertThat(latch.count).isEqualTo(2) } @@ -86,11 +88,11 @@ class TaskSchedulerTest { val taskScheduler = createScheduler(mainThread) val latch = CountDownLatch(3) taskScheduler.scheduleAt(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, TaskScheduler.SchedulePolicy.REPLACE) + 500, TaskScheduler.SCHEDULE_POLICY_REPLACE) taskScheduler.scheduleAt(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, TaskScheduler.SchedulePolicy.REPLACE) + 500, TaskScheduler.SCHEDULE_POLICY_REPLACE) taskScheduler.scheduleAt(SameTaskWrapper(Runnable { latch.countDown() }, 101), - 500, TaskScheduler.SchedulePolicy.REPLACE) + 500, TaskScheduler.SCHEDULE_POLICY_REPLACE) latch.await(1, TimeUnit.SECONDS) // will timeout assertThat(latch.count).isEqualTo(2) } @@ -132,7 +134,7 @@ class TaskSchedulerTest { latch.countDown() } taskScheduler.schedulePeriod(task, 500, 1000) - taskScheduler.schedulePeriod(task, 500, 1000, TaskScheduler.SchedulePolicy.NO_CHECK) + taskScheduler.schedulePeriod(task, 500, 1000, TaskScheduler.SCHEDULE_POLICY_NO_CHECK) taskScheduler.schedulePeriod(task, 500, 1000) latch.await(1, TimeUnit.SECONDS) assertThat(latch.count).isEqualTo(0) @@ -153,11 +155,11 @@ class TaskSchedulerTest { assertThat(SystemClock.elapsedRealtime() - startTime).isAtLeast(500) latch.countDown() }, 101) - taskScheduler.schedulePeriod(task, 500, 1000, TaskScheduler.SchedulePolicy.IGNORE) + taskScheduler.schedulePeriod(task, 500, 1000, TaskScheduler.SCHEDULE_POLICY_IGNORE) taskScheduler.schedulePeriod(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, 1000, TaskScheduler.SchedulePolicy.IGNORE) + 500, 1000, TaskScheduler.SCHEDULE_POLICY_IGNORE) taskScheduler.schedulePeriod(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, 1000, TaskScheduler.SchedulePolicy.IGNORE) + 500, 1000, TaskScheduler.SCHEDULE_POLICY_IGNORE) latch.await(1, TimeUnit.SECONDS) // will timeout assertThat(latch.count).isEqualTo(2) taskScheduler.cancel(task) @@ -178,10 +180,10 @@ class TaskSchedulerTest { latch.countDown() }, 101) taskScheduler.schedulePeriod(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, 1000, TaskScheduler.SchedulePolicy.REPLACE) + 500, 1000, TaskScheduler.SCHEDULE_POLICY_REPLACE) taskScheduler.schedulePeriod(SameTaskWrapper(Runnable { fail("Should be ignored") }, 101), - 500, 1000, TaskScheduler.SchedulePolicy.REPLACE) - taskScheduler.schedulePeriod(task, 500, 1000, TaskScheduler.SchedulePolicy.REPLACE) + 500, 1000, TaskScheduler.SCHEDULE_POLICY_REPLACE) + taskScheduler.schedulePeriod(task, 500, 1000, TaskScheduler.SCHEDULE_POLICY_REPLACE) latch.await(1, TimeUnit.SECONDS) // will timeout assertThat(latch.count).isEqualTo(2) taskScheduler.cancel(task) @@ -323,8 +325,8 @@ class TaskSchedulerTest { private fun clear(mainThread: Boolean) { val taskScheduler = createScheduler(mainThread) val latch = CountDownLatch(2) - taskScheduler.scheduleAt({ latch.countDown() }, 1500) - taskScheduler.schedulePeriod({ latch.countDown() }, 500, 1000) + taskScheduler.scheduleAt(Runnable { latch.countDown() }, 1500) + taskScheduler.schedulePeriod(Runnable { latch.countDown() }, 500, 1000) SystemClock.sleep(1000) taskScheduler.clear() latch.await(2, TimeUnit.SECONDS) // will timeout diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/LocalServiceConnector.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/LocalServiceConnector.kt index cdac1af..0e27c56 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/LocalServiceConnector.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/LocalServiceConnector.kt @@ -11,10 +11,10 @@ class LocalServiceConnector(cxt: Context) : ServiceConnector(cxt, @NonNull override fun getServiceIntent(): Intent { - return Intent(mAppContext, LocalService::class.java) + return Intent(appContext, LocalService::class.java) } - override fun asInterface(service: IBinder): IDemoService? { + override fun asInterface(service: IBinder): IDemoService { return IDemoService.Stub.asInterface(service) } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/RemoteServiceConnector.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/RemoteServiceConnector.kt index 4e69301..ea363cf 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/RemoteServiceConnector.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/demo/service/RemoteServiceConnector.kt @@ -12,10 +12,10 @@ open class RemoteServiceConnector(cxt: Context) : @NonNull public override fun getServiceIntent(): Intent { - return Intent(mAppContext, RemoteService::class.java) + return Intent(appContext, RemoteService::class.java) } - override fun asInterface(service: IBinder): IDemoService? { + override fun asInterface(service: IBinder): IDemoService { return IDemoService.Stub.asInterface(service) } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIATest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIATest.kt index e138e54..104ad6c 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIATest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIATest.kt @@ -26,6 +26,6 @@ class ActivityManagerIATest { @Test fun test_forceStopPackage() { - assertTrue(ActivityManagerIA.checkReflect_forceStopPackage()) + assertTrue(ActivityManagerIA.checkReflectForceStopPackage()) } } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIATest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIATest.kt index 16d004d..2a04a5a 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIATest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIATest.kt @@ -20,26 +20,26 @@ class PowerManagerIATest { @Test fun test_getIPowerManager() { - assertNotNull(PowerManagerIA.getIPowerManager()) + assertNotNull(PowerManagerIA.iPowerManager) } @Test fun test_reboot() { - assertTrue(PowerManagerIA.checkReflect_reboot()) + assertTrue(PowerManagerIA.checkReflectReboot()) } @Test fun test_shutdown() { - assertTrue(PowerManagerIA.checkReflect_shutdown()) + assertTrue(PowerManagerIA.checkReflectShutdown()) } @Test fun test_crash() { - assertTrue(PowerManagerIA.checkReflect_crash()) + assertTrue(PowerManagerIA.checkReflectCrash()) } @Test fun test_goToSleep() { - assertTrue(PowerManagerIA.checkReflect_goToSleep()) + assertTrue(PowerManagerIA.checkReflectGoToSleep()) } } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIATest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIATest.kt index 709fd6e..818f455 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIATest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIATest.kt @@ -14,17 +14,17 @@ import org.junit.Assert.assertTrue class ProcessIATest { @Test fun test_setArgV0() { - assertTrue("failed to reflect #setArgV0", ProcessIA.checkReflect_setArgV0()) + assertTrue("failed to reflect #setArgV0", ProcessIA.checkReflectSetArgV0()) } @Test fun test_readProcLines() { - assertTrue("failed to reflect #readProcLines", ProcessIA.checkReflect_readProcLines()) + assertTrue("failed to reflect #readProcLines", ProcessIA.checkReflectReadProcLines()) } @Test fun test_getParentPid() { - assertTrue("failed to reflect #getParentPid", ProcessIA.checkReflect_getParentPid()) + assertTrue("failed to reflect #getParentPid", ProcessIA.checkReflectGetParentPid()) // app process --> zygote val pid = android.os.Process.myPid() val zygotePid = ProcessIA.getParentPid(pid) @@ -33,7 +33,7 @@ class ProcessIATest { @Test fun test_myPpid() { - assertTrue("failed to reflect #myPpid", ProcessIA.checkReflect_myPpid()) + assertTrue("failed to reflect #myPpid", ProcessIA.checkReflectMyPpid()) // app process --> zygote val pid = android.os.Process.myPid() val zygotePid = ProcessIA.myPpid() diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIATest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIATest.kt index f14665a..c5e4794 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIATest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIATest.kt @@ -11,21 +11,21 @@ import org.junit.Assert.assertTrue class ServiceManagerIATest { @Test fun test_getService() { - assertTrue(ServiceManagerIA.checkReflect_getService()) + assertTrue(ServiceManagerIA.checkReflectGetService()) } @Test fun test_checkService() { - assertTrue(ServiceManagerIA.checkReflect_checkService()) + assertTrue(ServiceManagerIA.checkReflectCheckService()) } @Test fun test_addService() { - assertTrue(ServiceManagerIA.checkReflect_addService()) + assertTrue(ServiceManagerIA.checkReflectAddService()) } @Test fun test_listServices() { - assertTrue(ServiceManagerIA.checkReflect_listServices()) + assertTrue(ServiceManagerIA.checkReflectListServices()) } } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIATest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIATest.kt index cfcc19c..903ac46 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIATest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIATest.kt @@ -11,6 +11,6 @@ import org.junit.Assert.assertTrue class UserHandleIATest { @Test fun test_myUserId() { - assertTrue(UserHandleIA.checkReflect_myUserId()) + assertTrue(UserHandleIA.checkReflectMyUserId()) } } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt index 357a673..f660efd 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt @@ -21,6 +21,7 @@ import me.ycdev.android.lib.common.demo.service.operation.HelloOperation import me.ycdev.android.lib.common.utils.ThreadManager import com.google.common.truth.Truth.assertThat +import me.ycdev.android.lib.common.demo.service.IDemoService @RunWith(AndroidJUnit4::class) @LargeTest @@ -36,11 +37,13 @@ class ServiceClientBaseTest { // Service not connected run { val latch = CountDownLatch(1) - client.addOperation { service -> - assertThat(service).isNotNull() - assertThat(Looper.myLooper()!!).isSameAs(ThreadManager.instance.remoteServiceRequestIpcLooper()) - latch.countDown() - } + client.addOperation(object : IpcOperation { + override fun execute(service: IDemoService) { + assertThat(service).isNotNull() + assertThat(Looper.myLooper()!!).isSameAs(ThreadManager.instance.remoteServiceRequestIpcLooper()) + latch.countDown() + } + }) assertThat(latch.count).isEqualTo(1) // Waiting for service connected and operation executed @@ -50,11 +53,13 @@ class ServiceClientBaseTest { // Service already connected run { val latch = CountDownLatch(1) - client.addOperation { service -> - assertThat(service).isNotNull() - assertThat(Looper.myLooper()!!).isSameAs(ThreadManager.instance.remoteServiceRequestIpcLooper()) - latch.countDown() - } + client.addOperation(object : IpcOperation { + override fun execute(service: IDemoService) { + assertThat(service).isNotNull() + assertThat(Looper.myLooper()!!).isSameAs(ThreadManager.instance.remoteServiceRequestIpcLooper()) + latch.countDown() + } + }) assertThat(latch.count).isEqualTo(1) // Waiting for service connected and operation executed @@ -91,20 +96,22 @@ class ServiceClientBaseTest { val latch = CountDownLatch(1) client.addOperation(HelloOperation("Hello, world").setNotifier(latch)) - assertThat(client.serviceConnector.service).isNull() + assertThat(client.serviceConnector.getService()).isNull() latch.await() - assertThat(client.serviceConnector.service).isNotNull() + assertThat(client.serviceConnector.getService()).isNotNull() timeStart = SystemClock.elapsedRealtime() } // Waiting for the service disconnected and check the timeout run { val latch = CountDownLatch(1) - client.serviceConnector.addListener { newState -> - if (newState == ServiceConnector.STATE_DISCONNECTED) { - latch.countDown() + client.serviceConnector.addListener(object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_DISCONNECTED) { + latch.countDown() + } } - } + }) latch.await() val timeUsed = SystemClock.elapsedRealtime() - timeStart assertThat(timeUsed).isGreaterThan(disconnectTimeout) @@ -128,11 +135,13 @@ class ServiceClientBaseTest { // connect run { val latch = CountDownLatch(1) - client.serviceConnector.addListener { newState -> - if (newState == ServiceConnector.STATE_CONNECTED) { - latch.countDown() + client.serviceConnector.addListener(object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_CONNECTED) { + latch.countDown() + } } - } + }) // Make sure the service will not be connected if no connect and no operations latch.await(500, TimeUnit.MILLISECONDS) @@ -146,11 +155,13 @@ class ServiceClientBaseTest { // disconnect run { val latch = CountDownLatch(1) - client.serviceConnector.addListener { newState -> - if (newState == ServiceConnector.STATE_DISCONNECTED) { - latch.countDown() + client.serviceConnector.addListener(object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_DISCONNECTED) { + latch.countDown() + } } - } + }) client.disconnect() latch.await() } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt index 0d6ccb7..e306dfc 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt @@ -38,7 +38,8 @@ class ServiceConnectorTest { connectSync(connector) // BinderProxy - assertThat(connector.service.asBinder().javaClass.name).isEqualTo("android.os.BinderProxy") + + assertThat(connector.getService()!!.asBinder().javaClass.name).isEqualTo("android.os.BinderProxy") disconnectSync(connector) } @@ -51,7 +52,7 @@ class ServiceConnectorTest { connectSync(connector) // Local object - assertThat(connector.service.asBinder().javaClass.name) + assertThat(connector.getService()!!.asBinder().javaClass.name) .isEqualTo("me.ycdev.android.lib.common.demo.service.LocalService\$BinderServer") disconnectSync(connector) } @@ -62,12 +63,12 @@ class ServiceConnectorTest { val context = ApplicationProvider.getApplicationContext() run { val connector = RemoteServiceConnector(context) - assertThat(connector.connectLooper).isEqualTo(Looper.getMainLooper()) + assertThat(connector.getConnectLooper()).isEqualTo(Looper.getMainLooper()) } run { val connector = LocalServiceConnector(context) - assertThat(connector.connectLooper).isEqualTo(Looper.getMainLooper()) + assertThat(connector.getConnectLooper()).isEqualTo(Looper.getMainLooper()) } } @@ -77,19 +78,19 @@ class ServiceConnectorTest { val context = ApplicationProvider.getApplicationContext() run { val connector = RemoteServiceConnector(context) - assertThat(connector.isServiceExist).isTrue() + assertThat(connector.isServiceExist()).isTrue() } run { val connector = LocalServiceConnector(context) - assertThat(connector.isServiceExist).isTrue() + assertThat(connector.isServiceExist()).isTrue() } run { val connector = FakeServiceConnector(context) - assertThat(connector.isServiceExist).isFalse() + assertThat(connector.isServiceExist()).isFalse() } run { val connector = NoPermServiceConnector(context) - assertThat(connector.isServiceExist).isFalse() + assertThat(connector.isServiceExist()).isFalse() } } @@ -100,7 +101,7 @@ class ServiceConnectorTest { run { val connector = RemoteServiceConnector(context) val servicesList = context.packageManager.queryIntentServices( - connector.serviceIntent, 0 + connector.getServiceIntent(), 0 ) assertThat(servicesList).isNotNull() val cn = connector.selectTargetService(servicesList) @@ -110,7 +111,7 @@ class ServiceConnectorTest { run { val connector = NoPermServiceConnector(context) val servicesList = context.packageManager.queryIntentServices( - connector.serviceIntent, 0 + connector.getServiceIntent(), 0 ) assertThat(servicesList).isNotNull() assertThat(connector.selectTargetService(servicesList)).isNull() @@ -125,14 +126,18 @@ class ServiceConnectorTest { val latch1 = CountDownLatch(2) val latch2 = CountDownLatch(2) - val listener1 = ConnectStateListener { newState -> - if (newState == ServiceConnector.STATE_CONNECTED || newState == ServiceConnector.STATE_DISCONNECTED) { - latch1.countDown() + val listener1 = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_CONNECTED || newState == ServiceConnector.STATE_DISCONNECTED) { + latch1.countDown() + } } } - val listener2 = ConnectStateListener { newState -> - if (newState == ServiceConnector.STATE_CONNECTED || newState == ServiceConnector.STATE_DISCONNECTED) { - latch2.countDown() + val listener2 = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_CONNECTED || newState == ServiceConnector.STATE_DISCONNECTED) { + latch2.countDown() + } } } connector.addListener(listener1) @@ -180,11 +185,15 @@ class ServiceConnectorTest { private fun test_disconnect_state(connector: ServiceConnector<*>) { connectSync(connector) val stateChangeCount = IntegerHolder(0) - val listener = ConnectStateListener { stateChangeCount.value++ } + val listener = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + stateChangeCount.value++ + } + } connector.addListener(listener) connector.disconnect() - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) - assertThat(connector.service).isNull() + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.getService()).isNull() assertThat(stateChangeCount.value).isEqualTo(0) } @@ -206,16 +215,20 @@ class ServiceConnectorTest { private fun test_waitForConnected_forever(connector: ServiceConnector<*>) { val stateChangeCount = IntegerHolder(0) - val listener = ConnectStateListener { stateChangeCount.value++ } + val listener = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + stateChangeCount.value++ + } + } connector.addListener(listener) connector.waitForConnected() - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) - assertThat(connector.service).isNotNull() + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.getService()).isNotNull() assertThat(stateChangeCount.value).isEqualTo(2) // connecting & connected connector.waitForConnected() - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) assertThat(stateChangeCount.value).isEqualTo(2) // already connected, no change anymore disconnectSync(connector) @@ -229,12 +242,12 @@ class ServiceConnectorTest { run { val connector = FakeServiceConnector(context) connector.waitForConnected() // should fail immediately - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) } run { val connector = NoPermServiceConnector(context) connector.waitForConnected() // should fail immediately - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) } } @@ -245,15 +258,19 @@ class ServiceConnectorTest { val connector = ConnectDelayServiceConnector(context, 300) val stateChangeCount = IntegerHolder(0) - val listener = ConnectStateListener { stateChangeCount.value++ } + val listener = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + stateChangeCount.value++ + } + } connector.addListener(listener) connector.waitForConnected(100) // - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTING) + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTING) assertThat(stateChangeCount.value).isEqualTo(1) // connecting connector.waitForConnected() - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) assertThat(stateChangeCount.value).isEqualTo(2) // connected disconnectSync(connector) @@ -265,22 +282,24 @@ class ServiceConnectorTest { fun getService() { val context = ApplicationProvider.getApplicationContext() val connector = RemoteServiceConnector(context) - assertThat(connector.service).isNull() + assertThat(connector.getService()).isNull() - val listener = ConnectStateListener { newState -> - if (newState == ServiceConnector.STATE_CONNECTED) { - assertThat(connector.service).isNotNull() - } else { - assertThat(connector.service).isNull() + val listener = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_CONNECTED) { + assertThat(connector.getService()).isNotNull() + } else { + assertThat(connector.getService()).isNull() + } } } connector.addListener(listener) connector.waitForConnected() - assertThat(connector.service).isNotNull() + assertThat(connector.getService()).isNotNull() connector.disconnect() - assertThat(connector.service).isNull() + assertThat(connector.getService()).isNull() } private class FakeServiceConnector internal constructor(cxt: Context) : @@ -291,8 +310,8 @@ class ServiceConnectorTest { return Intent("me.ycdev.android.lib.common.demo.action.FAKE_SERVICE") } - override fun asInterface(service: IBinder): IDemoService? { - return null + override fun asInterface(service: IBinder): IDemoService { + return IDemoService.Stub.asInterface(service) } } @@ -309,7 +328,7 @@ class ServiceConnectorTest { internal var mConnectDelay: Long ) : RemoteServiceConnector(cxt) { - override fun asInterface(service: IBinder): IDemoService? { + override fun asInterface(service: IBinder): IDemoService { SystemClock.sleep(mConnectDelay) return super.asInterface(service) } @@ -337,18 +356,20 @@ class ServiceConnectorTest { private fun connectSync(connector: ServiceConnector<*>) { val latch = CountDownLatch(1) - val listener = ConnectStateListener { newState -> - if (newState == ServiceConnector.STATE_CONNECTED) { - latch.countDown() + val listener = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_CONNECTED) { + latch.countDown() + } } } connector.addListener(listener) - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) - assertThat(connector.service).isNull() + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.getService()).isNull() connector.connect() - assertThat(connector.connectState) + assertThat(connector.getConnectState()) .isAnyOf(ServiceConnector.STATE_CONNECTING, ServiceConnector.STATE_CONNECTED) try { @@ -357,24 +378,26 @@ class ServiceConnectorTest { fail("Should not happen: $e") } - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) - assertThat(connector.service).isNotNull() + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.getService()).isNotNull() } private fun disconnectSync(connector: ServiceConnector<*>) { - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) - assertThat(connector.service).isNotNull() + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.getService()).isNotNull() val latch = CountDownLatch(1) - val listener = ConnectStateListener { newState -> - if (newState == ServiceConnector.STATE_DISCONNECTED) { - latch.countDown() + val listener = object : ConnectStateListener { + override fun onStateChanged(newState: Int) { + if (newState == ServiceConnector.STATE_DISCONNECTED) { + latch.countDown() + } } } connector.addListener(listener) connector.disconnect() - assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) - assertThat(connector.service).isNull() + assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.getService()).isNull() try { latch.await() } catch (e: InterruptedException) { diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/net/NetworkUtilsTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/net/NetworkUtilsTest.kt index 4936f9a..1d4592f 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/net/NetworkUtilsTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/net/NetworkUtilsTest.kt @@ -7,13 +7,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.google.common.truth.Truth.assertWithMessage import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_2G -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_3G -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_4G -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_COMPANION_PROXY -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_MOBILE -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_NONE -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_WIFI +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_2G +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_3G +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_4G +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_COMPANION_PROXY +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_MOBILE +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_NONE +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_WIFI import me.ycdev.android.lib.common.utils.SystemSwitchUtils import org.junit.Rule import org.junit.Test diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/annotation/GuardedBy.java b/baseLib/src/main/java/me/ycdev/android/lib/common/annotation/GuardedBy.java deleted file mode 100644 index 08e8407..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/annotation/GuardedBy.java +++ /dev/null @@ -1,20 +0,0 @@ -package me.ycdev.android.lib.common.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation type used to mark a method or field that can only be accessed when - * holding the referenced lock. - *

- * Note: Copied from com.android.internal.annotations.Immutable.VisibleForTesting. - */ -@Documented -@Target({ElementType.FIELD, ElementType.METHOD}) -@Retention(RetentionPolicy.CLASS) -public @interface GuardedBy { - String value(); -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/annotation/Immutable.java b/baseLib/src/main/java/me/ycdev/android/lib/common/annotation/Immutable.java deleted file mode 100644 index 7c70ed7..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/annotation/Immutable.java +++ /dev/null @@ -1,18 +0,0 @@ -package me.ycdev.android.lib.common.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation type used to mark a class which is immutable. - *

- * Note: Copied from com.android.internal.annotations.Immutable.VisibleForTesting. - */ -@Documented -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.CLASS) -public @interface Immutable { -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.java b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.java deleted file mode 100644 index 2f79c4c..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.java +++ /dev/null @@ -1,116 +0,0 @@ -package me.ycdev.android.lib.common.apps; - -import android.graphics.drawable.Drawable; -import androidx.annotation.Nullable; - -import java.text.Collator; -import java.util.Comparator; - -import me.ycdev.android.lib.common.utils.DateTimeUtils; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class AppInfo { - public String pkgName; - public int appUid; - public String sharedUid; - @Nullable - public String appName; - @Nullable - public Drawable appIcon; - @Nullable - public String versionName; - public int versionCode; - @Nullable - public String apkPath; - public long installTime; - public long updateTime; - public boolean isSysApp; - public boolean isUpdatedSysApp; - public boolean isDisabled; - public boolean isUnmounted; - public boolean isSelected; - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("AppInfo["); - sb.append("pkgName: ").append(pkgName); - sb.append(", appUid: ").append(appUid); - sb.append(", sharedUid: ").append(sharedUid); - sb.append(", appName: ").append(appName); - sb.append(", versionName: ").append(versionName); - sb.append(", versionCode: ").append(versionCode); - sb.append(", apkPath: ").append(apkPath); - sb.append(", installTime: ").append(DateTimeUtils.getReadableTimeStamp(installTime)); - sb.append(", updateTime: ").append(DateTimeUtils.getReadableTimeStamp(updateTime)); - sb.append(", isSysApp: ").append(isSysApp); - sb.append(", isUpdatedSysApp: ").append(isUpdatedSysApp); - sb.append(", isDisabled: ").append(isDisabled); - sb.append(", isUnmounted: ").append(isUnmounted); - sb.append(", isSelected: ").append(isSelected); - sb.append("]"); - return sb.toString(); - } - - public static class AppNameComparator implements Comparator { - private Collator mCollator = Collator.getInstance(); - - @Override - public int compare(AppInfo lhs, AppInfo rhs) { - return mCollator.compare(lhs.appName, rhs.appName); - } - } - - public static class PkgNameComparator implements Comparator { - @Override - public int compare(AppInfo lhs, AppInfo rhs) { - return lhs.pkgName.compareTo(rhs.pkgName); - } - } - - public static class UidComparator implements Comparator { - private PkgNameComparator mPkgNameComparator = new PkgNameComparator(); - - @Override - public int compare(AppInfo lhs, AppInfo rhs) { - if (lhs.appUid < rhs.appUid) { - return -1; - } else if (lhs.appUid > rhs.appUid) { - return 1; - } else { - return mPkgNameComparator.compare(lhs, rhs); - } - } - } - - public static class InstallTimeComparator implements Comparator { - private PkgNameComparator mPkgNameComparator = new PkgNameComparator(); - - @Override - public int compare(AppInfo lhs, AppInfo rhs) { - if (lhs.installTime < rhs.installTime) { - return 1; - } else if (lhs.installTime > rhs.installTime) { - return -1; - } else { - return mPkgNameComparator.compare(lhs, rhs); - } - } - } - - public static class UpdateTimeComparator implements Comparator { - private PkgNameComparator mPkgNameComparator = new PkgNameComparator(); - - @Override - public int compare(AppInfo lhs, AppInfo rhs) { - if (lhs.updateTime < rhs.updateTime) { - return 1; - } else if (lhs.updateTime > rhs.updateTime) { - return -1; - } else { - return mPkgNameComparator.compare(lhs, rhs); - } - } - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.kt new file mode 100644 index 0000000..76b75de --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppInfo.kt @@ -0,0 +1,102 @@ +package me.ycdev.android.lib.common.apps + +import android.graphics.drawable.Drawable + +import java.text.Collator +import java.util.Comparator + +import me.ycdev.android.lib.common.utils.DateTimeUtils + +data class AppInfo(val pkgName: String) { + var appUid: Int = 0 + var sharedUid: String? = null + var appName: String? = null + var appIcon: Drawable? = null + var versionName: String? = null + var versionCode: Int = 0 + var apkPath: String? = null + var installTime: Long = 0 + var updateTime: Long = 0 + var isSysApp: Boolean = false + var isUpdatedSysApp: Boolean = false + var isDisabled: Boolean = false + var isUnmounted: Boolean = false + var isSelected: Boolean = false + + override fun toString(): String { + val sb = StringBuilder() + sb.append("AppInfo[") + sb.append("pkgName: ").append(pkgName) + sb.append(", appUid: ").append(appUid) + sb.append(", sharedUid: ").append(sharedUid) + sb.append(", appName: ").append(appName) + sb.append(", versionName: ").append(versionName) + sb.append(", versionCode: ").append(versionCode) + sb.append(", apkPath: ").append(apkPath) + sb.append(", installTime: ").append(DateTimeUtils.getReadableTimeStamp(installTime)) + sb.append(", updateTime: ").append(DateTimeUtils.getReadableTimeStamp(updateTime)) + sb.append(", isSysApp: ").append(isSysApp) + sb.append(", isUpdatedSysApp: ").append(isUpdatedSysApp) + sb.append(", isDisabled: ").append(isDisabled) + sb.append(", isUnmounted: ").append(isUnmounted) + sb.append(", isSelected: ").append(isSelected) + sb.append("]") + return sb.toString() + } + + class AppNameComparator : Comparator { + private val mCollator = Collator.getInstance() + + override fun compare(lhs: AppInfo, rhs: AppInfo): Int { + return mCollator.compare(lhs.appName, rhs.appName) + } + } + + class PkgNameComparator : Comparator { + override fun compare(lhs: AppInfo, rhs: AppInfo): Int { + return lhs.pkgName.compareTo(rhs.pkgName) + } + } + + class UidComparator : Comparator { + private val mPkgNameComparator = PkgNameComparator() + + override fun compare(lhs: AppInfo, rhs: AppInfo): Int { + return if (lhs.appUid < rhs.appUid) { + -1 + } else if (lhs.appUid > rhs.appUid) { + 1 + } else { + mPkgNameComparator.compare(lhs, rhs) + } + } + } + + class InstallTimeComparator : Comparator { + private val mPkgNameComparator = PkgNameComparator() + + override fun compare(lhs: AppInfo, rhs: AppInfo): Int { + return if (lhs.installTime < rhs.installTime) { + 1 + } else if (lhs.installTime > rhs.installTime) { + -1 + } else { + mPkgNameComparator.compare(lhs, rhs) + } + } + } + + class UpdateTimeComparator : Comparator { + private val mPkgNameComparator = PkgNameComparator() + + override fun compare(lhs: AppInfo, rhs: AppInfo): Int { + return if (lhs.updateTime < rhs.updateTime) { + 1 + } else if (lhs.updateTime > rhs.updateTime) { + -1 + } else { + mPkgNameComparator.compare(lhs, rhs) + } + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.java b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.java deleted file mode 100644 index 63e8f37..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.ycdev.android.lib.common.apps; - -@SuppressWarnings("WeakerAccess") -public class AppsLoadConfig { - /** - * Load the app name (true by default). - */ - public boolean loadLabel = true; - - /** - * Load the app icon (true by default). - */ - public boolean loadIcon = true; -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.kt new file mode 100644 index 0000000..c0aa5a4 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadConfig.kt @@ -0,0 +1,12 @@ +package me.ycdev.android.lib.common.apps + +data class AppsLoadConfig( + /** + * Load the app name (true by default). + */ + var loadLabel: Boolean = true, + /** + * Load the app icon (true by default). + */ + var loadIcon: Boolean = true +) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.java b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.java deleted file mode 100644 index f517299..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.java +++ /dev/null @@ -1,32 +0,0 @@ -package me.ycdev.android.lib.common.apps; - -@SuppressWarnings("WeakerAccess") -public class AppsLoadFilter { - /** - * Get mounted apps only (true by default). - */ - public boolean onlyMounted = true; - - /** - * Get enabled apps only (true by default). - */ - public boolean onlyEnabled = true; - - /** - * Include all system apps (true by default). - * Note: if this config is true, {@link #includeUpdatedSysApp} will be ignored; - * otherwise, {@link #includeUpdatedSysApp} will be checked. - */ - public boolean includeSysApp = true; - - /** - * Include updated system apps (true by default). - * Note: this config will be ignored if {@link #includeSysApp} is true. - */ - public boolean includeUpdatedSysApp = true; - - /** - * Include myself (true by default). - */ - public boolean includeMyself = true; -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.kt new file mode 100644 index 0000000..58935c2 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadFilter.kt @@ -0,0 +1,31 @@ +package me.ycdev.android.lib.common.apps + +class AppsLoadFilter { + /** + * Get mounted apps only (true by default). + */ + var onlyMounted = true + + /** + * Get enabled apps only (true by default). + */ + var onlyEnabled = true + + /** + * Include all system apps (true by default). + * Note: if this config is true, [.includeUpdatedSysApp] will be ignored; + * otherwise, [.includeUpdatedSysApp] will be checked. + */ + var includeSysApp = true + + /** + * Include updated system apps (true by default). + * Note: this config will be ignored if [.includeSysApp] is true. + */ + var includeUpdatedSysApp = true + + /** + * Include myself (true by default). + */ + var includeMyself = true +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadListener.java b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadListener.kt similarity index 61% rename from baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadListener.java rename to baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadListener.kt index dc5d6df..ccad73f 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadListener.java +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoadListener.kt @@ -1,14 +1,11 @@ -package me.ycdev.android.lib.common.apps; +package me.ycdev.android.lib.common.apps -@SuppressWarnings("WeakerAccess") -public interface AppsLoadListener { +interface AppsLoadListener { /** * This method can be used to cancel the apps loading. * @return false will be returned by default. */ - default boolean isCancelled() { - return false; - } + fun isCancelled(): Boolean = false /** * You can override this method to listen the loading progress and loaded app info. @@ -16,5 +13,5 @@ default boolean isCancelled() { * @param percent Value range [1, 2, ..., 100] * @param appInfo May be null */ - void onProgressUpdated(int percent, AppInfo appInfo); + fun onProgressUpdated(percent: Int, appInfo: AppInfo) } diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.java b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.java deleted file mode 100644 index c084617..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.java +++ /dev/null @@ -1,147 +0,0 @@ -package me.ycdev.android.lib.common.apps; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Build; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import me.ycdev.android.lib.common.utils.MiscUtils; -import me.ycdev.android.lib.common.utils.PackageUtils; -import me.ycdev.android.lib.common.utils.StringUtils; - -@SuppressWarnings("unused") -public class AppsLoader { - private Context mAppContext; - private PackageManager mPm; - private String mMyselfPkgName; - - @SuppressLint("StaticFieldLeak") - private static volatile AppsLoader sInstance; - - private AppsLoader(Context cxt) { - mAppContext = cxt.getApplicationContext(); - mPm = cxt.getPackageManager(); - mMyselfPkgName = cxt.getPackageName(); - } - - public static AppsLoader getInstance(Context cxt) { - if (sInstance == null) { - synchronized (AppsLoader.class) { - if (sInstance == null) { - sInstance = new AppsLoader(cxt); - } - } - } - return sInstance; - } - - @TargetApi(Build.VERSION_CODES.N) - public List loadInstalledApps(AppsLoadFilter filter, AppsLoadConfig config, - AppsLoadListener listener) { - HashMap allApps = new HashMap<>(); - List installedApps = mPm.getInstalledPackages(0); - int i = 0; - int n = installedApps.size(); - for (PackageInfo pkgInfo : installedApps) { - if (listener != null && listener.isCancelled()) { - return new ArrayList<>(allApps.values()); - } - - AppInfo item = retrieveAppInfo(pkgInfo, filter, config); - if (item != null) { - allApps.put(item.pkgName, item); - } - if (listener != null) { - i++; - int percent = MiscUtils.calcProgressPercent(1, 50, i, n); - listener.onProgressUpdated(percent, item); - } - } - - // The flag 'PackageManager.GET_UNINSTALLED_PACKAGES' may cause less information - // about currently installed applications to be returned! - // Such as, install time & update time, APK path, and so on. - installedApps = mPm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES); - i = 0; - n = installedApps.size(); - for (PackageInfo pkgInfo : installedApps) { - if (listener != null && listener.isCancelled()) { - return new ArrayList<>(allApps.values()); - } - - AppInfo item = null; - if (!allApps.containsKey(pkgInfo.packageName)) { - // unmounted app - item = retrieveAppInfo(pkgInfo, filter, config); - if (item != null) { - allApps.put(item.pkgName, item); - } - } - if (listener != null) { - i++; - int percent = MiscUtils.calcProgressPercent(51, 100, i, n); - listener.onProgressUpdated(percent, item); - } - } - - return new ArrayList<>(allApps.values()); - } - - private AppInfo retrieveAppInfo(PackageInfo pkgInfo, AppsLoadFilter filter, - AppsLoadConfig config) { - AppInfo item = new AppInfo(); - item.pkgName = pkgInfo.packageName; - item.appUid = pkgInfo.applicationInfo.uid; - item.sharedUid = pkgInfo.sharedUserId; - - int aiFlag = pkgInfo.applicationInfo.flags; - item.isSysApp = (aiFlag & ApplicationInfo.FLAG_SYSTEM) != 0; - item.isUpdatedSysApp = (aiFlag & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; - - item.versionName = pkgInfo.versionName; - item.versionCode = pkgInfo.versionCode; - - item.apkPath = pkgInfo.applicationInfo.sourceDir; - item.isDisabled = !PackageUtils.isPkgEnabled(mAppContext, pkgInfo.packageName); - // pkgInfo.applicationInfo.sourceDir may be null if the app is unmounted - item.isUnmounted = pkgInfo.applicationInfo.sourceDir == null || - !new File(pkgInfo.applicationInfo.sourceDir).exists(); - item.installTime = pkgInfo.firstInstallTime; - item.updateTime = pkgInfo.lastUpdateTime; - - if (filter.onlyMounted && item.isUnmounted) { - return null; - } - if (filter.onlyEnabled && item.isDisabled) { - return null; - } - if (!filter.includeSysApp && item.isSysApp) { - if (!filter.includeUpdatedSysApp) { - return null; // don't keep any system app and it's system app - } else if (!item.isUpdatedSysApp) { - return null; // only keep updated system app and it's not updated system app - } - } - if (!filter.includeMyself && item.pkgName.equals(mMyselfPkgName)) { - return null; - } - - // do heavy loading - if (config.loadLabel) { - item.appName = StringUtils.trimPrefixSpaces(pkgInfo.applicationInfo.loadLabel(mPm).toString()); - } - if (config.loadIcon) { - item.appIcon = pkgInfo.applicationInfo.loadIcon(mPm); - } - - return item; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt new file mode 100644 index 0000000..8b78ac1 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt @@ -0,0 +1,154 @@ +package me.ycdev.android.lib.common.apps + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import me.ycdev.android.lib.common.utils.MiscUtils +import me.ycdev.android.lib.common.utils.PackageUtils +import me.ycdev.android.lib.common.utils.StringUtils +import java.io.File +import java.util.ArrayList +import java.util.HashMap + +class AppsLoader private constructor(cxt: Context) { + private val appContext: Context + private val pm: PackageManager + private val myselfPkgName: String + + init { + appContext = cxt.applicationContext + pm = cxt.packageManager + myselfPkgName = cxt.packageName + } + + @TargetApi(Build.VERSION_CODES.N) + fun loadInstalledApps( + filter: AppsLoadFilter, + config: AppsLoadConfig, + listener: AppsLoadListener? + ): List { + val allApps = HashMap() + var installedApps = pm.getInstalledPackages(0) + var i = 0 + var n = installedApps.size + for (pkgInfo in installedApps) { + if (listener != null && listener.isCancelled()) { + return ArrayList(allApps.values) + } + + val item = retrieveAppInfo(pkgInfo, filter, config) + if (item != null) { + allApps[item.pkgName] = item + + if (listener != null) { + i++ + val percent = MiscUtils.calcProgressPercent(1, 50, i, n) + listener.onProgressUpdated(percent, item) + } + } + } + + // The flag 'PackageManager.GET_UNINSTALLED_PACKAGES' may cause less information + // about currently installed applications to be returned! + // Such as, install time & update time, APK path, and so on. + installedApps = pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES) + i = 0 + n = installedApps.size + for (pkgInfo in installedApps) { + if (listener != null && listener.isCancelled()) { + return ArrayList(allApps.values) + } + + var item: AppInfo? = null + if (!allApps.containsKey(pkgInfo.packageName)) { + // unmounted app + item = retrieveAppInfo(pkgInfo, filter, config) + if (item != null) { + allApps[item.pkgName] = item + } + } + if (listener != null && item != null) { + i++ + val percent = MiscUtils.calcProgressPercent(51, 100, i, n) + listener.onProgressUpdated(percent, item) + } + } + + return ArrayList(allApps.values) + } + + private fun retrieveAppInfo( + pkgInfo: PackageInfo, + filter: AppsLoadFilter, + config: AppsLoadConfig + ): AppInfo? { + val item = AppInfo(pkgInfo.packageName) + item.appUid = pkgInfo.applicationInfo.uid + item.sharedUid = pkgInfo.sharedUserId + + val aiFlag = pkgInfo.applicationInfo.flags + item.isSysApp = aiFlag and ApplicationInfo.FLAG_SYSTEM != 0 + item.isUpdatedSysApp = aiFlag and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0 + + item.versionName = pkgInfo.versionName + item.versionCode = pkgInfo.versionCode + + item.apkPath = pkgInfo.applicationInfo.sourceDir + item.isDisabled = !PackageUtils.isPkgEnabled(appContext, pkgInfo.packageName) + // pkgInfo.applicationInfo.sourceDir may be null if the app is unmounted + item.isUnmounted = + pkgInfo.applicationInfo.sourceDir == null || !File(pkgInfo.applicationInfo.sourceDir).exists() + item.installTime = pkgInfo.firstInstallTime + item.updateTime = pkgInfo.lastUpdateTime + + if (filter.onlyMounted && item.isUnmounted) { + return null + } + if (filter.onlyEnabled && item.isDisabled) { + return null + } + if (!filter.includeSysApp && item.isSysApp) { + if (!filter.includeUpdatedSysApp) { + return null // don't keep any system app and it's system app + } else if (!item.isUpdatedSysApp) { + return null // only keep updated system app and it's not updated system app + } + } + if (!filter.includeMyself && item.pkgName == myselfPkgName) { + return null + } + + // do heavy loading + if (config.loadLabel) { + item.appName = + StringUtils.trimPrefixSpaces(pkgInfo.applicationInfo.loadLabel(pm).toString()) + } + if (config.loadIcon) { + item.appIcon = pkgInfo.applicationInfo.loadIcon(pm) + } + + return item + } + + companion object { + + @SuppressLint("StaticFieldLeak") + @Volatile + private var instance: AppsLoader? = null + + fun getInstance(cxt: Context): AppsLoader { + if (instance == null) { + synchronized(AppsLoader::class.java) { + if (instance == null) { + instance = AppsLoader(cxt) + } + } + } + return instance!! + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.java b/baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.java deleted file mode 100644 index a2b65dc..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.java +++ /dev/null @@ -1,158 +0,0 @@ -package me.ycdev.android.lib.common.async; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.annotation.RestrictTo; - -import me.ycdev.android.lib.common.utils.Preconditions; -import timber.log.Timber; - -/** - * An utility class for processing tasks async. It's similar to {@link android.app.IntentService} - * and has following features: - *

  • 1. All tasks are executed one-by-one in a worker thread by {@link Handler}.
  • - *
  • 2. The worker thread is created when needed, and destroyed when not needed anymore. - * Also, you can customize the delay time for the thread's auto destroying.
  • - *

    - * Because of the background limits in Android O, we cannot use {@link android.app.IntentService} - * anymore in background (if the target API is set to Android O or higher versions). - * This class may be a possible replacement for it. - */ -@SuppressWarnings({"unused", "WeakerAccess"}) -public class AsyncTaskQueue { - private static final String TAG = "AsyncTaskQueue"; - private static final boolean DEV_LOG = false; - - private static final int MSG_MAIN_NEW_TASK = 1; - private static final int MSG_MAIN_REMOVE_TASK = 2; - private static final int MSG_MAIN_WORKER_THREAD_QUIT = 3; - - private static final int MSG_WORKER_NEW_TASK = 11; - private static final int MSG_WORKER_THREAD_QUIT = 12; - - public static final long WORKER_THREAD_AUTO_QUIT_DELAY_MIN = 10 * 1000; // 10 seconds - public static final long WORKER_THREAD_AUTO_QUIT_DELAY_DEFAULT = 30 * 1000; // 30 seconds - - @NonNull - private String mName; - private long mAutoQuitDelay = WORKER_THREAD_AUTO_QUIT_DELAY_DEFAULT; - private Handler mTaskHandler; - - public AsyncTaskQueue(@NonNull String name) { - mName = name; - } - - public void setWorkerThreadAutoQuitDelay(long delay) { - if (delay < WORKER_THREAD_AUTO_QUIT_DELAY_MIN) { - Timber.tag(TAG).w("Ignore the requested delay [%d]. Set it to the minimum value [%d].", - delay, WORKER_THREAD_AUTO_QUIT_DELAY_MIN); - mAutoQuitDelay = WORKER_THREAD_AUTO_QUIT_DELAY_MIN; - } else { - mAutoQuitDelay = delay; - } - } - - public void addTask(Runnable task) { - addTask(task, 0L); - } - - public void addTask(Runnable task, long delay) { - if (DEV_LOG) Timber.tag(TAG).d("addTask: %s, delay: %d", task, delay); - TaskParams params = new TaskParams(task, delay); - mMainHandler.obtainMessage(MSG_MAIN_NEW_TASK, params).sendToTarget(); - } - - public void removeTask(Runnable task) { - if (DEV_LOG) Timber.tag(TAG).d("removeTask: %s", task); - mMainHandler.obtainMessage(MSG_MAIN_REMOVE_TASK, task).sendToTarget(); - } - - @RestrictTo(RestrictTo.Scope.TESTS) - Handler getTaskHandler() { - return mTaskHandler; - } - - @MainThread - private void setupTaskHandler() { - Preconditions.checkMainThread(); - if (mTaskHandler == null) { - Timber.tag(TAG).d("Creating task thread"); - HandlerThread thread = new HandlerThread(mName); - thread.start(); - mTaskHandler = new Handler(thread.getLooper(), mTaskCallback); - } - } - - @MainThread - private void prepareForNewTask() { - mMainHandler.removeMessages(MSG_MAIN_WORKER_THREAD_QUIT); - setupTaskHandler(); - mTaskHandler.removeMessages(MSG_WORKER_THREAD_QUIT); - } - - private Handler mMainHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - if (DEV_LOG) Timber.tag(TAG).d("MainHandler#handleMessage: %s", msg); - if (msg.what == MSG_MAIN_NEW_TASK) { - TaskParams params = (TaskParams) msg.obj; - prepareForNewTask(); - Message taskMessage = mTaskHandler.obtainMessage(MSG_WORKER_NEW_TASK, params.task); - if (params.delay > 0) { - mTaskHandler.sendMessageDelayed(taskMessage, params.delay); - } else { - mTaskHandler.sendMessage(taskMessage); - } - } else if (msg.what == MSG_MAIN_REMOVE_TASK) { - Runnable task = (Runnable) msg.obj; - prepareForNewTask(); - mTaskHandler.removeMessages(MSG_WORKER_NEW_TASK, task); - } else if (msg.what == MSG_MAIN_WORKER_THREAD_QUIT) { - Timber.tag(TAG).d("task thread quiting"); - mTaskHandler.getLooper().quit(); - mTaskHandler = null; - } - } - }; - - private Handler.Callback mTaskCallback = new Handler.Callback() { - @Override - public boolean handleMessage(Message msg) { - if (DEV_LOG) Timber.tag(TAG).d("TaskHandler#handleMessage: %s", msg); - if (msg.what == MSG_WORKER_NEW_TASK) { - // Execute the task - Runnable task = (Runnable) msg.obj; - task.run(); - - // Post a cleaner task! - // Don't need to check null. If that happens, there MUST be bugs. - mTaskHandler.removeMessages(MSG_WORKER_THREAD_QUIT); - if (mAutoQuitDelay > 0) { - mTaskHandler.sendEmptyMessageDelayed(MSG_WORKER_THREAD_QUIT, mAutoQuitDelay); - } else { - mTaskHandler.sendEmptyMessage(MSG_WORKER_THREAD_QUIT); - } - } else if (msg.what == MSG_WORKER_THREAD_QUIT) { - mMainHandler.sendEmptyMessage(MSG_MAIN_WORKER_THREAD_QUIT); - } else { - return false; - } - - return true; - } - }; - - private static class TaskParams { - Runnable task; - long delay; - - TaskParams(Runnable task, long delay) { - this.task = task; - this.delay = delay; - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.kt new file mode 100644 index 0000000..e71b3d3 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/async/AsyncTaskQueue.kt @@ -0,0 +1,140 @@ +package me.ycdev.android.lib.common.async + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.Message +import androidx.annotation.MainThread +import androidx.annotation.RestrictTo + +import me.ycdev.android.lib.common.utils.Preconditions +import timber.log.Timber + +/** + * An utility class for processing tasks async. It's similar to [android.app.IntentService] + * and has following features: + * * 1. All tasks are executed one-by-one in a worker thread by [Handler]. + * * 2. The worker thread is created when needed, and destroyed when not needed anymore. + * Also, you can customize the delay time for the thread's auto destroying. + * + * + * Because of the background limits in Android O, we cannot use [android.app.IntentService] + * anymore in background (if the target API is set to Android O or higher versions). + * This class may be a possible replacement for it. + */ +class AsyncTaskQueue(private val name: String) { + private var autoQuitDelay = WORKER_THREAD_AUTO_QUIT_DELAY_DEFAULT + @get:RestrictTo(RestrictTo.Scope.TESTS) + internal var taskHandler: Handler? = null + private set + + private val mainHandler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + if (DEV_LOG) Timber.tag(TAG).d("MainHandler#handleMessage: %s", msg) + if (msg.what == MSG_MAIN_NEW_TASK) { + val params = msg.obj as TaskParams + prepareForNewTask() + val taskMessage = taskHandler!!.obtainMessage(MSG_WORKER_NEW_TASK, params.task) + if (params.delay > 0) { + taskHandler!!.sendMessageDelayed(taskMessage, params.delay) + } else { + taskHandler!!.sendMessage(taskMessage) + } + } else if (msg.what == MSG_MAIN_REMOVE_TASK) { + val task = msg.obj as Runnable + prepareForNewTask() + taskHandler!!.removeMessages(MSG_WORKER_NEW_TASK, task) + } else if (msg.what == MSG_MAIN_WORKER_THREAD_QUIT) { + Timber.tag(TAG).d("task thread quiting") + taskHandler!!.looper.quit() + taskHandler = null + } + } + } + + private val taskCallback = Handler.Callback { msg -> + if (DEV_LOG) Timber.tag(TAG).d("TaskHandler#handleMessage: %s", msg) + if (msg.what == MSG_WORKER_NEW_TASK) { + // Execute the task + val task = msg.obj as Runnable + task.run() + + // Post a cleaner task! + // Don't need to check null. If that happens, there MUST be bugs. + taskHandler!!.removeMessages(MSG_WORKER_THREAD_QUIT) + if (autoQuitDelay > 0) { + taskHandler!!.sendEmptyMessageDelayed(MSG_WORKER_THREAD_QUIT, autoQuitDelay) + } else { + taskHandler!!.sendEmptyMessage(MSG_WORKER_THREAD_QUIT) + } + } else if (msg.what == MSG_WORKER_THREAD_QUIT) { + mainHandler.sendEmptyMessage(MSG_MAIN_WORKER_THREAD_QUIT) + } else { + return@Callback false + } + + true + } + + fun setWorkerThreadAutoQuitDelay(delay: Long) { + if (delay < WORKER_THREAD_AUTO_QUIT_DELAY_MIN) { + Timber.tag(TAG).w( + "Ignore the requested delay [%d]. Set it to the minimum value [%d].", + delay, WORKER_THREAD_AUTO_QUIT_DELAY_MIN + ) + autoQuitDelay = WORKER_THREAD_AUTO_QUIT_DELAY_MIN + } else { + autoQuitDelay = delay + } + } + + @JvmOverloads + fun addTask(task: Runnable, delay: Long = 0L) { + if (DEV_LOG) Timber.tag(TAG).d("addTask: %s, delay: %d", task, delay) + val params = TaskParams(task, delay) + mainHandler.obtainMessage(MSG_MAIN_NEW_TASK, params).sendToTarget() + } + + fun removeTask(task: Runnable) { + if (DEV_LOG) Timber.tag(TAG).d("removeTask: %s", task) + mainHandler.obtainMessage(MSG_MAIN_REMOVE_TASK, task).sendToTarget() + } + + @MainThread + private fun setupTaskHandler() { + Preconditions.checkMainThread() + if (taskHandler == null) { + Timber.tag(TAG).d("Creating task thread") + val thread = HandlerThread(name) + thread.start() + taskHandler = Handler(thread.looper, taskCallback) + } + } + + @MainThread + private fun prepareForNewTask() { + mainHandler.removeMessages(MSG_MAIN_WORKER_THREAD_QUIT) + setupTaskHandler() + taskHandler!!.removeMessages(MSG_WORKER_THREAD_QUIT) + } + + private class TaskParams internal constructor( + internal var task: Runnable, + internal var delay: Long + ) + + companion object { + private const val TAG = "AsyncTaskQueue" + private const val DEV_LOG = false + + private const val MSG_MAIN_NEW_TASK = 1 + private const val MSG_MAIN_REMOVE_TASK = 2 + private const val MSG_MAIN_WORKER_THREAD_QUIT = 3 + + private const val MSG_WORKER_NEW_TASK = 11 + private const val MSG_WORKER_THREAD_QUIT = 12 + + const val WORKER_THREAD_AUTO_QUIT_DELAY_MIN = 10 * 1000L // 10 seconds + const val WORKER_THREAD_AUTO_QUIT_DELAY_DEFAULT = 30 * 1000L // 30 seconds + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.java b/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.java deleted file mode 100644 index e3cde31..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.java +++ /dev/null @@ -1,32 +0,0 @@ -package me.ycdev.android.lib.common.async; - -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; - -import java.util.List; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class HandlerExecutor implements ITaskExecutor { - private Handler mTaskHandler; - - public HandlerExecutor(@NonNull Looper looper) { - mTaskHandler = new Handler(looper); - } - - @Override - public void postTasks(@NonNull List tasks) { - for (Runnable task : tasks) { - mTaskHandler.post(task); - } - } - - @Override - public void clearTasks() { - mTaskHandler.removeCallbacksAndMessages(null); - } - - public static HandlerExecutor withMainLooper() { - return new HandlerExecutor(Looper.getMainLooper()); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.kt new file mode 100644 index 0000000..eeb02db --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerExecutor.kt @@ -0,0 +1,31 @@ +package me.ycdev.android.lib.common.async + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +open class HandlerExecutor(looper: Looper) : ITaskExecutor { + private val taskHandler: Handler = Handler(looper) + + override fun postTasks(tasks: List) { + for (task in tasks) { + taskHandler.post(task) + } + } + + override fun clearTasks() { + taskHandler.removeCallbacksAndMessages(null) + } + + companion object { + fun withMainLooper(): HandlerExecutor { + return HandlerExecutor(Looper.getMainLooper()) + } + + fun withHandlerThread(name: String): HandlerExecutor { + val thread = HandlerThread(name) + thread.start() + return HandlerExecutor(thread.looper) + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerThreadExecutor.java b/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerThreadExecutor.java deleted file mode 100644 index cae9a63..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/async/HandlerThreadExecutor.java +++ /dev/null @@ -1,21 +0,0 @@ -package me.ycdev.android.lib.common.async; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import androidx.annotation.NonNull; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class HandlerThreadExecutor extends HandlerExecutor { - private Handler mTaskHandler; - - public HandlerThreadExecutor(@NonNull String name) { - super(startThread(name)); - } - - private static Looper startThread(@NonNull String name) { - HandlerThread thread = new HandlerThread(name); - thread.start(); - return thread.getLooper(); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.java b/baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.java deleted file mode 100644 index fc0669b..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.java +++ /dev/null @@ -1,19 +0,0 @@ -package me.ycdev.android.lib.common.async; - -import androidx.annotation.NonNull; - -import java.util.List; - -public interface ITaskExecutor { - /** - * Post a task to execute. - *

    - * This method should return immediately and the task should be executed asynchronously. - */ - void postTasks(@NonNull List tasks); - - /** - * Clear all pending tasks. - */ - void clearTasks(); -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.kt new file mode 100644 index 0000000..73d961e --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/async/ITaskExecutor.kt @@ -0,0 +1,16 @@ +package me.ycdev.android.lib.common.async + +interface ITaskExecutor { + /** + * Post a task to execute. + * + * + * This method should return immediately and the task should be executed asynchronously. + */ + fun postTasks(tasks: List) + + /** + * Clear all pending tasks. + */ + fun clearTasks() +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.java b/baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.java deleted file mode 100644 index 09d9732..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.java +++ /dev/null @@ -1,359 +0,0 @@ -package me.ycdev.android.lib.common.async; - -import android.annotation.SuppressLint; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; - -import androidx.annotation.IntDef; -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicInteger; - -import androidx.annotation.VisibleForTesting; -import me.ycdev.android.lib.common.utils.DateTimeUtils; -import me.ycdev.android.lib.common.utils.Preconditions; -import timber.log.Timber; - -import static me.ycdev.android.lib.common.async.TaskScheduler.SchedulePolicy.IGNORE; -import static me.ycdev.android.lib.common.async.TaskScheduler.SchedulePolicy.NO_CHECK; -import static me.ycdev.android.lib.common.async.TaskScheduler.SchedulePolicy.REPLACE; -import static me.ycdev.android.lib.common.utils.ThreadUtils.isMainThread; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class TaskScheduler { - private static final String TAG = "TaskScheduler"; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({NO_CHECK, IGNORE, REPLACE}) - @interface SchedulePolicy { - int NO_CHECK = 1; - int IGNORE = 2; - int REPLACE = 3; - } - - private static final int MSG_ADD_TASK = 1; - private static final int MSG_REMOVE_TASK = 2; - private static final int MSG_CHECK_TASKS = 3; - private static final int MSG_CLEAR_TASKS = 4; - - @VisibleForTesting - static final long DEFAULT_CHECK_INTERVAL = 10_000; // 10 seconds - - private static AtomicInteger sTaskSchedulerId = new AtomicInteger(1); - - private ITaskExecutor mTaskExecutor; - private String mOwnerTag; - private long mCheckInterval = DEFAULT_CHECK_INTERVAL; - private boolean mLogEnabled = false; - - private Handler mMainHandler = new MainHandler(); - private ArrayList mTasks = new ArrayList<>(); - - // for test only - @VisibleForTesting int mCheckCount; - - public TaskScheduler(@NonNull ITaskExecutor executor, @NonNull String ownerTag) { - Preconditions.checkNotNull(executor); - Preconditions.checkNotNull(ownerTag); - mTaskExecutor = executor; - mOwnerTag = sTaskSchedulerId.getAndIncrement() + "-" + ownerTag; - } - - public void setCheckInterval(long interval) { - if (interval < 1000) { - throw new IllegalArgumentException("Interval less than 1 second is not allowed."); - } - mCheckInterval = interval; - } - - public void enableDebugLogs(boolean enable) { - mLogEnabled = enable; - } - - private static String schedulePolicyToString(@SchedulePolicy int policy) { - switch (policy) { - case NO_CHECK: return "NO_CHECK"; - case IGNORE: return "IGNORE"; - case REPLACE: return "REPLACE"; - default: throw new RuntimeException("Unknown policy: " + policy); - } - } - - private static void checkSchedulePolicy(@SchedulePolicy int policy) { - switch (policy) { - case NO_CHECK: - case IGNORE: - case REPLACE: - return; - default: throw new RuntimeException("Unknown policy: " + policy); - } - } - - public void scheduleAt(@NonNull Runnable task, long delayedMs) { - scheduleAt(task, delayedMs, NO_CHECK); - } - - public void scheduleAt(@NonNull Runnable task, long delayedMs, @SchedulePolicy int policy) { - checkSchedulePolicy(policy); - TaskInfo taskInfo = new TaskInfo(task, delayedMs); - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] schedule one-off task: %s, policy: %s", - mOwnerTag, taskInfo, schedulePolicyToString(policy)); - } - scheduleTask(taskInfo, policy); - } - - public void schedulePeriod(@NonNull Runnable task, long delayedMs, long periodMs) { - schedulePeriod(task, delayedMs, periodMs, NO_CHECK); - } - - public void schedulePeriod(@NonNull Runnable task, long delayedMs, long periodMs, - @SchedulePolicy int policy) { - checkSchedulePolicy(policy); - TaskInfo taskInfo = new TaskInfo(task, delayedMs, periodMs); - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] schedule period task: %s, policy: %s", - mOwnerTag, taskInfo, schedulePolicyToString(policy)); - } - scheduleTask(taskInfo, policy); - } - - private void scheduleTask(TaskInfo taskInfo, @SchedulePolicy int policy) { - if (isMainThread()) { - addTask(taskInfo, policy); - } else { - mMainHandler.obtainMessage(MSG_ADD_TASK, policy, 0, taskInfo).sendToTarget(); - } - } - - public void cancel(@NonNull Runnable task) { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] cancel task: %s", mOwnerTag, task); - } - if (isMainThread()) { - removeTask(task); - } else { - mMainHandler.obtainMessage(MSG_REMOVE_TASK, task).sendToTarget(); - } - } - - public void clear() { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] clear tasks", mOwnerTag); - } - if (isMainThread()) { - clearTasks(); - } else { - mMainHandler.sendEmptyMessage(MSG_CLEAR_TASKS); - } - } - - public void trigger() { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] trigger checking", mOwnerTag); - } - if (isMainThread()) { - checkTasks(); - } else { - mMainHandler.sendEmptyMessage(MSG_CHECK_TASKS); - } - } - - @MainThread - private void addTask(TaskInfo task, @SchedulePolicy int policy) { - boolean taskAdded = false; - if (policy == NO_CHECK) { - mTasks.add(task); - taskAdded = true; - } else { - int index = findTaskIndex(task.task); - if (index == -1) { - mTasks.add(task); - taskAdded = true; - } else { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] duplicate task found when add %s", mOwnerTag, task); - } - if (policy == REPLACE) { - mTasks.set(index, task); - taskAdded = true; - } //else: nothing to do for ignore - } - } - - if (taskAdded) { - scheduleCheckTask(task.delay); - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] addTask: %s, policy: %s", - mOwnerTag, task, schedulePolicyToString(policy)); - } - } - } - - @MainThread - private int findTaskIndex(@NonNull Runnable task) { - for (int i = 0; i < mTasks.size(); i++) { - TaskInfo info = mTasks.get(i); - if (info.task.equals(task)) { - return i; - } - } - return -1; - } - - @MainThread - private void removeTask(@NonNull Runnable task) { - for (int i = 0; i < mTasks.size(); /* empty */) { - TaskInfo info = mTasks.get(i); - if (info.task.equals(task)) { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] task removed: %s", mOwnerTag, info); - } - mTasks.remove(i); - } else { - i++; - } - } - } - - @MainThread - private void checkTasks() { - mCheckCount++; // for test only - if (mTasks.isEmpty()) { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] Tasks empty, cancel check.", mOwnerTag); - } - mMainHandler.removeMessages(MSG_CHECK_TASKS); - return; - } - - if (mLogEnabled) { - Timber.tag(TAG).v("[%s] check tasks, taskCount: %d", mOwnerTag, mTasks.size()); - } - Iterator it = mTasks.iterator(); - ArrayList pendingTasks = new ArrayList<>(); - long nextEventDelay = mCheckInterval; - while (it.hasNext()) { - TaskInfo info = it.next(); - if (SystemClock.elapsedRealtime() >= info.triggerAt) { - if (mLogEnabled) { - Timber.tag(TAG).d("[%s] task to execute: %s", mOwnerTag, info); - } - pendingTasks.add(info.task); - if (info.period > 0) { - info.triggerAt = SystemClock.elapsedRealtime() + info.period; - } else { - it.remove(); - info = null; // mark it removed from queue - } - } - - if (info != null) { - long timeout = info.triggerAt - SystemClock.elapsedRealtime(); - if (timeout < nextEventDelay) { - nextEventDelay = timeout; - } - } - } - - if (mLogEnabled) { - Timber.tag(TAG).v("[%s] next check at %s", mOwnerTag, - DateTimeUtils.getReadableTimeStamp(System.currentTimeMillis() + nextEventDelay)); - } - mMainHandler.removeMessages(MSG_CHECK_TASKS); - mMainHandler.sendEmptyMessageDelayed(MSG_CHECK_TASKS, nextEventDelay); - - if (pendingTasks.size() > 0) { - mTaskExecutor.postTasks(pendingTasks); - } - } - - @MainThread - private void clearTasks() { - mTasks.clear(); - mTaskExecutor.clearTasks(); - } - - @MainThread - private void scheduleCheckTask(long delay) { - if (delay > mCheckInterval) { - delay = mCheckInterval; - } - mMainHandler.sendEmptyMessageDelayed(MSG_CHECK_TASKS, delay); - } - - @SuppressLint("HandlerLeak") - private class MainHandler extends Handler { - MainHandler() { - super(Looper.getMainLooper()); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_ADD_TASK: { - TaskInfo task = (TaskInfo) msg.obj; - int policy = msg.arg1; - addTask(task, policy); - break; - } - - case MSG_REMOVE_TASK: { - Runnable task = (Runnable) msg.obj; - removeTask(task); - break; - } - - case MSG_CHECK_TASKS: { - checkTasks(); - break; - } - - case MSG_CLEAR_TASKS: { - clearTasks(); - break; - } - } - } - } -} - -class TaskInfo { - private static AtomicInteger sTaskId = new AtomicInteger(1); - - private int taskId; - Runnable task; - long delay; - long period = -1; - long triggerAt; - - TaskInfo(@NonNull Runnable task, long delay) { - this.taskId = sTaskId.getAndIncrement(); - this.task = task; - this.delay = delay; - this.triggerAt = SystemClock.elapsedRealtime() + delay; - } - - TaskInfo(@NonNull Runnable task, long delay, long period) { - this.taskId = sTaskId.getAndIncrement(); - this.task = task; - this.delay = delay; - this.period = period; - this.triggerAt = SystemClock.elapsedRealtime() + delay; - } - - @Override - public String toString() { - long timestamp = System.currentTimeMillis() - (SystemClock.elapsedRealtime() - triggerAt); - return "TaskInfo[id=" + taskId + ", delay=" + delay - + ", triggerAt=" + DateTimeUtils.getReadableTimeStamp(timestamp) - + ", period=" + period + "]"; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.kt new file mode 100644 index 0000000..3864000 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/async/TaskScheduler.kt @@ -0,0 +1,353 @@ +package me.ycdev.android.lib.common.async + +import android.annotation.SuppressLint +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.SystemClock +import androidx.annotation.IntDef +import androidx.annotation.MainThread +import androidx.annotation.VisibleForTesting +import me.ycdev.android.lib.common.utils.DateTimeUtils +import me.ycdev.android.lib.common.utils.Preconditions +import me.ycdev.android.lib.common.utils.ThreadUtils.isMainThread +import timber.log.Timber +import java.util.ArrayList +import java.util.concurrent.atomic.AtomicInteger + +class TaskScheduler(private val mTaskExecutor: ITaskExecutor, ownerTag: String) { + private val mOwnerTag: String + private var mCheckInterval = DEFAULT_CHECK_INTERVAL + private var mLogEnabled = false + + private val mMainHandler = MainHandler() + private val mTasks = ArrayList() + + // for test only + @VisibleForTesting + internal var mCheckCount: Int = 0 + + init { + Preconditions.checkNotNull(mTaskExecutor) + Preconditions.checkNotNull(ownerTag) + mOwnerTag = sTaskSchedulerId.getAndIncrement().toString() + "-" + ownerTag + } + + fun setCheckInterval(interval: Long) { + if (interval < 1000) { + throw IllegalArgumentException("Interval less than 1 second is not allowed.") + } + mCheckInterval = interval + } + + fun enableDebugLogs(enable: Boolean) { + mLogEnabled = enable + } + + @JvmOverloads + fun scheduleAt( + task: Runnable, + delayedMs: Long, + @SchedulePolicy policy: Int = SCHEDULE_POLICY_NO_CHECK + ) { + checkSchedulePolicy(policy) + val taskInfo = TaskInfo(task, delayedMs) + if (mLogEnabled) { + Timber.tag(TAG).d( + "[%s] schedule one-off task: %s, policy: %s", + mOwnerTag, taskInfo, schedulePolicyToString(policy) + ) + } + scheduleTask(taskInfo, policy) + } + + @JvmOverloads + fun schedulePeriod( + task: Runnable, + delayedMs: Long, + periodMs: Long, + @SchedulePolicy policy: Int = SCHEDULE_POLICY_NO_CHECK + ) { + checkSchedulePolicy(policy) + val taskInfo = TaskInfo(task, delayedMs, periodMs) + if (mLogEnabled) { + Timber.tag(TAG).d( + "[%s] schedule period task: %s, policy: %s", + mOwnerTag, taskInfo, schedulePolicyToString(policy) + ) + } + scheduleTask(taskInfo, policy) + } + + private fun scheduleTask(taskInfo: TaskInfo, @SchedulePolicy policy: Int) { + if (isMainThread) { + addTask(taskInfo, policy) + } else { + mMainHandler.obtainMessage(MSG_ADD_TASK, policy, 0, taskInfo).sendToTarget() + } + } + + fun cancel(task: Runnable) { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] cancel task: %s", mOwnerTag, task) + } + if (isMainThread) { + removeTask(task) + } else { + mMainHandler.obtainMessage(MSG_REMOVE_TASK, task).sendToTarget() + } + } + + fun clear() { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] clear tasks", mOwnerTag) + } + if (isMainThread) { + clearTasks() + } else { + mMainHandler.sendEmptyMessage(MSG_CLEAR_TASKS) + } + } + + fun trigger() { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] trigger checking", mOwnerTag) + } + if (isMainThread) { + checkTasks() + } else { + mMainHandler.sendEmptyMessage(MSG_CHECK_TASKS) + } + } + + @MainThread + private fun addTask(task: TaskInfo, @SchedulePolicy policy: Int) { + var taskAdded = false + if (policy == SCHEDULE_POLICY_NO_CHECK) { + mTasks.add(task) + taskAdded = true + } else { + val index = findTaskIndex(task.task) + if (index == -1) { + mTasks.add(task) + taskAdded = true + } else { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] duplicate task found when add %s", mOwnerTag, task) + } + if (policy == SCHEDULE_POLICY_REPLACE) { + mTasks[index] = task + taskAdded = true + } // else: nothing to do for ignore + } + } + + if (taskAdded) { + scheduleCheckTask(task.delay) + if (mLogEnabled) { + Timber.tag(TAG).d( + "[%s] addTask: %s, policy: %s", + mOwnerTag, task, schedulePolicyToString(policy) + ) + } + } + } + + @MainThread + private fun findTaskIndex(task: Runnable): Int { + for (i in mTasks.indices) { + val info = mTasks[i] + if (info.task == task) { + return i + } + } + return -1 + } + + @MainThread + private fun removeTask(task: Runnable) { + var i = 0 + while (i < mTasks.size) { + val info = mTasks[i] + if (info.task == task) { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] task removed: %s", mOwnerTag, info) + } + mTasks.removeAt(i) + } else { + i++ + } + } /* empty */ + } + + @MainThread + private fun checkTasks() { + mCheckCount++ // for test only + if (mTasks.isEmpty()) { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] Tasks empty, cancel check.", mOwnerTag) + } + mMainHandler.removeMessages(MSG_CHECK_TASKS) + return + } + + if (mLogEnabled) { + Timber.tag(TAG).v("[%s] check tasks, taskCount: %d", mOwnerTag, mTasks.size) + } + val it = mTasks.iterator() + val pendingTasks = ArrayList() + var nextEventDelay = mCheckInterval + while (it.hasNext()) { + var info: TaskInfo? = it.next() + if (SystemClock.elapsedRealtime() >= info!!.triggerAt) { + if (mLogEnabled) { + Timber.tag(TAG).d("[%s] task to execute: %s", mOwnerTag, info) + } + pendingTasks.add(info.task) + if (info.period > 0) { + info.triggerAt = SystemClock.elapsedRealtime() + info.period + } else { + it.remove() + info = null // mark it removed from queue + } + } + + if (info != null) { + val timeout = info.triggerAt - SystemClock.elapsedRealtime() + if (timeout < nextEventDelay) { + nextEventDelay = timeout + } + } + } + + if (mLogEnabled) { + Timber.tag(TAG).v( + "[%s] next check at %s", mOwnerTag, + DateTimeUtils.getReadableTimeStamp(System.currentTimeMillis() + nextEventDelay) + ) + } + mMainHandler.removeMessages(MSG_CHECK_TASKS) + mMainHandler.sendEmptyMessageDelayed(MSG_CHECK_TASKS, nextEventDelay) + + if (pendingTasks.size > 0) { + mTaskExecutor.postTasks(pendingTasks) + } + } + + @MainThread + private fun clearTasks() { + mTasks.clear() + mTaskExecutor.clearTasks() + } + + @MainThread + private fun scheduleCheckTask(delay: Long) { + var delayTmp = delay + if (delayTmp > mCheckInterval) { + delayTmp = mCheckInterval + } + mMainHandler.sendEmptyMessageDelayed(MSG_CHECK_TASKS, delayTmp) + } + + @SuppressLint("HandlerLeak") + private inner class MainHandler internal constructor() : Handler(Looper.getMainLooper()) { + + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_ADD_TASK -> { + val task = msg.obj as TaskInfo + val policy = msg.arg1 + addTask(task, policy) + } + + MSG_REMOVE_TASK -> { + val task = msg.obj as Runnable + removeTask(task) + } + + MSG_CHECK_TASKS -> { + checkTasks() + } + + MSG_CLEAR_TASKS -> { + clearTasks() + } + } + } + } + + @Retention(AnnotationRetention.SOURCE) + @IntDef(SCHEDULE_POLICY_NO_CHECK, SCHEDULE_POLICY_IGNORE, SCHEDULE_POLICY_REPLACE) + annotation class SchedulePolicy + + companion object { + private val TAG = "TaskScheduler" + + const val SCHEDULE_POLICY_NO_CHECK = 1 + const val SCHEDULE_POLICY_IGNORE = 2 + const val SCHEDULE_POLICY_REPLACE = 3 + + private const val MSG_ADD_TASK = 1 + private const val MSG_REMOVE_TASK = 2 + private const val MSG_CHECK_TASKS = 3 + private const val MSG_CLEAR_TASKS = 4 + + @VisibleForTesting + internal val DEFAULT_CHECK_INTERVAL: Long = 10000 // 10 seconds + + private val sTaskSchedulerId = AtomicInteger(1) + + private fun schedulePolicyToString(@SchedulePolicy policy: Int): String { + when (policy) { + SCHEDULE_POLICY_NO_CHECK -> return "NO_CHECK" + SCHEDULE_POLICY_IGNORE -> return "IGNORE" + SCHEDULE_POLICY_REPLACE -> return "REPLACE" + else -> throw RuntimeException("Unknown policy: $policy") + } + } + + private fun checkSchedulePolicy(@SchedulePolicy policy: Int) { + when (policy) { + SCHEDULE_POLICY_NO_CHECK, + SCHEDULE_POLICY_IGNORE, + SCHEDULE_POLICY_REPLACE -> return + else -> throw RuntimeException("Unknown policy: $policy") + } + } + } +} + +internal class TaskInfo { + + private var taskId: Int = 0 + var task: Runnable + var delay: Long = 0 + var period: Long = -1 + var triggerAt: Long = 0 + + constructor(task: Runnable, delay: Long) { + this.taskId = sTaskId.getAndIncrement() + this.task = task + this.delay = delay + this.triggerAt = SystemClock.elapsedRealtime() + delay + } + + constructor(task: Runnable, delay: Long, period: Long) { + this.taskId = sTaskId.getAndIncrement() + this.task = task + this.delay = delay + this.period = period + this.triggerAt = SystemClock.elapsedRealtime() + delay + } + + override fun toString(): String { + val timestamp = System.currentTimeMillis() - (SystemClock.elapsedRealtime() - triggerAt) + return ("TaskInfo[id=" + taskId + ", delay=" + delay + + ", triggerAt=" + DateTimeUtils.getReadableTimeStamp(timestamp) + + ", period=" + period + "]") + } + + companion object { + private val sTaskId = AtomicInteger(1) + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.java b/baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.java deleted file mode 100644 index 4ca2f19..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.ycdev.android.lib.common.base; - -@SuppressWarnings("unused") -@FunctionalInterface -public interface ICallback { - void callback(Object... params); -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.kt new file mode 100644 index 0000000..ba8c1af --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/base/ICallback.kt @@ -0,0 +1,6 @@ +package me.ycdev.android.lib.common.base + +@FunctionalInterface +interface ICallback { + fun callback(vararg params: Any) +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/compat/PowerManagerCompat.java b/baseLib/src/main/java/me/ycdev/android/lib/common/compat/PowerManagerCompat.java deleted file mode 100644 index 1b94408..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/compat/PowerManagerCompat.java +++ /dev/null @@ -1,17 +0,0 @@ -package me.ycdev.android.lib.common.compat; - -import android.annotation.TargetApi; -import android.os.Build; -import android.os.PowerManager; - -public class PowerManagerCompat { - @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) - public static boolean isScreenOn(PowerManager pm) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { - return pm.isInteractive(); - } else { - //noinspection deprecation - return pm.isScreenOn(); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/compat/ViewsCompat.java b/baseLib/src/main/java/me/ycdev/android/lib/common/compat/ViewsCompat.java deleted file mode 100644 index 2889b75..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/compat/ViewsCompat.java +++ /dev/null @@ -1,26 +0,0 @@ -package me.ycdev.android.lib.common.compat; - -import me.ycdev.android.lib.common.utils.AndroidVersionUtils; - -import android.annotation.TargetApi; -import android.os.Build; -import androidx.annotation.NonNull; -import android.widget.ImageView; - -@SuppressWarnings("unused") -public class ViewsCompat { - /** - * Set alpha of the image view - * @param imageView The target image view - * @param alpha [0~255] - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - @SuppressWarnings("deprecation") - public static void setImageViewAlpha(@NonNull ImageView imageView, int alpha) { - if (AndroidVersionUtils.hasJellyBean()) { - imageView.setImageAlpha(alpha); - } else { - imageView.setAlpha(alpha); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.java b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.java deleted file mode 100644 index 5b61103..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.ycdev.android.lib.common.dbmgr; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import androidx.annotation.NonNull; - -public interface SQLiteDbCreator { - SQLiteDatabase createDb(@NonNull Context cxt); -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.kt new file mode 100644 index 0000000..ce4d535 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbCreator.kt @@ -0,0 +1,8 @@ +package me.ycdev.android.lib.common.dbmgr + +import android.content.Context +import android.database.sqlite.SQLiteDatabase + +interface SQLiteDbCreator { + fun createDb(cxt: Context): SQLiteDatabase +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.java b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.java deleted file mode 100644 index c8f8e7f..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.java +++ /dev/null @@ -1,92 +0,0 @@ -package me.ycdev.android.lib.common.dbmgr; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import androidx.annotation.NonNull; - -import java.util.HashMap; - -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings("unused") -public class SQLiteDbMgr { - private static final String TAG = "SQLiteDbMgr"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static class DbInfo { - SQLiteDatabase db; - int referenceCount; - } - - private Context mAppContext; - private HashMap, DbInfo> mOpenHelpers = new HashMap<>(); - - @SuppressLint("StaticFieldLeak") - private static volatile SQLiteDbMgr sInstance; - - private SQLiteDbMgr(Context cxt) { - mAppContext = cxt.getApplicationContext(); - } - - private static SQLiteDbMgr getInstance(Context cxt) { - if (sInstance == null) { - synchronized (SQLiteDbMgr.class) { - if (sInstance == null) { - sInstance = new SQLiteDbMgr(cxt); - } - } - } - return sInstance; - } - - private SQLiteDatabase acquireDatabase(Class dbInfoClass) { - if (DEBUG) LibLogger.d(TAG, "acquire DB: " + dbInfoClass.getName()); - SQLiteDatabase db; - synchronized (SQLiteDbMgr.class) { - DbInfo info = mOpenHelpers.get(dbInfoClass); - if (info == null) { - try { - if (DEBUG) LibLogger.d(TAG, "create DB: " + dbInfoClass.getName()); - SQLiteDbCreator helper = dbInfoClass.newInstance(); - info = new DbInfo(); - info.db = helper.createDb(mAppContext); - info.referenceCount = 0; - mOpenHelpers.put(dbInfoClass, info); - } catch (Exception e) { - throw new RuntimeException("failed to create SQLiteOpenHelper instance", e); - } - } - info.referenceCount++; - db = info.db; - } - return db; - } - - private void releaseDatabase(Class dbInfoClass) { - if (DEBUG) LibLogger.d(TAG, "release DB: " + dbInfoClass.getName()); - synchronized (SQLiteDbMgr.class) { - DbInfo info = mOpenHelpers.get(dbInfoClass); - if (info != null) { - info.referenceCount--; - if (info.referenceCount == 0) { - if (DEBUG) LibLogger.d(TAG, "close DB: " + dbInfoClass.getName()); - info.db.close(); - info.db = null; - mOpenHelpers.remove(dbInfoClass); - } - } - } - } - - public static SQLiteDatabase acquireDatabase(@NonNull Context cxt, - @NonNull Class dbInfoClass) { - return getInstance(cxt).acquireDatabase(dbInfoClass); - } - - public static void releaseDatabase(@NonNull Context cxt, - @NonNull Class dbInfoClass) { - getInstance(cxt).releaseDatabase(dbInfoClass); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt new file mode 100644 index 0000000..c487775 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt @@ -0,0 +1,92 @@ +package me.ycdev.android.lib.common.dbmgr + +import android.annotation.SuppressLint +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import timber.log.Timber + +import java.util.HashMap + +@Suppress("unused") +class SQLiteDbMgr private constructor(cxt: Context) { + + private val mAppContext: Context = cxt.applicationContext + private val mOpenHelpers = HashMap, DbInfo>() + + private class DbInfo { + internal var db: SQLiteDatabase? = null + internal var referenceCount: Int = 0 + } + + private fun acquireDatabase(dbInfoClass: Class): SQLiteDatabase? { + Timber.tag(TAG).d("acquire DB: %s", dbInfoClass.name) + val db: SQLiteDatabase? + synchronized(SQLiteDbMgr::class.java) { + var info = mOpenHelpers[dbInfoClass] + if (info == null) { + try { + Timber.tag(TAG).d("create DB: %s", dbInfoClass.name) + val helper = dbInfoClass.newInstance() + info = DbInfo() + info.db = helper.createDb(mAppContext) + info.referenceCount = 0 + mOpenHelpers[dbInfoClass] = info + } catch (e: Exception) { + throw RuntimeException("failed to create SQLiteOpenHelper instance", e) + } + } + info.referenceCount++ + db = info.db + } + return db + } + + private fun releaseDatabase(dbInfoClass: Class) { + Timber.tag(TAG).d("release DB: %s", dbInfoClass.name) + synchronized(SQLiteDbMgr::class.java) { + val info = mOpenHelpers[dbInfoClass] + if (info != null) { + info.referenceCount-- + if (info.referenceCount == 0) { + Timber.tag(TAG).d("close DB: %s", dbInfoClass.name) + info.db!!.close() + info.db = null + mOpenHelpers.remove(dbInfoClass) + } + } + } + } + + companion object { + private const val TAG = "SQLiteDbMgr" + + @SuppressLint("StaticFieldLeak") + @Volatile + private var instance: SQLiteDbMgr? = null + + private fun getInstance(cxt: Context): SQLiteDbMgr { + if (instance == null) { + synchronized(SQLiteDbMgr::class.java) { + if (instance == null) { + instance = SQLiteDbMgr(cxt) + } + } + } + return instance!! + } + + fun acquireDatabase( + cxt: Context, + dbInfoClass: Class + ): SQLiteDatabase? { + return getInstance(cxt).acquireDatabase(dbInfoClass) + } + + fun releaseDatabase( + cxt: Context, + dbInfoClass: Class + ) { + getInstance(cxt).releaseDatabase(dbInfoClass) + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.java deleted file mode 100644 index a436925..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.java +++ /dev/null @@ -1,142 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.app; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.internalapi.android.os.ServiceManagerIA; -import me.ycdev.android.lib.common.internalapi.android.os.UserHandleIA; -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class ActivityManagerIA { - private static final String TAG = "ActivityManagerIA"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static final int API_VERSION_1 = 1; - private static final int API_VERSION_2 = 2; - - private static Method sMtd_asInterface; - - private static Class sClass_IActivityManager; - private static Method sMtd_forceStopPackage; - private static int sVersion_forceStopPackage; - - static { - try { - Class stubClass = Class.forName("android.app.ActivityManagerNative", false, - Thread.currentThread().getContextClassLoader()); - sMtd_asInterface = stubClass.getMethod("asInterface", IBinder.class); - - sClass_IActivityManager = Class.forName("android.app.IActivityManager", false, - Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - if (DEBUG) LibLogger.w(TAG, "class not found", e); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - private ActivityManagerIA() { - // nothing to do - } - - /** - * Get "android.os.IActivityManager" object from the service binder. - * @return null will be returned if failed - */ - @Nullable - public static Object asInterface(@NonNull IBinder binder) { - if (sMtd_asInterface != null) { - try { - return sMtd_asInterface.invoke(null, binder); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #asInterface()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #asInterface() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#asInterface() not available"); - } - return null; - } - - /** - * Get "android.os.IActivityManager" object from the service manager. - * @return null will be returned if failed - */ - @Nullable - public static Object getIActivityManager() { - IBinder binder = ServiceManagerIA.getService(Context.ACTIVITY_SERVICE); - if (binder != null) { - return asInterface(binder); - } - return null; - } - - private static void reflect_forceStopPackage() { - if (sMtd_forceStopPackage != null || sClass_IActivityManager == null) { - return; - } - - try { - try { - // Android 2.2 ~ Android 4.1: void forceStopPackage(String packageName); - sMtd_forceStopPackage = sClass_IActivityManager.getMethod("forceStopPackage", String.class); - sVersion_forceStopPackage = API_VERSION_1; - } catch (NoSuchMethodException e) { - // Android 4.2: void forceStopPackage(String packageName, int userId); - sMtd_forceStopPackage = sClass_IActivityManager.getMethod("forceStopPackage", - String.class, int.class); - sVersion_forceStopPackage = API_VERSION_2; - } - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Force stop the specified app. - * @param service The "android.os.IActivityManager" object. - * @param pkgName The package name of the app - * @see #asInterface(android.os.IBinder) - */ - public static void forceStopPackage(@NonNull Object service, @NonNull String pkgName) { - reflect_forceStopPackage(); - if (sMtd_forceStopPackage != null) { - try { - if (sVersion_forceStopPackage == API_VERSION_1) { - sMtd_forceStopPackage.invoke(service, pkgName); - } else if (sVersion_forceStopPackage == API_VERSION_2) { - sMtd_forceStopPackage.invoke(service, pkgName, UserHandleIA.myUserId()); - } else { - if (DEBUG) LibLogger.e(TAG, "reboot, unknown api version: " + sVersion_forceStopPackage); - } - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #forceStopPackage()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #forceStopPackage() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#forceStopPackage() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_forceStopPackage() { - reflect_forceStopPackage(); - return sMtd_forceStopPackage != null; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.kt new file mode 100644 index 0000000..ae18235 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/app/ActivityManagerIA.kt @@ -0,0 +1,133 @@ +package me.ycdev.android.lib.common.internalapi.android.app + +import android.annotation.SuppressLint +import android.content.Context +import android.os.IBinder +import androidx.annotation.RestrictTo +import me.ycdev.android.lib.common.internalapi.android.os.ServiceManagerIA +import me.ycdev.android.lib.common.internalapi.android.os.UserHandleIA +import timber.log.Timber +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@Suppress("unused") +@SuppressLint("PrivateApi") +object ActivityManagerIA { + private const val TAG = "ActivityManagerIA" + + private const val API_VERSION_1 = 1 + private const val API_VERSION_2 = 2 + + private var mtd_asInterface: Method? = null + + private var class_IActivityManager: Class<*>? = null + private var mtd_forceStopPackage: Method? = null + private var version_forceStopPackage: Int = 0 + + init { + try { + val stubClass = Class.forName( + "android.app.ActivityManagerNative", false, + Thread.currentThread().contextClassLoader + ) + mtd_asInterface = stubClass.getMethod("asInterface", IBinder::class.java) + + class_IActivityManager = Class.forName( + "android.app.IActivityManager", false, + Thread.currentThread().contextClassLoader + ) + } catch (e: ClassNotFoundException) { + Timber.tag(TAG).w(e, "class not found") + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Get "android.os.IActivityManager" object from the service manager. + * @return null will be returned if failed + */ + fun getIActivityManager(): Any? { + val binder = ServiceManagerIA.getService(Context.ACTIVITY_SERVICE) ?: return null + return asInterface(binder) + } + + /** + * Get "android.os.IActivityManager" object from the service binder. + * @return null will be returned if failed + */ + fun asInterface(binder: IBinder): Any? { + if (mtd_asInterface != null) { + try { + return mtd_asInterface!!.invoke(null, binder) + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #asInterface()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #asInterface() more") + } + } else { + Timber.tag(TAG).w("#asInterface() not available") + } + return null + } + + private fun reflectForceStopPackage() { + if (mtd_forceStopPackage != null || class_IActivityManager == null) { + return + } + + try { + try { + // Android 2.2 ~ Android 4.1: void forceStopPackage(String packageName); + mtd_forceStopPackage = + class_IActivityManager!!.getMethod("forceStopPackage", String::class.java) + version_forceStopPackage = API_VERSION_1 + } catch (e: NoSuchMethodException) { + // Android 4.2: void forceStopPackage(String packageName, int userId); + mtd_forceStopPackage = class_IActivityManager!!.getMethod( + "forceStopPackage", + String::class.java, Int::class.javaPrimitiveType + ) + version_forceStopPackage = API_VERSION_2 + } + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Force stop the specified app. + * @param service The "android.os.IActivityManager" object. + * @param pkgName The package name of the app + * @see .asInterface + */ + fun forceStopPackage(service: Any, pkgName: String) { + reflectForceStopPackage() + if (mtd_forceStopPackage != null) { + try { + when (version_forceStopPackage) { + API_VERSION_1 -> mtd_forceStopPackage!!.invoke(service, pkgName) + API_VERSION_2 -> mtd_forceStopPackage!!.invoke(service, pkgName, UserHandleIA.myUserId()) + else -> Timber.tag(TAG).e( + "reboot, unknown api version: $version_forceStopPackage" + ) + } + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #forceStopPackage()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #forceStopPackage() more") + } + } else { + Timber.tag(TAG).w("#forceStopPackage() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectForceStopPackage(): Boolean { + reflectForceStopPackage() + return mtd_forceStopPackage != null + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.java deleted file mode 100644 index b09affe..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.java +++ /dev/null @@ -1,140 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.os; - -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -import android.annotation.SuppressLint; -import android.os.Environment; -import androidx.annotation.Nullable; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class EnvironmentIA { - private static final String TAG = "EnvironmentIA"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static Method sMtd_getExternalStorageAndroidDataDir; - private static Method sMtd_isEncryptedFilesystemEnabled; - private static Method sMtd_getSecureDataDirectory; - private static Method sMtd_getSystemSecureDirectory; - - static { - try { - // API 8: File getExternalStorageAndroidDataDir() - sMtd_getExternalStorageAndroidDataDir = Environment.class.getMethod( - "getExternalStorageAndroidDataDir"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "#getExternalStorageAndroidDataDir() not found", e); - } - - try { - // API 9: boolean isEncryptedFilesystemEnabled() - sMtd_isEncryptedFilesystemEnabled = Environment.class.getMethod( - "isEncryptedFilesystemEnabled"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "#isEncryptedFilesystemEnabled() not found", e); - } - - try { - // API 9: File getSecureDataDirectory() - sMtd_getSecureDataDirectory = Environment.class.getMethod( - "getSecureDataDirectory"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "#getSecureDataDirectory() not found", e); - } - - try { - // API 9: File getSystemSecureDirectory() - sMtd_getSystemSecureDirectory = Environment.class.getMethod( - "getSystemSecureDirectory"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "#getSystemSecureDirectory() not found", e); - } - } - - private EnvironmentIA() { - // nothing to do - } - - /** - * Same to the hided method Environment#getExternalStorageAndroidDataDir() (API 8) - * @return null may be returned if the method not supported or failed to invoke it - */ - @Nullable - public static File getExternalStorageAndroidDataDir() { - if (sMtd_getExternalStorageAndroidDataDir != null) { - try { - return (File) sMtd_getExternalStorageAndroidDataDir.invoke(null); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #getExternalStorageAndroidDataDir()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #getExternalStorageAndroidDataDir() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#getExternalStorageAndroidDataDir() not found"); - } - return null; - } - - /** - * Same to the hided Environment#isEncryptedFilesystemEnabled() (API 9) - */ - public static boolean isEncryptedFilesystemEnabled() { - if (sMtd_isEncryptedFilesystemEnabled != null) { - try { - return (Boolean) sMtd_isEncryptedFilesystemEnabled.invoke(null); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #isEncryptedFilesystemEnabled()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #isEncryptedFilesystemEnabled() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#isEncryptedFilesystemEnabled() not found"); - } - return false; - } - - /** - * Same to the hided method Environment#getSecureDataDirectory() (API 9) - * @return null may be returned if the method not supported or failed to invoke it - */ - @Nullable - public static File getSecureDataDirectory() { - if (sMtd_getSecureDataDirectory != null) { - try { - return (File) sMtd_getSecureDataDirectory.invoke(null); - } catch (IllegalAccessException e) { - LibLogger.w(TAG, "Failed to invoke #getSecureDataDirectory()", e); - } catch (InvocationTargetException e) { - LibLogger.w(TAG, "Failed to invoke #getSecureDataDirectory() more", e); - } - } else { - LibLogger.w(TAG, "#getSecureDataDirectory() not found"); - } - return null; - } - - /** - * Same to the hided method Environment#getSystemSecureDirectory() (API 9) - * @return null may be returned if the method not supported or failed to invoke it - */ - @Nullable - public static File getSystemSecureDirectory() { - if (sMtd_getSystemSecureDirectory != null) { - try { - return (File) sMtd_getSystemSecureDirectory.invoke(null); - } catch (IllegalAccessException e) { - LibLogger.w(TAG, "Failed to invoke #getSystemSecureDirectory()", e); - } catch (InvocationTargetException e) { - LibLogger.w(TAG, "Failed to invoke #getSystemSecureDirectory() more", e); - } - } else { - LibLogger.w(TAG, "#getSystemSecureDirectory() not found"); - } - return null; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.kt new file mode 100644 index 0000000..91ecbdd --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/EnvironmentIA.kt @@ -0,0 +1,140 @@ +package me.ycdev.android.lib.common.internalapi.android.os + +import android.annotation.SuppressLint +import android.os.Environment +import timber.log.Timber +import java.io.File +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@Suppress("unused") +@SuppressLint("PrivateApi") +object EnvironmentIA { + private const val TAG = "EnvironmentIA" + + private var sMtd_getExternalStorageAndroidDataDir: Method? = null + private var sMtd_isEncryptedFilesystemEnabled: Method? = null + private var sMtd_getSecureDataDirectory: Method? = null + private var sMtd_getSystemSecureDirectory: Method? = null + + init { + try { + // API 8: File getExternalStorageAndroidDataDir() + sMtd_getExternalStorageAndroidDataDir = Environment::class.java.getMethod( + "getExternalStorageAndroidDataDir" + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "#getExternalStorageAndroidDataDir() not found") + } + + try { + // API 9: boolean isEncryptedFilesystemEnabled() + sMtd_isEncryptedFilesystemEnabled = Environment::class.java.getMethod( + "isEncryptedFilesystemEnabled" + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "#isEncryptedFilesystemEnabled() not found") + } + + try { + // API 9: File getSecureDataDirectory() + sMtd_getSecureDataDirectory = Environment::class.java.getMethod( + "getSecureDataDirectory" + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "#getSecureDataDirectory() not found") + } + + try { + // API 9: File getSystemSecureDirectory() + sMtd_getSystemSecureDirectory = Environment::class.java.getMethod( + "getSystemSecureDirectory" + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "#getSystemSecureDirectory() not found") + } + } + + /** + * Same to the hided method Environment#getExternalStorageAndroidDataDir() (API 8) + * @return null may be returned if the method not supported or failed to invoke it + */ + fun getExternalStorageAndroidDataDir(): File? { + if (sMtd_getExternalStorageAndroidDataDir != null) { + try { + return sMtd_getExternalStorageAndroidDataDir!!.invoke(null) as File + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w( + e, "Failed to invoke #getExternalStorageAndroidDataDir()" + ) + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w( + e, "Failed to invoke #getExternalStorageAndroidDataDir() ag" + ) + } + } else { + Timber.tag(TAG).w("#getExternalStorageAndroidDataDir() not found") + } + return null + } + + /** + * Same to the hided Environment#isEncryptedFilesystemEnabled() (API 9) + */ + fun isEncryptedFilesystemEnabled(): Boolean { + if (sMtd_isEncryptedFilesystemEnabled != null) { + try { + return sMtd_isEncryptedFilesystemEnabled!!.invoke(null) as Boolean + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w( + e, "Failed to invoke #isEncryptedFilesystemEnabled()" + ) + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w( + e, "Failed to invoke #isEncryptedFilesystemEnabled() ag" + ) + } + } else { + Timber.tag(TAG).w("#isEncryptedFilesystemEnabled() not found") + } + return false + } + + /** + * Same to the hided method Environment#getSecureDataDirectory() (API 9) + * @return null may be returned if the method not supported or failed to invoke it + */ + fun getSecureDataDirectory(): File? { + if (sMtd_getSecureDataDirectory != null) { + try { + return sMtd_getSecureDataDirectory!!.invoke(null) as File + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #getSecureDataDirectory()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #getSecureDataDirectory() ag") + } + } else { + Timber.tag(TAG).w("#getSecureDataDirectory() not found") + } + return null + } + + /** + * Same to the hided method Environment#getSystemSecureDirectory() (API 9) + * @return null may be returned if the method not supported or failed to invoke it + */ + fun getSystemSecureDirectory(): File? { + if (sMtd_getSystemSecureDirectory != null) { + try { + return sMtd_getSystemSecureDirectory!!.invoke(null) as File + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #getSystemSecureDirectory()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #getSystemSecureDirectory() ag") + } + } else { + Timber.tag(TAG).w("#getSystemSecureDirectory() not found") + } + return null + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.java deleted file mode 100644 index d5712b2..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.java +++ /dev/null @@ -1,304 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.os; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Build; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class PowerManagerIA { - private static final String TAG = "PowerManagerIA"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static final int API_VERSION_1 = 1; - private static final int API_VERSION_2 = 2; - - /** - * Go to sleep reason code: Going to sleep due by user request. - */ - private static final int GO_TO_SLEEP_REASON_USER = 0; - - private static Method sMtd_asInterface; - - private static Class sClass_IPowerManager; - private static Method sMtd_reboot; - private static int sVersion_reboot; - private static Method sMtd_shutdown; - private static int sVersion_shutdown; - private static Method sMtd_crash; - private static Method sMtd_goToSleep; - private static int sVersion_goToSleep; - - static { - try { - Class stubClass = Class.forName("android.os.IPowerManager$Stub", false, - Thread.currentThread().getContextClassLoader()); - sMtd_asInterface = stubClass.getMethod("asInterface", IBinder.class); - - sClass_IPowerManager = Class.forName("android.os.IPowerManager", false, - Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - if (DEBUG) LibLogger.w(TAG, "class not found", e); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - private PowerManagerIA() { - // nothing to do - } - - /** - * Get "android.os.IPowerManager" object from the service binder. - * @return null will be returned if failed - */ - @Nullable - public static Object asInterface(@NonNull IBinder binder) { - if (sMtd_asInterface != null) { - try { - return sMtd_asInterface.invoke(null, binder); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #asInterface()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #asInterface() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#asInterface() not available"); - } - return null; - } - - /** - * Get "android.os.IPowerManager" object from the service manager. - * @return null will be returned if failed - */ - @Nullable - public static Object getIPowerManager() { - IBinder binder = ServiceManagerIA.getService(Context.POWER_SERVICE); - if (binder != null) { - return asInterface(binder); - } - return null; - } - - private static void reflect_reboot() { - if (sMtd_reboot != null || sClass_IPowerManager == null) { - return; - } - - try { - try { - // Android 2.2 ~ Android 4.1: void reboot(String reason); - sMtd_reboot = sClass_IPowerManager.getMethod("reboot", String.class); - sVersion_reboot = API_VERSION_1; - } catch (NoSuchMethodException e) { - // Android 4.2: void reboot(boolean confirm, String reason, boolean wait); - sMtd_reboot = sClass_IPowerManager.getMethod("reboot", - boolean.class, String.class, boolean.class); - sVersion_reboot = API_VERSION_2; - } - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Reboot the device. - * @param service The "android.os.IPowerManager" object. - * @param reason Just for logging - * @see #asInterface(android.os.IBinder) - */ - public static void reboot(@NonNull Object service, @NonNull String reason) { - reflect_reboot(); - if (sMtd_reboot != null) { - try { - if (sVersion_reboot == API_VERSION_1) { - sMtd_reboot.invoke(service, reason); - } else if (sVersion_reboot == API_VERSION_2) { - sMtd_reboot.invoke(service, false, reason, false); - } else { - if (DEBUG) LibLogger.e(TAG, "reboot, unknown api version: " + sVersion_reboot); - } - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #reboot()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #reboot() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#reboot() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_reboot() { - reflect_reboot(); - return sMtd_reboot != null; - } - - private static void reflect_shutdown() { - if (sMtd_shutdown != null || sClass_IPowerManager == null || - Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { - return; - } - - try { - try { - // Android 4.2: void shutdown(boolean confirm, boolean wait); - sMtd_shutdown = sClass_IPowerManager.getMethod("shutdown", - boolean.class, boolean.class); - sVersion_shutdown = API_VERSION_1; - } catch (NoSuchMethodException e) { - // Android 7.0: void shutdown(boolean confirm, String reason, boolean wait); - sMtd_shutdown = sClass_IPowerManager.getMethod("shutdown", - boolean.class, String.class, boolean.class); - sVersion_shutdown = API_VERSION_2; - } - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - public static void shutdown(@NonNull Object service, String reason) { - reflect_shutdown(); - if (sMtd_shutdown != null) { - try { - if (sVersion_shutdown == API_VERSION_1) { - sMtd_shutdown.invoke(service, false, false); - } else if (sVersion_shutdown == API_VERSION_2) { - sMtd_shutdown.invoke(service, false, reason, false); - } else { - if (DEBUG) LibLogger.e(TAG, "shutdown, unknown api version: " + sVersion_shutdown); - } - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #shutdown()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #shutdown() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#shutdown() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_shutdown() { - reflect_shutdown(); - return sMtd_shutdown != null || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1; - } - - private static void reflect_crash() { - if (sMtd_crash != null || sClass_IPowerManager == null) { - return; - } - - try { - // Android 2.2 and next versions: void crash(String message); - sMtd_crash = sClass_IPowerManager.getMethod("crash", String.class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - public static void crash(@NonNull Object service, @NonNull String msg) { - reflect_crash(); - if (sMtd_crash != null) { - try { - sMtd_crash.invoke(service, msg); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #crash()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #crash() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#crash() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_crash() { - reflect_crash(); - return sMtd_crash != null; - } - - private static void reflect_goToSleep() { - if (sMtd_goToSleep != null || sClass_IPowerManager == null) { - return; - } - - try { - try { - // Android 2.2 ~ Android 4.1: void goToSleepWithReason(long time, int reason); - sMtd_goToSleep = sClass_IPowerManager.getMethod("goToSleepWithReason", long.class, int.class); - sVersion_goToSleep = API_VERSION_1; - } catch (NoSuchMethodException e) { - try { - // Android 4.2: void goToSleep(long time, int reason); - sMtd_goToSleep = sClass_IPowerManager.getMethod("goToSleep", long.class, int.class); - sVersion_goToSleep = API_VERSION_1; - } catch (NoSuchMethodException e1) { - // Android 5.0: void goToSleep(long time, int reason, int flags); - sMtd_goToSleep = sClass_IPowerManager.getMethod("goToSleep", long.class, int.class, int.class); - sVersion_goToSleep = API_VERSION_2; - } - - } - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Forces the device to go to sleep. Please refer android.os.PowerManager#goToSleep(long). - * @param service The IPowerManager object - * @param time The time when the request to go to sleep was issued, - * in the {@link android.os.SystemClock#uptimeMillis()} time base. - * This timestamp is used to correctly order the go to sleep request with - * other power management functions. It should be set to the timestamp - * of the input event that caused the request to go to sleep. - */ - public static void goToSleep(@NonNull Object service, long time) { - reflect_goToSleep(); - if (sMtd_goToSleep != null) { - try { - if (sVersion_goToSleep == API_VERSION_1) { - sMtd_goToSleep.invoke(service, time, GO_TO_SLEEP_REASON_USER); - } else if (sVersion_goToSleep == API_VERSION_2) { - sMtd_goToSleep.invoke(service, time, GO_TO_SLEEP_REASON_USER, 0); - } - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #crash()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #crash() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#crash() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_goToSleep() { - reflect_goToSleep(); - return sMtd_goToSleep != null; - } -} \ No newline at end of file diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.kt new file mode 100644 index 0000000..5f7573a --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/PowerManagerIA.kt @@ -0,0 +1,310 @@ +package me.ycdev.android.lib.common.internalapi.android.os + +import android.annotation.SuppressLint +import android.content.Context +import android.os.IBinder +import androidx.annotation.RestrictTo +import timber.log.Timber +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@Suppress("unused") +@SuppressLint("PrivateApi") +object PowerManagerIA { + private const val TAG = "PowerManagerIA" + + private const val API_VERSION_1 = 1 + private const val API_VERSION_2 = 2 + + /** + * Go to sleep reason code: Going to sleep due by user request. + */ + private const val GO_TO_SLEEP_REASON_USER = 0 + + private var sMtd_asInterface: Method? = null + + private var sClass_IPowerManager: Class<*>? = null + private var sMtd_reboot: Method? = null + private var sVersion_reboot: Int = 0 + private var sMtd_shutdown: Method? = null + private var sVersion_shutdown: Int = 0 + private var sMtd_crash: Method? = null + private var sMtd_goToSleep: Method? = null + private var sVersion_goToSleep: Int = 0 + + /** + * Get "android.os.IPowerManager" object from the service manager. + * @return null will be returned if failed + */ + val iPowerManager: Any? + get() { + val binder = ServiceManagerIA.getService(Context.POWER_SERVICE) + return if (binder != null) { + asInterface(binder) + } else null + } + + init { + try { + val stubClass = Class.forName( + "android.os.IPowerManager\$Stub", false, + Thread.currentThread().contextClassLoader + ) + sMtd_asInterface = stubClass.getMethod("asInterface", IBinder::class.java) + + sClass_IPowerManager = Class.forName( + "android.os.IPowerManager", false, + Thread.currentThread().contextClassLoader + ) + } catch (e: ClassNotFoundException) { + Timber.tag(TAG).w(e, "class not found") + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Get "android.os.IPowerManager" object from the service binder. + * @return null will be returned if failed + */ + fun asInterface(binder: IBinder): Any? { + if (sMtd_asInterface != null) { + try { + return sMtd_asInterface!!.invoke(null, binder) + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #asInterface()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #asInterface() more") + } + } else { + Timber.tag(TAG).w("#asInterface() not available") + } + return null + } + + private fun reflectReboot() { + if (sMtd_reboot != null || sClass_IPowerManager == null) { + return + } + + try { + try { + // Android 2.2 ~ Android 4.1: void reboot(String reason); + sMtd_reboot = sClass_IPowerManager!!.getMethod("reboot", String::class.java) + sVersion_reboot = API_VERSION_1 + } catch (e: NoSuchMethodException) { + // Android 4.2: void reboot(boolean confirm, String reason, boolean wait); + sMtd_reboot = sClass_IPowerManager!!.getMethod( + "reboot", + Boolean::class.javaPrimitiveType, + String::class.java, + Boolean::class.javaPrimitiveType + ) + sVersion_reboot = API_VERSION_2 + } + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Reboot the device. + * @param service The "android.os.IPowerManager" object. + * @param reason Just for logging + * @see .asInterface + */ + fun reboot(service: Any, reason: String) { + reflectReboot() + if (sMtd_reboot != null) { + try { + when (sVersion_reboot) { + API_VERSION_1 -> sMtd_reboot!!.invoke(service, reason) + API_VERSION_2 -> sMtd_reboot!!.invoke(service, false, reason, false) + else -> Timber.tag(TAG).e("reboot, unknown api version: $sVersion_reboot") + } + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #reboot()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #reboot() more") + } + } else { + Timber.tag(TAG).w("#reboot() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectReboot(): Boolean { + reflectReboot() + return sMtd_reboot != null + } + + private fun reflectShutdown() { + if (sMtd_shutdown != null || sClass_IPowerManager == null) return + + try { + try { + // Android 4.2: void shutdown(boolean confirm, boolean wait); + sMtd_shutdown = sClass_IPowerManager!!.getMethod( + "shutdown", + Boolean::class.javaPrimitiveType, Boolean::class.javaPrimitiveType + ) + sVersion_shutdown = API_VERSION_1 + } catch (e: NoSuchMethodException) { + // Android 7.0: void shutdown(boolean confirm, String reason, boolean wait); + sMtd_shutdown = sClass_IPowerManager!!.getMethod( + "shutdown", + Boolean::class.javaPrimitiveType, + String::class.java, + Boolean::class.javaPrimitiveType + ) + sVersion_shutdown = API_VERSION_2 + } + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun shutdown(service: Any, reason: String) { + reflectShutdown() + if (sMtd_shutdown != null) { + try { + when (sVersion_shutdown) { + API_VERSION_1 -> sMtd_shutdown!!.invoke(service, false, false) + API_VERSION_2 -> sMtd_shutdown!!.invoke(service, false, reason, false) + else -> Timber.tag(TAG).e("shutdown, unknown api version: $sVersion_shutdown") + } + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #shutdown()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #shutdown() more") + } + } else { + Timber.tag(TAG).w("#shutdown() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectShutdown(): Boolean { + reflectShutdown() + return sMtd_shutdown != null + } + + private fun reflectCrash() { + if (sMtd_crash != null || sClass_IPowerManager == null) { + return + } + + try { + // Android 2.2 and next versions: void crash(String message); + sMtd_crash = sClass_IPowerManager!!.getMethod("crash", String::class.java) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun crash(service: Any, msg: String) { + reflectCrash() + if (sMtd_crash != null) { + try { + sMtd_crash!!.invoke(service, msg) + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #crash()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #crash() more") + } + } else { + Timber.tag(TAG).w("#crash() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectCrash(): Boolean { + reflectCrash() + return sMtd_crash != null + } + + private fun reflectGoToSleep() { + if (sMtd_goToSleep != null || sClass_IPowerManager == null) { + return + } + + try { + try { + // Android 2.2 ~ Android 4.1: void goToSleepWithReason(long time, int reason); + sMtd_goToSleep = sClass_IPowerManager!!.getMethod( + "goToSleepWithReason", + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + sVersion_goToSleep = API_VERSION_1 + } catch (e: NoSuchMethodException) { + try { + // Android 4.2: void goToSleep(long time, int reason); + sMtd_goToSleep = sClass_IPowerManager!!.getMethod( + "goToSleep", + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + sVersion_goToSleep = API_VERSION_1 + } catch (e1: NoSuchMethodException) { + // Android 5.0: void goToSleep(long time, int reason, int flags); + sMtd_goToSleep = sClass_IPowerManager!!.getMethod( + "goToSleep", + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + sVersion_goToSleep = API_VERSION_2 + } + } + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Forces the device to go to sleep. Please refer android.os.PowerManager#goToSleep(long). + * @param service The IPowerManager object + * @param time The time when the request to go to sleep was issued, + * in the [android.os.SystemClock.uptimeMillis] time base. + * This timestamp is used to correctly order the go to sleep request with + * other power management functions. It should be set to the timestamp + * of the input event that caused the request to go to sleep. + */ + fun goToSleep(service: Any, time: Long) { + reflectGoToSleep() + if (sMtd_goToSleep != null) { + try { + if (sVersion_goToSleep == API_VERSION_1) { + sMtd_goToSleep!!.invoke(service, time, GO_TO_SLEEP_REASON_USER) + } else if (sVersion_goToSleep == API_VERSION_2) { + sMtd_goToSleep!!.invoke(service, time, GO_TO_SLEEP_REASON_USER, 0) + } + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #crash()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #crash() more") + } + } else { + Timber.tag(TAG).w("#crash() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectGoToSleep(): Boolean { + reflectGoToSleep() + return sMtd_goToSleep != null + } +} \ No newline at end of file diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.java deleted file mode 100644 index 738d4ea..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.java +++ /dev/null @@ -1,228 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.os; - -import android.annotation.SuppressLint; -import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import android.text.TextUtils; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.utils.IoUtils; -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; -import me.ycdev.android.lib.common.utils.StringUtils; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class ProcessIA { - private static final String TAG = "ProcessIA"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static Method sMtd_setArgV0; - private static Method sMtd_readProcLines; - private static Method sMtd_getParentPid; - private static Method sMtd_myPpid; - - private static void reflect_setArgV0() { - if (sMtd_setArgV0 != null) { - return; - } - - try { - // Android 1.6: public static final native void setArgV0(String text); - sMtd_setArgV0 = android.os.Process.class.getMethod("setArgV0", String.class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - public static void setArgV0(@NonNull String processName) { - reflect_setArgV0(); - if (sMtd_setArgV0 != null) { - try { - sMtd_setArgV0.invoke(null, processName); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #setArgV0()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #setArgV0() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#setArgV0() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_setArgV0() { - reflect_setArgV0(); - return sMtd_setArgV0 != null; - } - - private static void reflect_readProcLines() { - if (sMtd_readProcLines != null) { - return; - } - - try { - // Android 1.6: public static final native void readProcLines(String path, - // String[] reqFields, long[] outSizes); - sMtd_readProcLines = android.os.Process.class.getMethod("readProcLines", - String.class, String[].class, long[].class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - public static void readProcLines(@NonNull String path, @NonNull String[] reqFields, - @NonNull long[] outSizes) { - reflect_readProcLines(); - if (sMtd_readProcLines != null) { - try { - sMtd_readProcLines.invoke(null, path, reqFields, outSizes); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #readProcLines()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #readProcLines() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#readProcLines() not available"); - } - } - - @Nullable - public static String getProcessName(int pid) { - String cmdlineFile = "/proc/" + pid + "/cmdline"; - try { - return IoUtils.readAllLines(cmdlineFile).trim(); - } catch (IOException e) { - if (DEBUG) LibLogger.w(TAG, "cannot read cmdline file", e); - } - return null; - } - - /** - * Return the pid of the specified process name. If there are multiple processes - * which have same process name, then just return the first one. - * @param procName The process name - * @return -1 if the specified process not found - */ - public static int getProcessPid(@NonNull String procName) { - File[] procList = new File("/proc").listFiles(); - if (procList != null && procList.length > 0) { - for (File procFile : procList) { - if (!procFile.isDirectory()) { - continue; - } - if (!TextUtils.isDigitsOnly(procFile.getName())) { - continue; - } - int pid = StringUtils.parseInt(procFile.getName(), -1); - if (pid > -1) { - String curProcName = getProcessName(pid); - if (procName.equals(curProcName)) { - return pid; - } - } - } - } - return -1; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_readProcLines() { - reflect_readProcLines(); - return sMtd_readProcLines != null; - } - - private static void reflect_getParentPid() { - if (sMtd_getParentPid != null) { - return; - } - - try { - // Android 4.0: public static final int getParentPid(int pid) - sMtd_getParentPid = android.os.Process.class.getMethod("getParentPid", int.class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - public static int getParentPid(int pid) { - reflect_getParentPid(); - if (sMtd_getParentPid != null) { - try { - return (int) sMtd_getParentPid.invoke(null, pid); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #getParentPid()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #getParentPid() more", e); - } - } else { - String[] procStatusLabels = { "PPid:" }; - long[] procStatusValues = new long[1]; - procStatusValues[0] = -1; - readProcLines("/proc/" + pid + "/status", procStatusLabels, procStatusValues); - return (int) procStatusValues[0]; - } - return -1; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_getParentPid() { - reflect_getParentPid(); - return sMtd_getParentPid != null; - } - - - private static void reflect_myPpid() { - if (sMtd_myPpid != null || Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return; - } - - try { - // Android 4.4: public static final int myPpid() - sMtd_myPpid = android.os.Process.class.getMethod("myPpid"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - public static int myPpid() { - reflect_myPpid(); - if (sMtd_myPpid != null) { - try { - return (int) sMtd_myPpid.invoke(null); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #myPpid()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #myPpid() more", e); - } - } else { - return getParentPid(android.os.Process.myPid()); - } - return -1; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_myPpid() { - reflect_myPpid(); - return sMtd_myPpid != null || Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.kt new file mode 100644 index 0000000..b5cc7f2 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ProcessIA.kt @@ -0,0 +1,226 @@ +package me.ycdev.android.lib.common.internalapi.android.os + +import android.annotation.SuppressLint +import android.text.TextUtils +import androidx.annotation.RestrictTo +import me.ycdev.android.lib.common.utils.IoUtils +import me.ycdev.android.lib.common.utils.StringUtils +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@Suppress("MemberVisibilityCanBePrivate", "unused") +@SuppressLint("PrivateApi") +object ProcessIA { + private const val TAG = "ProcessIA" + + private var sMtd_setArgV0: Method? = null + private var sMtd_readProcLines: Method? = null + private var sMtd_getParentPid: Method? = null + private var sMtd_myPpid: Method? = null + + private fun reflectSetArgV0() { + if (sMtd_setArgV0 != null) { + return + } + + try { + // Android 1.6: public static final native void setArgV0(String text); + sMtd_setArgV0 = android.os.Process::class.java.getMethod("setArgV0", String::class.java) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun setArgV0(processName: String) { + reflectSetArgV0() + if (sMtd_setArgV0 != null) { + try { + sMtd_setArgV0!!.invoke(null, processName) + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #setArgV0()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #setArgV0() ag") + } + } else { + Timber.tag(TAG).w("#setArgV0() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectSetArgV0(): Boolean { + reflectSetArgV0() + return sMtd_setArgV0 != null + } + + private fun reflectReadProcLines() { + if (sMtd_readProcLines != null) { + return + } + + try { + // Android 1.6: public static final native void readProcLines(String path, + // String[] reqFields, long[] outSizes); + sMtd_readProcLines = android.os.Process::class.java.getMethod( + "readProcLines", + String::class.java, Array::class.java, LongArray::class.java + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun readProcLines( + path: String, + reqFields: Array, + outSizes: LongArray + ) { + reflectReadProcLines() + if (sMtd_readProcLines != null) { + try { + sMtd_readProcLines!!.invoke(null, path, reqFields, outSizes) + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #readProcLines()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #readProcLines() ag") + } + } else { + Timber.tag(TAG).w("#readProcLines() not available") + } + } + + fun getProcessName(pid: Int): String? { + val cmdlineFile = "/proc/$pid/cmdline" + try { + return IoUtils.readAllLines(cmdlineFile).trim { it <= ' ' } + } catch (e: IOException) { + Timber.tag(TAG).w(e, "cannot read cmdline file") + } + return null + } + + /** + * Return the pid of the specified process name. If there are multiple processes + * which have same process name, then just return the first one. + * @param procName The process name + * @return -1 if the specified process not found + */ + fun getProcessPid(procName: String): Int { + val procList = File("/proc").listFiles() + if (procList != null && procList.isNotEmpty()) { + for (procFile in procList) { + if (!procFile.isDirectory) { + continue + } + if (!TextUtils.isDigitsOnly(procFile.name)) { + continue + } + val pid = StringUtils.parseInt(procFile.name, -1) + if (pid > -1) { + val curProcName = getProcessName(pid) + if (procName == curProcName) { + return pid + } + } + } + } + return -1 + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectReadProcLines(): Boolean { + reflectReadProcLines() + return sMtd_readProcLines != null + } + + private fun reflectGetParentPid() { + if (sMtd_getParentPid != null) { + return + } + + try { + // Android 4.0: public static final int getParentPid(int pid) + sMtd_getParentPid = android.os.Process::class.java.getMethod( + "getParentPid", + Int::class.javaPrimitiveType!! + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun getParentPid(pid: Int): Int { + reflectGetParentPid() + if (sMtd_getParentPid != null) { + try { + return sMtd_getParentPid!!.invoke(null, pid) as Int + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #getParentPid()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #getParentPid() ag") + } + } else { + val procStatusLabels = arrayOf("PPid:") + val procStatusValues = LongArray(1) + procStatusValues[0] = -1 + readProcLines("/proc/$pid/status", procStatusLabels, procStatusValues) + return procStatusValues[0].toInt() + } + return -1 + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectGetParentPid(): Boolean { + reflectGetParentPid() + return sMtd_getParentPid != null + } + + private fun reflectMyPpid() { + if (sMtd_myPpid != null) { + return + } + + try { + // Android 4.4: public static final int myPpid() + sMtd_myPpid = android.os.Process::class.java.getMethod("myPpid") + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun myPpid(): Int { + reflectMyPpid() + if (sMtd_myPpid != null) { + try { + return sMtd_myPpid!!.invoke(null) as Int + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #myPpid()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #myPpid() ag") + } + } else { + return getParentPid(android.os.Process.myPid()) + } + return -1 + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectMyPpid(): Boolean { + reflectMyPpid() + return sMtd_myPpid != null + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.java deleted file mode 100644 index 0dd94c8..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.java +++ /dev/null @@ -1,216 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.os; - -import android.annotation.SuppressLint; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class ServiceManagerIA { - private static final String TAG = "ServiceManagerIA"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static Class sClass_ServiceManager; - - private static Method sMtd_getService; - private static Method sMtd_checkService; - private static Method sMtd_addService; - private static Method sMtd_listServices; - - static { - try { - sClass_ServiceManager = Class.forName("android.os.ServiceManager", false, - Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - if (DEBUG) LibLogger.w(TAG, "class not found", e); - } - } - - private ServiceManagerIA() { - // nothing to do - } - - private static void reflect_getService() { - if (sMtd_getService != null || sClass_ServiceManager == null) { - return; - } - - try { - // public static IBinder getService(String name) - sMtd_getService = sClass_ServiceManager.getMethod("getService", String.class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Returns a reference to a service with the given name. - * - *

    Important: May block the calling thread!

    - * @param name the name of the service to get - * @return a reference to the service, or null if the service doesn't exist - */ - @Nullable - public static IBinder getService(@NonNull String name) { - reflect_getService(); - if (sMtd_getService != null) { - try { - return (IBinder) sMtd_getService.invoke(null, name); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #getService()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #getService() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#getService() not available"); - } - return null; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_getService() { - reflect_getService(); - return sMtd_getService != null; - } - - private static void reflect_checkService() { - if (sMtd_checkService != null || sClass_ServiceManager == null) { - return; - } - - try { - // public static IBinder checkService(String name) - sMtd_checkService = sClass_ServiceManager.getMethod("checkService", String.class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Retrieve an existing service called @a name from the - * service manager. Non-blocking. - */ - @Nullable - public static IBinder checkService(@NonNull String name) { - reflect_checkService(); - if (sMtd_checkService != null) { - try { - return (IBinder) sMtd_checkService.invoke(null, name); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #checkService()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #checkService() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#checkService() not available"); - } - return null; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_checkService() { - reflect_checkService(); - return sMtd_checkService != null; - } - - private static void reflect_addService() { - if (sMtd_addService != null || sClass_ServiceManager == null) { - return; - } - - try { - // public static void addService(String name, IBinder service) - sMtd_addService = sClass_ServiceManager.getMethod("addService", - String.class, IBinder.class); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Place a new @a service called @a name into the service - * manager. - * - * @param name the name of the new service - * @param service the service object - */ - public static void addService(@NonNull String name, @NonNull IBinder service) { - reflect_addService(); - if (sMtd_addService != null) { - try { - sMtd_addService.invoke(null, name, service); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #addService()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #addService() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#addService() not available"); - } - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_addService() { - reflect_addService(); - return sMtd_addService != null; - } - - private static void reflect_listServices() { - if (sMtd_listServices != null || sClass_ServiceManager == null) { - return; - } - - try { - // public static String[] listServices() throws RemoteException - sMtd_listServices = sClass_ServiceManager.getMethod("listServices"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - - /** - * Return a list of all currently running services. - */ - @Nullable - public static String[] listServices() { - reflect_listServices(); - if (sMtd_listServices != null) { - try { - return (String[]) sMtd_listServices.invoke(null); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #listServices()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #listServices() more", e); - } - } else { - if (DEBUG) LibLogger.w(TAG, "#listServices() not available"); - } - return null; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_listServices() { - reflect_listServices(); - return sMtd_listServices != null; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.kt new file mode 100644 index 0000000..aec67a1 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/ServiceManagerIA.kt @@ -0,0 +1,211 @@ +package me.ycdev.android.lib.common.internalapi.android.os + +import android.annotation.SuppressLint +import android.os.IBinder +import androidx.annotation.RestrictTo +import timber.log.Timber + +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@Suppress("unused") +@SuppressLint("PrivateApi") +object ServiceManagerIA { + private const val TAG = "ServiceManagerIA" + + private var sClass_ServiceManager: Class<*>? = null + + private var sMtd_getService: Method? = null + private var sMtd_checkService: Method? = null + private var sMtd_addService: Method? = null + private var sMtd_listServices: Method? = null + + init { + try { + sClass_ServiceManager = Class.forName( + "android.os.ServiceManager", false, + Thread.currentThread().contextClassLoader + ) + } catch (e: ClassNotFoundException) { + Timber.tag(TAG).w(e, "class not found") + } + } + + private fun reflectGetService() { + if (sMtd_getService != null || sClass_ServiceManager == null) { + return + } + + try { + // public static IBinder getService(String name) + sMtd_getService = sClass_ServiceManager!!.getMethod("getService", String::class.java) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Returns a reference to a service with the given name. + * + * + * Important: May block the calling thread! + * @param name the name of the service to get + * @return a reference to the service, or `null` if the service doesn't exist + */ + fun getService(name: String): IBinder? { + reflectGetService() + if (sMtd_getService != null) { + try { + return sMtd_getService!!.invoke(null, name) as IBinder + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #getService()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #getService() ag") + } + } else { + Timber.tag(TAG).w("#getService() not available") + } + return null + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectGetService(): Boolean { + reflectGetService() + return sMtd_getService != null + } + + private fun reflectCheckService() { + if (sMtd_checkService != null || sClass_ServiceManager == null) { + return + } + + try { + // public static IBinder checkService(String name) + sMtd_checkService = + sClass_ServiceManager!!.getMethod("checkService", String::class.java) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Retrieve an existing service called @a name from the + * service manager. Non-blocking. + */ + fun checkService(name: String): IBinder? { + reflectCheckService() + if (sMtd_checkService != null) { + try { + return sMtd_checkService!!.invoke(null, name) as IBinder + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #checkService()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #checkService() ag") + } + } else { + Timber.tag(TAG).w("#checkService() not available") + } + return null + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectCheckService(): Boolean { + reflectCheckService() + return sMtd_checkService != null + } + + private fun reflectAddService() { + if (sMtd_addService != null || sClass_ServiceManager == null) { + return + } + + try { + // public static void addService(String name, IBinder service) + sMtd_addService = sClass_ServiceManager!!.getMethod( + "addService", + String::class.java, IBinder::class.java + ) + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Place a new @a service called @a name into the service + * manager. + * + * @param name the name of the new service + * @param service the service object + */ + fun addService(name: String, service: IBinder) { + reflectAddService() + if (sMtd_addService != null) { + try { + sMtd_addService!!.invoke(null, name, service) + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #addService()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #addService() ag") + } + } else { + Timber.tag(TAG).w("#addService() not available") + } + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectAddService(): Boolean { + reflectAddService() + return sMtd_addService != null + } + + private fun reflectListServices() { + if (sMtd_listServices != null || sClass_ServiceManager == null) { + return + } + + try { + // public static String[] listServices() throws RemoteException + sMtd_listServices = sClass_ServiceManager!!.getMethod("listServices") + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + /** + * Return a list of all currently running services. + */ + fun listServices(): Array? { + reflectListServices() + if (sMtd_listServices != null) { + try { + @Suppress("UNCHECKED_CAST") + return sMtd_listServices!!.invoke(null) as Array + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #listServices()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #listServices() more") + } + } else { + Timber.tag(TAG).w("#listServices() not available") + } + return null + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectListServices(): Boolean { + reflectListServices() + return sMtd_listServices != null + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.java deleted file mode 100644 index d209d74..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.java +++ /dev/null @@ -1,92 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.os; - -import android.annotation.SuppressLint; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class SystemPropertiesIA { - private static final String TAG = "SystemBuildPropCompat"; - - private static Method sMtd_get; - private static Method sMtd_getInt; - private static Method sMtd_getLong; - private static Method sMtd_getBoolean; - - static { - try { - Class classObj = Class.forName("android.os.SystemProperties", false, - Thread.currentThread().getContextClassLoader()); - sMtd_get = classObj.getMethod("get", String.class, String.class); - sMtd_getInt = classObj.getMethod("getInt", String.class, int.class); - sMtd_getLong = classObj.getMethod("getLong", String.class, long.class); - sMtd_getBoolean = classObj.getMethod("getBoolean", String.class, boolean.class); - } catch (Exception e) { - LibLogger.w(TAG, "Failed to reflect SystemProperties", e); - } - } - - private SystemPropertiesIA() { - // nothing to do - } - - public static String get(String key, String def) { - if (sMtd_get != null) { - try { - Object result = sMtd_get.invoke(null, key, def); - return (String) result; - } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { - LibLogger.w(TAG, "Failed to invoke get(String, String)", e); - } - } else { - LibLogger.w(TAG, "#get(String, String) not found"); - } - return def; - } - - public static int getInt(String key, int def) { - if (sMtd_getInt != null) { - try { - Object result = sMtd_getInt.invoke(null, key, def); - return (Integer) result; - } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { - LibLogger.w(TAG, "Failed to invoke get(String, int)", e); - } - } else { - LibLogger.w(TAG, "#getInt(String, int) not found"); - } - return def; - } - - public static long getLong(String key, long def) { - if (sMtd_getLong != null) { - try { - Object result = sMtd_getLong.invoke(null, key, def); - return (Long) result; - } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { - LibLogger.w(TAG, "Failed to invoke get(String, long)", e); - } - } else { - LibLogger.w(TAG, "#getLong(String, long) not found"); - } - return def; - } - - public static boolean getBoolean(String key, boolean def) { - if (sMtd_getBoolean != null) { - try { - Object result = sMtd_getBoolean.invoke(null, key, def); - return (Boolean) result; - } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { - LibLogger.w(TAG, "Failed to invoke get(String, boolean)", e); - } - } else { - LibLogger.w(TAG, "#getBoolean(String, boolean) not found"); - } - return def; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.kt new file mode 100644 index 0000000..3690b9a --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/SystemPropertiesIA.kt @@ -0,0 +1,110 @@ +package me.ycdev.android.lib.common.internalapi.android.os + +import android.annotation.SuppressLint +import timber.log.Timber + +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@SuppressLint("PrivateApi") +object SystemPropertiesIA { + private const val TAG = "SystemBuildPropCompat" + + private var sMtd_get: Method? = null + private var sMtd_getInt: Method? = null + private var sMtd_getLong: Method? = null + private var sMtd_getBoolean: Method? = null + + init { + try { + val classObj = Class.forName( + "android.os.SystemProperties", false, + Thread.currentThread().contextClassLoader + ) + sMtd_get = classObj.getMethod("get", String::class.java, String::class.java) + sMtd_getInt = + classObj.getMethod("getInt", String::class.java, Int::class.javaPrimitiveType) + sMtd_getLong = + classObj.getMethod("getLong", String::class.java, Long::class.javaPrimitiveType) + sMtd_getBoolean = classObj.getMethod( + "getBoolean", + String::class.java, + Boolean::class.javaPrimitiveType + ) + } catch (e: Exception) { + Timber.tag(TAG).w(e, "Failed to reflect SystemProperties") + } + } + + fun get(key: String, def: String): String { + if (sMtd_get != null) { + try { + val result = sMtd_get!!.invoke(null, key, def) + return result as String + } catch (e: IllegalArgumentException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, String)") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, String)") + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, String)") + } + } else { + Timber.tag(TAG).w("#get(String, String) not found") + } + return def + } + + fun getInt(key: String, def: Int): Int { + if (sMtd_getInt != null) { + try { + val result = sMtd_getInt!!.invoke(null, key, def) + return result as Int + } catch (e: IllegalArgumentException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, int)") + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, int)") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, int)") + } + } else { + Timber.tag(TAG).w("#getInt(String, int) not found") + } + return def + } + + fun getLong(key: String, def: Long): Long { + if (sMtd_getLong != null) { + try { + val result = sMtd_getLong!!.invoke(null, key, def) + return result as Long + } catch (e: IllegalArgumentException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, long)") + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, long)") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, long)") + } + } else { + Timber.tag(TAG).w("#getLong(String, long) not found") + } + return def + } + + fun getBoolean(key: String, def: Boolean): Boolean { + if (sMtd_getBoolean != null) { + try { + val result = sMtd_getBoolean!!.invoke(null, key, def) + return result as Boolean + } catch (e: IllegalArgumentException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, boolean)") + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, boolean)") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke get(String, boolean)") + } + } else { + Timber.tag(TAG).w("#getBoolean(String, boolean) not found") + } + return def + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.java b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.java deleted file mode 100644 index d4676de..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.java +++ /dev/null @@ -1,72 +0,0 @@ -package me.ycdev.android.lib.common.internalapi.android.os; - -import android.annotation.SuppressLint; -import android.os.Build; -import androidx.annotation.RestrictTo; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -@SuppressLint("PrivateApi") -public class UserHandleIA { - private static final String TAG = "UserHandleIA"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private static Class sClass_UserHandle; - private static Method sMtd_myUserId; - - static { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - try { - // Android 4.1 - // There are both "UserId" and "UserHandle" in "Xiaomi MI 2SC, MIUI V5-3, Android 4.1.1" - sClass_UserHandle = Class.forName("android.os.UserId"); - } catch (ClassNotFoundException e) { - try { - // Android 4.2 ~ ? - sClass_UserHandle = Class.forName("android.os.UserHandle"); - } catch (ClassNotFoundException e1) { - if (DEBUG) LibLogger.w(TAG, "class not found", e1); - } - } - - if (sClass_UserHandle != null) { - try { - sMtd_myUserId = sClass_UserHandle.getMethod("myUserId"); - } catch (NoSuchMethodException e) { - if (DEBUG) LibLogger.w(TAG, "method not found", e); - } - } - } - } - - private UserHandleIA() { - // nothing to do - } - - public static int myUserId() { - if (sMtd_myUserId != null) { - try { - return (Integer) sMtd_myUserId.invoke(null); - } catch (IllegalAccessException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #myUserId()", e); - } catch (InvocationTargetException e) { - if (DEBUG) LibLogger.w(TAG, "Failed to invoke #myUserId() more", e); - } - } - return 0; - } - - /** - * Just for unit test. - */ - @RestrictTo(RestrictTo.Scope.TESTS) - static boolean checkReflect_myUserId() { - return sMtd_myUserId != null || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.kt new file mode 100644 index 0000000..05551dd --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/internalapi/android/os/UserHandleIA.kt @@ -0,0 +1,44 @@ +package me.ycdev.android.lib.common.internalapi.android.os + +import android.annotation.SuppressLint +import android.os.UserHandle +import androidx.annotation.RestrictTo +import timber.log.Timber +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@SuppressLint("PrivateApi") +object UserHandleIA { + private const val TAG = "UserHandleIA" + + private var sMtd_myUserId: Method? = null + + init { + try { + sMtd_myUserId = UserHandle::class.java.getMethod("myUserId") + } catch (e: NoSuchMethodException) { + Timber.tag(TAG).w(e, "method not found") + } + } + + fun myUserId(): Int { + if (sMtd_myUserId != null) { + try { + return sMtd_myUserId!!.invoke(null) as Int + } catch (e: IllegalAccessException) { + Timber.tag(TAG).w(e, "Failed to invoke #myUserId()") + } catch (e: InvocationTargetException) { + Timber.tag(TAG).w(e, "Failed to invoke #myUserId() ag") + } + } + return 0 + } + + /** + * Just for unit test. + */ + @RestrictTo(RestrictTo.Scope.TESTS) + internal fun checkReflectMyUserId(): Boolean { + return sMtd_myUserId != null + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.java b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.java deleted file mode 100644 index 2cf022c..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package me.ycdev.android.lib.common.ipc; - -public interface ConnectStateListener { - void onStateChanged(@ServiceConnector.ConnectState int newState); -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.kt new file mode 100644 index 0000000..03a294c --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ConnectStateListener.kt @@ -0,0 +1,5 @@ +package me.ycdev.android.lib.common.ipc + +interface ConnectStateListener { + fun onStateChanged(@ServiceConnector.ConnectState newState: Int) +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.java b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.java deleted file mode 100644 index 74e1aa0..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package me.ycdev.android.lib.common.ipc; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; - -@SuppressWarnings("unused") -public class IpcHandler extends Handler { - private static volatile IpcHandler sInstance; - - private IpcHandler() { - super(createLooper()); - } - - private static Looper createLooper() { - HandlerThread thread = new HandlerThread("IpcHandler"); - thread.start(); - return thread.getLooper(); - } - - public static IpcHandler getInstance() { - if (sInstance == null) { - synchronized (IpcHandler.class) { - if (sInstance == null) { - sInstance = new IpcHandler(); - } - } - } - return sInstance; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.kt new file mode 100644 index 0000000..8bd70a9 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcHandler.kt @@ -0,0 +1,14 @@ +package me.ycdev.android.lib.common.ipc + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +private fun createLooper(): Looper { + val thread = HandlerThread("IpcHandler") + thread.start() + return thread.looper +} + +@Suppress("unused") +object IpcHandler : Handler(createLooper()) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.java b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.java deleted file mode 100644 index 14863c2..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.java +++ /dev/null @@ -1,8 +0,0 @@ -package me.ycdev.android.lib.common.ipc; - -import android.os.RemoteException; -import androidx.annotation.NonNull; - -public interface IpcOperation { - void execute(@NonNull IService service) throws RemoteException; -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.kt new file mode 100644 index 0000000..d7de1f1 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/IpcOperation.kt @@ -0,0 +1,8 @@ +package me.ycdev.android.lib.common.ipc + +import android.os.RemoteException + +interface IpcOperation { + @Throws(RemoteException::class) + fun execute(service: IService) +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.java b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.java deleted file mode 100644 index 40d903e..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.java +++ /dev/null @@ -1,162 +0,0 @@ -package me.ycdev.android.lib.common.ipc; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; - -import java.util.LinkedList; -import java.util.Queue; - -import timber.log.Timber; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ServiceClientBase implements ConnectStateListener, Handler.Callback { - private static final String TAG = "ServiceClientBase"; - - private static final int MSG_NEW_OPERATION = 1; - private static final int MSG_PENDING_OPERATIONS = 2; - private static final int MSG_AUTO_DISCONNECT = 3; - - protected Context mAppContext; - protected ServiceConnector mServiceConnector; - - private String mServiceName; - private Handler mOperationHandler; - private Queue> mPendingOperations = new LinkedList<>(); - private boolean mAutoDisconnect; - private long mDelayToDisconnect; - - protected ServiceClientBase(@NonNull Context context, @NonNull String serviceName, - @NonNull Looper workLooper, @NonNull ServiceConnector serviceConnector) { - mAppContext = context.getApplicationContext(); - mServiceName = serviceName; - mOperationHandler = new Handler(workLooper, this); - - mServiceConnector = serviceConnector; - mServiceConnector.addListener(this); - } - - /** - * Enable/disable "auto disconnect" feature - * @param autoDisconnect Disconnect automatically if true - * @param delayToDisconnect The delay time to disconnect if no operations, in milliseconds. - */ - public void setAutoDisconnect(boolean autoDisconnect, long delayToDisconnect) { - mAutoDisconnect = autoDisconnect; - if (autoDisconnect) { - mDelayToDisconnect = delayToDisconnect > 0L ? delayToDisconnect : 0L; - } - } - - public boolean isAutoDisconnectEnabled() { - return mAutoDisconnect; - } - - @NonNull - public ServiceConnector getServiceConnector() { - return mServiceConnector; - } - - public void connect() { - mServiceConnector.connect(); - } - - /** - * Disconnect the Service connection. This may cause the pending operations to lost! - */ - public void disconnect() { - mServiceConnector.disconnect(); - } - - public void addOperation(IpcOperation operation) { - if (mServiceConnector.getConnectState() == ServiceConnector.STATE_DISCONNECTED) { - // try to connect if not connected or connecting - // (such as the Service APK was installed after the previous connecting) - // (such as autoDisconnect enabled) - mServiceConnector.connect(); - } - mOperationHandler.removeMessages(MSG_AUTO_DISCONNECT); - Message.obtain(mOperationHandler, MSG_NEW_OPERATION, operation).sendToTarget(); - } - - @Override - public void onStateChanged(int newState) { - Timber.tag(TAG).d("[%s] Service connect state changed: %d", mServiceName, newState); - if (newState == ServiceConnector.STATE_CONNECTED) { - mOperationHandler.removeMessages(MSG_AUTO_DISCONNECT); - Message.obtain(mOperationHandler, MSG_PENDING_OPERATIONS).sendToTarget(); - } - } - - @WorkerThread - private void handleOperation(@NonNull IpcOperation operation) { - IService service = mServiceConnector.getService(); - if (service != null) { - try { - operation.execute(service); - Timber.tag(TAG).d("[%s] Succeeded to handle incoming operation: %s", - mServiceName, operation); - return; // Success - } catch (RemoteException e) { - Timber.tag(TAG).w(e, "[%s] Failed to handle incoming operation: %s", - mServiceName, operation); - // add it into the queue again - } catch (Exception e) { - Timber.tag(TAG).e(e, "[%s] Cannot execute incoming operation: %s. Discard it.", - mServiceName, operation); - return; // discard the operation - } - } - - // Service not connected or failed to IPC - Timber.tag(TAG).d("[%s] Added into pending queue: %s", mServiceName, operation); - mPendingOperations.add(operation); - } - - @WorkerThread - private void handlePendingOperations() { - Timber.tag(TAG).d("[%s] handlePendingOperations: %d", mServiceName, mPendingOperations.size()); - while (mServiceConnector.getService() != null) { - IpcOperation operation = mPendingOperations.poll(); - if (operation == null) { - break; - } - handleOperation(operation); - } - Timber.tag(TAG).d("[%s] handlePendingOperations done: %d", mServiceName, mPendingOperations.size()); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_NEW_OPERATION: { - @SuppressWarnings("unchecked") - IpcOperation operation = (IpcOperation) msg.obj; - handleOperation(operation); - break; - } - - case MSG_PENDING_OPERATIONS: { - handlePendingOperations(); - break; - } - - case MSG_AUTO_DISCONNECT: { - Timber.tag(TAG).d("auto disconnect"); - mServiceConnector.disconnect(); - break; - } - } - - if (mAutoDisconnect && msg.what != MSG_AUTO_DISCONNECT) { - mOperationHandler.removeMessages(MSG_AUTO_DISCONNECT); - mOperationHandler.sendEmptyMessageDelayed(MSG_AUTO_DISCONNECT, mDelayToDisconnect); - } - - return true; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt new file mode 100644 index 0000000..3f586de --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt @@ -0,0 +1,153 @@ +package me.ycdev.android.lib.common.ipc + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.RemoteException +import androidx.annotation.WorkerThread +import timber.log.Timber +import java.util.LinkedList + +open class ServiceClientBase protected constructor( + context: Context, + private val serviceName: String, + workLooper: Looper, + var serviceConnector: ServiceConnector +) : ConnectStateListener, Handler.Callback { + + protected var appContext: Context = context.applicationContext + + @Suppress("LeakingThis") + private val operationHandler: Handler = Handler(workLooper, this) + private val pendingOperations = LinkedList>() + + var isAutoDisconnectEnabled: Boolean = false + private set + private var delayToDisconnect: Long = 0 + + init { + @Suppress("LeakingThis") + this.serviceConnector.addListener(this) + } + + /** + * Enable/disable "auto disconnect" feature + * @param autoDisconnect Disconnect automatically if true + * @param delayToDisconnect The delay time to disconnect if no operations, in milliseconds. + */ + fun setAutoDisconnect(autoDisconnect: Boolean, delayToDisconnect: Long) { + isAutoDisconnectEnabled = autoDisconnect + if (autoDisconnect) { + this.delayToDisconnect = if (delayToDisconnect > 0L) delayToDisconnect else 0L + } + } + + fun connect() { + serviceConnector.connect() + } + + /** + * Disconnect the Service connection. This may cause the pending operations to lost! + */ + fun disconnect() { + serviceConnector.disconnect() + } + + fun addOperation(operation: IpcOperation) { + if (serviceConnector.getConnectState() == ServiceConnector.STATE_DISCONNECTED) { + // try to connect if not connected or connecting + // (such as the Service APK was installed after the previous connecting) + // (such as autoDisconnect enabled) + serviceConnector.connect() + } + operationHandler.removeMessages(MSG_AUTO_DISCONNECT) + Message.obtain(operationHandler, MSG_NEW_OPERATION, operation).sendToTarget() + } + + override fun onStateChanged(newState: Int) { + Timber.tag(TAG).d("[%s] Service connect state changed: %d", serviceName, newState) + if (newState == ServiceConnector.STATE_CONNECTED) { + operationHandler.removeMessages(MSG_AUTO_DISCONNECT) + Message.obtain(operationHandler, MSG_PENDING_OPERATIONS).sendToTarget() + } + } + + @WorkerThread + private fun handleOperation(operation: IpcOperation) { + val service = serviceConnector.getService() + if (service != null) { + try { + operation.execute(service) + Timber.tag(TAG).d( + "[%s] Succeeded to handle incoming operation: %s", + serviceName, operation + ) + return // Success + } catch (e: RemoteException) { + Timber.tag(TAG).w( + e, "[%s] Failed to handle incoming operation: %s", + serviceName, operation + ) + // add it into the queue again + } catch (e: Exception) { + Timber.tag(TAG).e( + e, "[%s] Cannot execute incoming operation: %s. Discard it.", + serviceName, operation + ) + return // discard the operation + } + } + + // Service not connected or failed to IPC + Timber.tag(TAG).d("[%s] Added into pending queue: %s", serviceName, operation) + pendingOperations.add(operation) + } + + @WorkerThread + private fun handlePendingOperations() { + Timber.tag(TAG).d("[%s] handlePendingOperations: %d", serviceName, pendingOperations.size) + while (serviceConnector.getService() != null) { + val operation = pendingOperations.poll() ?: break + handleOperation(operation) + } + Timber.tag(TAG).d( + "[%s] handlePendingOperations done: %d", + serviceName, pendingOperations.size + ) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + MSG_NEW_OPERATION -> { + @Suppress("UNCHECKED_CAST") + val operation = msg.obj as IpcOperation + handleOperation(operation) + } + + MSG_PENDING_OPERATIONS -> { + handlePendingOperations() + } + + MSG_AUTO_DISCONNECT -> { + Timber.tag(TAG).d("auto disconnect") + serviceConnector.disconnect() + } + } + + if (isAutoDisconnectEnabled && msg.what != MSG_AUTO_DISCONNECT) { + operationHandler.removeMessages(MSG_AUTO_DISCONNECT) + operationHandler.sendEmptyMessageDelayed(MSG_AUTO_DISCONNECT, delayToDisconnect) + } + + return true + } + + companion object { + private const val TAG = "ServiceClientBase" + + private const val MSG_NEW_OPERATION = 1 + private const val MSG_PENDING_OPERATIONS = 2 + private const val MSG_AUTO_DISCONNECT = 3 + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.java b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.java deleted file mode 100644 index 95801ea..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.java +++ /dev/null @@ -1,342 +0,0 @@ -package me.ycdev.android.lib.common.ipc; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.ApplicationInfo; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import me.ycdev.android.lib.common.utils.Preconditions; -import me.ycdev.android.lib.common.utils.WeakListenerManager; -import timber.log.Timber; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class ServiceConnector { - private static final String TAG = "ServiceConnector"; - - public static final int STATE_DISCONNECTED = 1; - public static final int STATE_CONNECTING = 2; - public static final int STATE_CONNECTED = 3; - - private static final int MSG_RECONNECT = 1; - private static final int MSG_NOTIFY_LISTENERS = 2; - private static final int MSG_CONNECT_TIMEOUT_CHECK = 3; - - private static final long CONNECT_TIMEOUT_CHECK_INTERVAL = 5000; // 5s - private static final long FORCE_REBIND_TIME = 30 * 1000; // 30 seconds - - @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED}) - public @interface ConnectState {} - - protected Context mAppContext; - protected String mServiceName; - protected IServiceInterface mService; - - protected WeakListenerManager mStateListeners = new WeakListenerManager<>(); - private final Object mConnectWaitLock = new Object(); - private AtomicInteger mState = new AtomicInteger(STATE_DISCONNECTED); - private long mConnectStartTime; - private ServiceConnection mServiceConnection; - - protected ServiceConnector(Context cxt, String serviceName) { - mAppContext = cxt.getApplicationContext(); - mServiceName = serviceName; - } - - /** - * Get the looper used to connect/reconnect target Service. - * By default, it's the main looper. - */ - protected Looper getConnectLooper() { - return Looper.getMainLooper(); - } - - /** - * Get Intent to bind the target service. - */ - @NonNull - protected abstract Intent getServiceIntent(); - - protected boolean validatePermission(@Nullable String permission) { - return true; // Skip to validate permission by default - } - - /** - * Sub class can rewrite the candidate services select logic. - */ - @Nullable - protected ComponentName selectTargetService(@NonNull List servicesList) { - Timber.tag(TAG).i("[%s] Candidate services: %d", mServiceName, servicesList.size()); - Preconditions.checkArgument(servicesList.size() >= 1); - ServiceInfo serviceInfo = servicesList.get(0).serviceInfo; - for (ResolveInfo info : servicesList) { - if (!validatePermission(info.serviceInfo.permission)) { - Timber.tag(TAG).w("Skip not-matched permission candidate: %s, perm: %s", - info.serviceInfo.name, info.serviceInfo.permission); - continue; - } - if ((info.serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == - ApplicationInfo.FLAG_SYSTEM) { - serviceInfo = info.serviceInfo; // search the system candidate - Timber.tag(TAG).i("[%s] Service from system found and select it", mServiceName); - break; - } - } - - if (validatePermission(serviceInfo.permission)) { - return new ComponentName(serviceInfo.packageName, serviceInfo.name); - } - return null; - } - - /** - * Convert the IBinder object to interface. - */ - protected abstract IServiceInterface asInterface(IBinder service); - - /** - * Add a connect state listener, using {@link WeakListenerManager} to manager listeners. - * Callbacks will be invoked in {@link #getConnectLooper()} thread. - */ - public void addListener(@NonNull ConnectStateListener listener) { - mStateListeners.addListener(listener); - } - - public void removeListener(@NonNull ConnectStateListener listener) { - mStateListeners.removeListener(listener); - } - - public boolean isServiceExist() { - Intent intent = getServiceIntent(); - List servicesList = mAppContext.getPackageManager().queryIntentServices(intent, 0); - return servicesList != null && servicesList.size() > 0 && selectTargetService(servicesList) != null; - } - - public void connect() { - connectServiceIfNeeded(false); - } - - public void disconnect() { - Timber.tag(TAG).i("[%s] disconnect service...", mServiceName); - mConnectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK); - mConnectHandler.removeMessages(MSG_RECONNECT); - mService = null; - if (mServiceConnection != null) { - mAppContext.unbindService(mServiceConnection); - mServiceConnection = null; - } - updateConnectState(STATE_DISCONNECTED); - } - - private void connectServiceIfNeeded(boolean rebind) { - if (mService != null) { - Timber.tag(TAG).d("[%s] service is connected", mServiceName); - return; - } - if (!rebind) { - if (!mState.compareAndSet(STATE_DISCONNECTED, STATE_CONNECTING)) { - Timber.tag(TAG).d("[%s] Service is under connecting", mServiceName); - return; - } - updateConnectState(STATE_CONNECTING); - } - mConnectStartTime = SystemClock.elapsedRealtime(); - - Intent intent = getServiceIntent(); - List servicesList = mAppContext.getPackageManager().queryIntentServices(intent, 0); - if (servicesList == null || servicesList.size() == 0) { - Timber.tag(TAG).w("[%s] no service component available, cannot connect", mServiceName); - updateConnectState(STATE_DISCONNECTED); - return; - } - ComponentName candidateService = selectTargetService(servicesList); - if (candidateService == null) { - Timber.tag(TAG).w("[%s] no expected service component found, cannot connect", mServiceName); - updateConnectState(STATE_DISCONNECTED); - return; - } - // must set explicit component before bind/start service - intent.setComponent(candidateService); - - mServiceConnection = new ServiceConnection() { - private boolean mConnectLost = false; - - @Override - public void onServiceConnected(ComponentName cn, IBinder service) { - Timber.tag(TAG).i("[%s] service connected, cn: %s, mConnectLost: %s", - mServiceName, cn, mConnectLost); - if (!mConnectLost) { - // update 'mService' first, and then update the connect state and notify - mService = asInterface(service); - mConnectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK); - updateConnectState(STATE_CONNECTED); - } // else: waiting for reconnecting using new ServiceConnection object - } - - @Override - public void onServiceDisconnected(ComponentName cn) { - Timber.tag(TAG).i("[%s] service disconnected, cn: %s, mConnectLost: %s", - mServiceName, cn, mConnectLost); - if (mConnectLost) { - return; - } - - // Unbind the service and bind it again later - mConnectLost = true; - disconnect(); - - mConnectHandler.sendEmptyMessageDelayed(MSG_RECONNECT, 1000); - } - }; - - Timber.tag(TAG).i("[%s] connecting service...", mServiceName); - if (!mAppContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)) { - Timber.tag(TAG).w("[%s] cannot connect", mServiceName); - updateConnectState(STATE_DISCONNECTED); - } else { - mConnectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK); - mConnectHandler.sendEmptyMessageDelayed(MSG_CONNECT_TIMEOUT_CHECK, - CONNECT_TIMEOUT_CHECK_INTERVAL); - } - } - - private void updateConnectState(@ConnectState int newState) { - if (newState != STATE_CONNECTING) { - mState.set(newState); - } - mConnectHandler.obtainMessage(MSG_NOTIFY_LISTENERS, newState, 0).sendToTarget(); - } - - /** - * Waiting for the service connected. - */ - @WorkerThread - public void waitForConnected() { - waitForConnected(-1); - } - - /** - * Waiting for the service connected with timeout. - * - * @param timeoutMillis Timeout in milliseconds to wait for the service connected. - * 0 means no waiting and -1 means no timeout. - */ - @WorkerThread - public void waitForConnected(long timeoutMillis) { - Preconditions.checkNonMainThread(); - if (mService != null) { - Timber.tag(TAG).d("[%s] already connected", mServiceName); - return; - } - - synchronized (mConnectWaitLock) { - connect(); - long sleepTime = 50; - long timeElapsed = 0; - while (true) { - int connectState = mState.get(); - Timber.tag(TAG).d("[%s] checking, service: %s, state: %d, time: %d/%d", - mServiceName, mService, connectState, timeElapsed, timeoutMillis); - if (connectState == STATE_CONNECTED || connectState == STATE_DISCONNECTED) { - break; - } - if (timeoutMillis >= 0 && timeElapsed >= timeoutMillis) { - break; - } - - connect(); - try { - Thread.sleep(sleepTime); - } catch (InterruptedException e) { - Timber.tag(TAG).w(e, "interrupted"); - break; - } - - timeElapsed = timeElapsed + sleepTime; - sleepTime = sleepTime * 2; - if (sleepTime > 1000) { - sleepTime = 1000; - } - } - } - } - - public IServiceInterface getService() { - return mService; - } - - @ConnectState - public int getConnectState() { - //noinspection WrongConstant - return mState.get(); - } - - public static String strConnectState(int state) { - if (state == STATE_DISCONNECTED) { - return "disconnected"; - } else if (state == STATE_CONNECTING) { - return "connecting"; - } else if (state == STATE_CONNECTED) { - return "connected"; - } else { - return "unknown"; - } - } - - private Handler mConnectHandler = new Handler(getConnectLooper()) { - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_RECONNECT: { - Timber.tag(TAG).d("[%s] delayed reconnect fires...", mServiceName); - connect(); - break; - } - - case MSG_NOTIFY_LISTENERS: { - final @ConnectState int newState = msg.arg1; - Timber.tag(TAG).d("State changed: %s", strConnectState(newState)); - mStateListeners.notifyListeners(listener -> listener.onStateChanged(newState)); - break; - } - - case MSG_CONNECT_TIMEOUT_CHECK: { - Timber.tag(TAG).d("checking connect timeout"); - int curState = mState.get(); - if (SystemClock.elapsedRealtime() - mConnectStartTime >= FORCE_REBIND_TIME) { - Timber.tag(TAG).d("[%s] connect timeout, state: %s", - mServiceName, curState); - if (curState == STATE_CONNECTING) { - // force to rebind the service - connectServiceIfNeeded(true); - } - } else { - if (curState == STATE_CONNECTING) { - mConnectHandler.sendEmptyMessageDelayed(MSG_CONNECT_TIMEOUT_CHECK, - CONNECT_TIMEOUT_CHECK_INTERVAL); - } - } - break; - } - } - } - }; -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt new file mode 100644 index 0000000..20120ad --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt @@ -0,0 +1,321 @@ +package me.ycdev.android.lib.common.ipc + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.ApplicationInfo +import android.content.pm.ResolveInfo +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Message +import android.os.SystemClock +import androidx.annotation.IntDef +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import me.ycdev.android.lib.common.utils.Preconditions +import me.ycdev.android.lib.common.utils.WeakListenerManager +import timber.log.Timber +import java.util.concurrent.atomic.AtomicInteger + +abstract class ServiceConnector protected constructor( + cxt: Context, + protected var serviceName: String +) { + protected var appContext: Context = cxt.applicationContext + + protected var stateListeners = WeakListenerManager() + private val connectWaitLock = Any() + private val state = AtomicInteger(STATE_DISCONNECTED) + private var connectStartTime: Long = 0 + private var serviceConnection: ServiceConnection? = null + private var service: IServiceInterface? = null + + /** + * Get the looper used to connect/reconnect target Service. + * By default, it's the main looper. + */ + fun getConnectLooper(): Looper = Looper.getMainLooper() + + /** + * Get Intent to bind the target service. + */ + protected abstract fun getServiceIntent(): Intent + + /** + * Convert the IBinder object to interface. + */ + protected abstract fun asInterface(service: IBinder): IServiceInterface + + protected open fun validatePermission(permission: String?): Boolean { + return true // Skip to validate permission by default + } + + fun isServiceExist(): Boolean { + val intent = getServiceIntent() + val servicesList = appContext.packageManager.queryIntentServices(intent, 0) + return servicesList != null && servicesList.size > 0 && selectTargetService(servicesList) != null + } + + /** + * Sub class can rewrite the candidate services select logic. + */ + @VisibleForTesting + internal fun selectTargetService(servicesList: List): ComponentName? { + Timber.tag(TAG).i("[%s] Candidate services: %d", serviceName, servicesList.size) + Preconditions.checkArgument(servicesList.isNotEmpty()) + var serviceInfo = servicesList[0].serviceInfo + for (info in servicesList) { + if (!validatePermission(info.serviceInfo.permission)) { + Timber.tag(TAG).w( + "Skip not-matched permission candidate: %s, perm: %s", + info.serviceInfo.name, info.serviceInfo.permission + ) + continue + } + if (info.serviceInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM) { + serviceInfo = info.serviceInfo // search the system candidate + Timber.tag(TAG).i("[%s] Service from system found and select it", serviceName) + break + } + } + + return if (validatePermission(serviceInfo.permission)) { + ComponentName(serviceInfo.packageName, serviceInfo.name) + } else null + } + + @ConnectState + fun getConnectState(): Int = state.get() + + fun getService(): IServiceInterface? = service + + /** + * Add a connect state listener, using [WeakListenerManager] to manager listeners. + * Callbacks will be invoked in [.getConnectLooper] thread. + */ + fun addListener(listener: ConnectStateListener) { + stateListeners.addListener(listener) + } + + fun removeListener(listener: ConnectStateListener) { + stateListeners.removeListener(listener) + } + + fun connect() { + connectServiceIfNeeded(false) + } + + fun disconnect() { + Timber.tag(TAG).i("[%s] disconnect service...", serviceName) + connectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK) + connectHandler.removeMessages(MSG_RECONNECT) + service = null + if (serviceConnection != null) { + appContext.unbindService(serviceConnection!!) + serviceConnection = null + } + updateConnectState(STATE_DISCONNECTED) + } + + private fun connectServiceIfNeeded(rebind: Boolean) { + if (service != null) { + Timber.tag(TAG).d("[%s] service is connected", serviceName) + return + } + if (!rebind) { + if (!state.compareAndSet(STATE_DISCONNECTED, STATE_CONNECTING)) { + Timber.tag(TAG).d("[%s] Service is under connecting", serviceName) + return + } + updateConnectState(STATE_CONNECTING) + } + connectStartTime = SystemClock.elapsedRealtime() + + val intent = getServiceIntent() + val servicesList = appContext.packageManager.queryIntentServices(intent, 0) + if (servicesList == null || servicesList.size == 0) { + Timber.tag(TAG).w("[%s] no service component available, cannot connect", serviceName) + updateConnectState(STATE_DISCONNECTED) + return + } + val candidateService = selectTargetService(servicesList) + if (candidateService == null) { + Timber.tag(TAG) + .w("[%s] no expected service component found, cannot connect", serviceName) + updateConnectState(STATE_DISCONNECTED) + return + } + // must set explicit component before bind/start service + intent.component = candidateService + + serviceConnection = object : ServiceConnection { + private var mConnectLost = false + + override fun onServiceConnected(cn: ComponentName, service: IBinder) { + Timber.tag(TAG).i( + "[%s] service connected, cn: %s, mConnectLost: %s", + serviceName, cn, mConnectLost + ) + if (!mConnectLost) { + // update 'mService' first, and then update the connect state and notify + this@ServiceConnector.service = asInterface(service) + connectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK) + updateConnectState(STATE_CONNECTED) + } // else: waiting for reconnecting using new ServiceConnection object + } + + override fun onServiceDisconnected(cn: ComponentName) { + Timber.tag(TAG).i( + "[%s] service disconnected, cn: %s, mConnectLost: %s", + serviceName, cn, mConnectLost + ) + if (mConnectLost) { + return + } + + // Unbind the service and bind it again later + mConnectLost = true + disconnect() + + connectHandler.sendEmptyMessageDelayed(MSG_RECONNECT, 1000) + } + } + + Timber.tag(TAG).i("[%s] connecting service...", serviceName) + if (!appContext.bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE)) { + Timber.tag(TAG).w("[%s] cannot connect", serviceName) + updateConnectState(STATE_DISCONNECTED) + } else { + connectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK) + connectHandler.sendEmptyMessageDelayed( + MSG_CONNECT_TIMEOUT_CHECK, + CONNECT_TIMEOUT_CHECK_INTERVAL + ) + } + } + + private fun updateConnectState(@ConnectState newState: Int) { + if (newState != STATE_CONNECTING) { + state.set(newState) + } + connectHandler.obtainMessage(MSG_NOTIFY_LISTENERS, newState, 0).sendToTarget() + } + + /** + * Waiting for the service connected with timeout. + * + * @param timeoutMillis Timeout in milliseconds to wait for the service connected. + * 0 means no waiting and -1 means no timeout. + */ + @WorkerThread + fun waitForConnected(timeoutMillis: Long = -1) { + Preconditions.checkNonMainThread() + if (service != null) { + Timber.tag(TAG).d("[%s] already connected", serviceName) + return + } + + synchronized(connectWaitLock) { + connect() + var sleepTime: Long = 50 + var timeElapsed: Long = 0 + while (true) { + val connectState = state.get() + Timber.tag(TAG).d( + "[%s] checking, service: %s, state: %d, time: %d/%d", + serviceName, service, connectState, timeElapsed, timeoutMillis + ) + if (connectState == STATE_CONNECTED || connectState == STATE_DISCONNECTED) { + break + } + if (timeoutMillis in 0..timeElapsed) { + break + } + + connect() + try { + Thread.sleep(sleepTime) + } catch (e: InterruptedException) { + Timber.tag(TAG).w(e, "interrupted") + break + } + + timeElapsed += sleepTime + sleepTime *= 2 + if (sleepTime > 1000) { + sleepTime = 1000 + } + } + } + } + + private val connectHandler = object : Handler(getConnectLooper()) { + + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_RECONNECT -> { + Timber.tag(TAG).d("[%s] delayed reconnect fires...", serviceName) + connect() + } + + MSG_NOTIFY_LISTENERS -> { + @ConnectState val newState = msg.arg1 + Timber.tag(TAG).d("State changed: %s", strConnectState(newState)) + stateListeners.notifyListeners { listener -> listener.onStateChanged(newState) } + } + + MSG_CONNECT_TIMEOUT_CHECK -> { + Timber.tag(TAG).d("checking connect timeout") + val curState = state.get() + if (SystemClock.elapsedRealtime() - connectStartTime >= FORCE_REBIND_TIME) { + Timber.tag(TAG).d( + "[%s] connect timeout, state: %s", + serviceName, curState + ) + if (curState == STATE_CONNECTING) { + // force to rebind the service + connectServiceIfNeeded(true) + } + } else { + if (curState == STATE_CONNECTING) { + this.sendEmptyMessageDelayed( + MSG_CONNECT_TIMEOUT_CHECK, + CONNECT_TIMEOUT_CHECK_INTERVAL + ) + } + } + } + } + } + } + + @Retention(AnnotationRetention.SOURCE) + @IntDef(STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED) + annotation class ConnectState + + companion object { + private const val TAG = "ServiceConnector" + + const val STATE_DISCONNECTED = 1 + const val STATE_CONNECTING = 2 + const val STATE_CONNECTED = 3 + + private const val MSG_RECONNECT = 1 + private const val MSG_NOTIFY_LISTENERS = 2 + private const val MSG_CONNECT_TIMEOUT_CHECK = 3 + + private const val CONNECT_TIMEOUT_CHECK_INTERVAL: Long = 5000 // 5s + private const val FORCE_REBIND_TIME = (30 * 1000).toLong() // 30 seconds + + fun strConnectState(state: Int): String { + return when (state) { + STATE_DISCONNECTED -> "disconnected" + STATE_CONNECTING -> "connecting" + STATE_CONNECTED -> "connected" + else -> "unknown" + } + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.java b/baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.java deleted file mode 100644 index 390cd3c..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.java +++ /dev/null @@ -1,169 +0,0 @@ -package me.ycdev.android.lib.common.net; - -import android.content.Context; -import androidx.annotation.NonNull; - -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.zip.GZIPInputStream; -import java.util.zip.InflaterInputStream; - -import me.ycdev.android.lib.common.utils.IoUtils; -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class HttpClient { - private static final String TAG = "HttpClient"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - private String mCharset = "UTF-8"; - private int mConnectTimeout; // ms - private int mReadTimeout; // ms - - public HttpClient() { - setTimeout(10000, 10000); // default to 10 seconds - } - - public void setTimeout(int connectTimeout, int readTimeout) { - mConnectTimeout = connectTimeout; - mReadTimeout = readTimeout; - } - - @NonNull - public String get(@NonNull Context cxt, @NonNull String url, - @NonNull HashMap requestHeaders) throws IOException { - HttpURLConnection httpConn = getHttpConnection(cxt, url, false, requestHeaders); - try { - httpConn.connect(); - } catch (Exception e) { - throw new IOException(e.toString()); - } - try { - return getResponse(httpConn); - } finally { - httpConn.disconnect(); - } - } - - @NonNull - public String post(@NonNull Context cxt, @NonNull String url, @NonNull String body) - throws IOException { - HttpURLConnection httpConn = getHttpConnection(cxt, url, true, null); - DataOutputStream os = null; - - // Send the "POST" request - try { - os = new DataOutputStream(httpConn.getOutputStream()); - os.write(body.getBytes(mCharset)); - os.flush(); - return getResponse(httpConn); - } catch (Exception e) { - // should not be here, but..... - throw new IOException(e.toString()); - } finally { - // Must be called before calling HttpURLConnection.disconnect() - IoUtils.closeQuietly(os); - httpConn.disconnect(); - } - } - - @NonNull - public String post(@NonNull Context cxt, @NonNull String url, @NonNull byte[] body) - throws IOException { - HttpURLConnection httpConn = getHttpConnection(cxt, url, true, null); - DataOutputStream os = null; - - // Send the "POST" request - try { - os = new DataOutputStream(httpConn.getOutputStream()); - os.write(body); - os.flush(); - return getResponse(httpConn); - } catch (Exception e) { - // prepare for any unexpected exceptions - throw new IOException(e.toString()); - } finally { - // Must be called before calling HttpURLConnection.disconnect() - IoUtils.closeQuietly(os); - httpConn.disconnect(); - } - } - - @NonNull - private HttpURLConnection getHttpConnection(Context cxt, String url, - boolean post, HashMap requestHeaders) throws IOException { - HttpURLConnection httpConn = NetworkUtils.openHttpURLConnection(url); - httpConn.setConnectTimeout(mConnectTimeout); - httpConn.setReadTimeout(mReadTimeout); - httpConn.setDoInput(true); - httpConn.setUseCaches(false); - httpConn.setRequestProperty("Accept-Encoding", "gzip,deflate"); - httpConn.setRequestProperty("Charset", mCharset); - if (requestHeaders != null) { - addRequestHeaders(httpConn, requestHeaders); - } - if (post) { - httpConn.setDoOutput(true); - httpConn.setRequestMethod("POST"); - } else { - httpConn.setRequestMethod("GET"); // by default - } - return httpConn; - } - - private void addRequestHeaders(HttpURLConnection httpConn, HashMap requestHeaders) { - Set> allHeaders = requestHeaders.entrySet(); - for (Map.Entry header : allHeaders) { - httpConn.addRequestProperty(header.getKey(), header.getValue()); - } - } - - private String getResponse(HttpURLConnection httpConn) throws IOException { - String contentEncoding = httpConn.getContentEncoding(); - if (DEBUG) { - LibLogger.d(TAG, "response code: " + httpConn.getResponseCode() - + ", encoding: " + contentEncoding + ", method: " + httpConn.getRequestMethod()); - } - - InputStream httpInputStream = null; - try { - httpInputStream = httpConn.getInputStream(); - } catch (IOException | IllegalStateException e) { - // ignore - } - if (httpInputStream == null) { - // If httpConn.getInputStream() throws IOException, - // we can get the error message from the error stream. - // For example, the case when the response code is 4xx. - httpInputStream = httpConn.getErrorStream(); - } - if (httpInputStream == null) { - throw new IOException("HttpURLConnection.getInputStream() returned null"); - } - - InputStream is; - if (contentEncoding != null && contentEncoding.contains("gzip")) { - is = new GZIPInputStream(httpInputStream); - } else if (contentEncoding != null && contentEncoding.contains("deflate")) { - is = new InflaterInputStream(httpInputStream); - } else { - is = httpInputStream; - } - - // Read the response content - try { - byte[] responseContent = IoUtils.readAllBytes(is); - return new String(responseContent, mCharset); - } finally { - // Must be called before calling HttpURLConnection.disconnect() - IoUtils.closeQuietly(is); - } - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.kt new file mode 100644 index 0000000..748eb9d --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/net/HttpClient.kt @@ -0,0 +1,168 @@ +package me.ycdev.android.lib.common.net + +import me.ycdev.android.lib.common.utils.IoUtils +import me.ycdev.android.lib.common.utils.LibLogger +import java.io.DataOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection +import java.util.HashMap +import java.util.zip.GZIPInputStream +import java.util.zip.InflaterInputStream + +@Suppress("unused") +class HttpClient { + private val charset = "UTF-8" + private var connectTimeout: Int = 10_000 // ms + private var readTimeout: Int = 10_1000 // ms + + fun setTimeout(connectTimeout: Int, readTimeout: Int) { + this.connectTimeout = connectTimeout + this.readTimeout = readTimeout + } + + @Throws(IOException::class) + operator fun get( + url: String, + requestHeaders: HashMap + ): String { + val httpConn = getHttpConnection(url, false, requestHeaders) + try { + httpConn.connect() + } catch (e: Exception) { + throw IOException(e.toString()) + } + + try { + return getResponse(httpConn) + } finally { + httpConn.disconnect() + } + } + + @Throws(IOException::class) + fun post(url: String, body: String): String { + val httpConn = getHttpConnection(url, true, null) + var os: DataOutputStream? = null + + // Send the "POST" request + try { + os = DataOutputStream(httpConn.outputStream) + os.write(body.toByteArray(charset(charset))) + os.flush() + return getResponse(httpConn) + } catch (e: Exception) { + // should not be here, but..... + throw IOException(e.toString()) + } finally { + // Must be called before calling HttpURLConnection.disconnect() + IoUtils.closeQuietly(os) + httpConn.disconnect() + } + } + + @Throws(IOException::class) + fun post(url: String, body: ByteArray): String { + val httpConn = getHttpConnection(url, true, null) + var os: DataOutputStream? = null + + // Send the "POST" request + try { + os = DataOutputStream(httpConn.outputStream) + os.write(body) + os.flush() + return getResponse(httpConn) + } catch (e: Exception) { + // prepare for any unexpected exceptions + throw IOException(e.toString()) + } finally { + // Must be called before calling HttpURLConnection.disconnect() + IoUtils.closeQuietly(os) + httpConn.disconnect() + } + } + + @Throws(IOException::class) + private fun getHttpConnection( + url: String, + post: Boolean, + requestHeaders: HashMap? + ): HttpURLConnection { + val httpConn = NetworkUtils.openHttpURLConnection(url) + httpConn.connectTimeout = connectTimeout + httpConn.readTimeout = readTimeout + httpConn.doInput = true + httpConn.useCaches = false + httpConn.setRequestProperty("Accept-Encoding", "gzip,deflate") + httpConn.setRequestProperty("Charset", charset) + if (requestHeaders != null) { + addRequestHeaders(httpConn, requestHeaders) + } + if (post) { + httpConn.doOutput = true + httpConn.requestMethod = "POST" + } else { + httpConn.requestMethod = "GET" // by default + } + return httpConn + } + + private fun addRequestHeaders( + httpConn: HttpURLConnection, + requestHeaders: HashMap + ) { + val allHeaders = requestHeaders.entries + for ((key, value) in allHeaders) { + httpConn.addRequestProperty(key, value) + } + } + + @Throws(IOException::class) + private fun getResponse(httpConn: HttpURLConnection): String { + val contentEncoding = httpConn.contentEncoding + LibLogger.d( + TAG, "response code: " + httpConn.responseCode + + ", encoding: " + contentEncoding + ", method: " + httpConn.requestMethod + ) + + var httpInputStream: InputStream? = null + try { + httpInputStream = httpConn.inputStream + } catch (e: IOException) { + // ignore + } catch (e: IllegalStateException) { + } + + if (httpInputStream == null) { + // If httpConn.getInputStream() throws IOException, + // we can get the error message from the error stream. + // For example, the case when the response code is 4xx. + httpInputStream = httpConn.errorStream + } + if (httpInputStream == null) { + throw IOException("HttpURLConnection.getInputStream() returned null") + } + + val input: InputStream + if (contentEncoding != null && contentEncoding.contains("gzip")) { + input = GZIPInputStream(httpInputStream) + } else if (contentEncoding != null && contentEncoding.contains("deflate")) { + input = InflaterInputStream(httpInputStream) + } else { + input = httpInputStream + } + + // Read the response content + try { + val responseContent = input.readBytes() + return String(responseContent, charset(charset)) + } finally { + // Must be called before calling HttpURLConnection.disconnect() + IoUtils.closeQuietly(input) + } + } + + companion object { + private const val TAG = "HttpClient" + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.java deleted file mode 100644 index c3a11e7..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.java +++ /dev/null @@ -1,232 +0,0 @@ -package me.ycdev.android.lib.common.net; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Build; -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresPermission; -import androidx.annotation.VisibleForTesting; -import android.telephony.TelephonyManager; -import android.text.TextUtils; - -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - -import me.ycdev.android.lib.common.utils.LibConfigs; -import me.ycdev.android.lib.common.utils.LibLogger; - -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_2G; -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_3G; -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_4G; -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_MOBILE; -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_NONE; -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_COMPANION_PROXY; -import static me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_WIFI; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class NetworkUtils { - private static final String TAG = "NetworkUtils"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - public static final int WEAR_OS_COMPANION_PROXY = 16; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - NETWORK_TYPE_NONE, NETWORK_TYPE_WIFI, NETWORK_TYPE_MOBILE, - NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, - NETWORK_TYPE_COMPANION_PROXY - }) - public @interface NetworkType { - int NETWORK_TYPE_NONE = -1; - int NETWORK_TYPE_WIFI = 1; - int NETWORK_TYPE_MOBILE = 2; - int NETWORK_TYPE_2G = 10; - int NETWORK_TYPE_3G = 11; - int NETWORK_TYPE_4G = 12; - int NETWORK_TYPE_COMPANION_PROXY = 20; - } - - @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - @NonNull - public static String dumpActiveNetworkInfo(@NonNull Context cxt) { - NetworkInfo info = getActiveNetworkInfo(cxt); - if (info == null) { - return "No active network"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("type=").append(info.getType()) - .append(", subType=").append(info.getSubtype()) - .append(", infoDump=").append(info); - return sb.toString(); - } - - @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - public static NetworkInfo getActiveNetworkInfo(Context cxt) { - ConnectivityManager cm = (ConnectivityManager) cxt.getSystemService( - Context.CONNECTIVITY_SERVICE); - if (cm == null) { - if (DEBUG) LibLogger.w(TAG, "failed to get connectivity service"); - return null; - } - - NetworkInfo netInfo = null; - try { - netInfo = cm.getActiveNetworkInfo(); - } catch (Exception e) { - if (DEBUG) LibLogger.w(TAG, "failed to get active network info", e); - } - return netInfo; - } - - /** - * @return One of the values {@link NetworkType#NETWORK_TYPE_NONE}, - * {@link NetworkType#NETWORK_TYPE_WIFI} or {@link NetworkType#NETWORK_TYPE_MOBILE} - */ - @SuppressWarnings("deprecation") - @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - @NetworkType - public static int getNetworkType(Context cxt) { - NetworkInfo netInfo = getActiveNetworkInfo(cxt); - if (netInfo == null) { - return NETWORK_TYPE_NONE; - } - - return getNetworkType(netInfo.getType(), netInfo.getSubtype()); - } - - @NetworkType - @VisibleForTesting - static int getNetworkType(int type, int subType) { - if (type == ConnectivityManager.TYPE_WIFI - || type == ConnectivityManager.TYPE_WIMAX - || type == ConnectivityManager.TYPE_ETHERNET) { - return NETWORK_TYPE_WIFI; - } else if (type == ConnectivityManager.TYPE_MOBILE - || type == ConnectivityManager.TYPE_MOBILE_MMS) { - return NETWORK_TYPE_MOBILE; - } else if (type == WEAR_OS_COMPANION_PROXY) { - // Wear OS - return NETWORK_TYPE_COMPANION_PROXY; - } - return NETWORK_TYPE_NONE; // Take unknown networks as none - } - - /** - * @return One of values {@link NetworkType#NETWORK_TYPE_2G}, {@link NetworkType#NETWORK_TYPE_3G}, - * {@link NetworkType#NETWORK_TYPE_4G} or {@link NetworkType#NETWORK_TYPE_NONE} - */ - @NetworkType - public static int getMobileNetworkType(Context cxt) { - // Code from android-5.1.1_r4: - // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java - // in NetworkControllerImpl#mapIconSets() - TelephonyManager tm = (TelephonyManager) cxt.getSystemService(Context.TELEPHONY_SERVICE); - if (tm == null) { - if (DEBUG) LibLogger.w(TAG, "failed to get telephony service"); - return NETWORK_TYPE_NONE; - } - - int tmType; - try { - tmType = tm.getNetworkType(); - } catch (Exception e) { - if (DEBUG) LibLogger.w(TAG, "failed to get telephony network type", e); - return NETWORK_TYPE_NONE; - } - - switch (tmType) { - case TelephonyManager.NETWORK_TYPE_UNKNOWN: - return NETWORK_TYPE_NONE; - - case TelephonyManager.NETWORK_TYPE_LTE: - return NETWORK_TYPE_4G; - - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_EVDO_B: - case TelephonyManager.NETWORK_TYPE_EHRPD: - case TelephonyManager.NETWORK_TYPE_UMTS: -// case TelephonyManager.NETWORK_TYPE_TD_SCDMA: - return NETWORK_TYPE_3G; - - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_HSPAP: - return NETWORK_TYPE_3G; // H - - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_1xRTT: -// case TelephonyManager.NETWORK_TYPE_GSM: - return NETWORK_TYPE_2G; - } - return NETWORK_TYPE_2G; - } - - /** - * @return One of values {@link NetworkType#NETWORK_TYPE_WIFI}, {@link NetworkType#NETWORK_TYPE_2G}, - * {@link NetworkType#NETWORK_TYPE_3G}, {@link NetworkType#NETWORK_TYPE_4G} - * or {@link NetworkType#NETWORK_TYPE_NONE} - */ - @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - @NetworkType - public static int getMixedNetworkType(Context cxt) { - int type = getNetworkType(cxt); - if (type == NETWORK_TYPE_MOBILE) { - type = getMobileNetworkType(cxt); - } - return type; - } - - /** - * Check if there is an active network connection - */ - @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - public static boolean isNetworkAvailable(Context cxt) { - NetworkInfo network = getActiveNetworkInfo(cxt); - return network != null && network.isConnected(); - } - - /** - * Check if the current active network may cause monetary cost - * @see ConnectivityManager#isActiveNetworkMetered() - */ - @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - public static boolean isActiveNetworkMetered(Context cxt) { - ConnectivityManager cm = (ConnectivityManager) cxt.getSystemService( - Context.CONNECTIVITY_SERVICE); - if (cm == null) { - if (DEBUG) LibLogger.w(TAG, "failed to get connectivity service"); - return true; - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - return getNetworkType(cxt) == NETWORK_TYPE_MOBILE; - } - return cm.isActiveNetworkMetered(); - } - - /** - * Open a HTTP connection to the specified URL. Use proxy automatically if needed. - */ - public static HttpURLConnection openHttpURLConnection(String url) throws IOException { - // check if url can be parsed successfully to prevent host == null crash - // https://code.google.com/p/android/issues/detail?id=16895 - URL linkUrl = new URL(url); - String host = linkUrl.getHost(); - if (TextUtils.isEmpty(host)) { - throw new MalformedURLException("Malformed URL: " + url); - } - // TODO if needed to support proxy - return (HttpURLConnection) linkUrl.openConnection(); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.kt new file mode 100644 index 0000000..2db5fc0 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/net/NetworkUtils.kt @@ -0,0 +1,199 @@ +package me.ycdev.android.lib.common.net + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.telephony.TelephonyManager +import android.text.TextUtils +import androidx.annotation.IntDef +import androidx.annotation.RequiresPermission +import androidx.annotation.VisibleForTesting +import me.ycdev.android.lib.common.utils.LibLogger +import java.io.IOException +import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.URL + +@Suppress("unused") +object NetworkUtils { + private const val TAG = "NetworkUtils" + + const val WEAR_OS_COMPANION_PROXY = 16 + + const val NETWORK_TYPE_NONE = -1 + const val NETWORK_TYPE_WIFI = 1 + const val NETWORK_TYPE_MOBILE = 2 + const val NETWORK_TYPE_2G = 10 + const val NETWORK_TYPE_3G = 11 + const val NETWORK_TYPE_4G = 12 + const val NETWORK_TYPE_COMPANION_PROXY = 20 + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + NETWORK_TYPE_NONE, + NETWORK_TYPE_WIFI, + NETWORK_TYPE_MOBILE, + NETWORK_TYPE_2G, + NETWORK_TYPE_3G, + NETWORK_TYPE_4G, + NETWORK_TYPE_COMPANION_PROXY + ) + annotation class NetworkType + + @Suppress("DEPRECATION") + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + fun dumpActiveNetworkInfo(cxt: Context): String { + val info = getActiveNetworkInfo(cxt) ?: return "No active network" + + val sb = StringBuilder() + sb.append("type=").append(info.type) + .append(", subType=").append(info.subtype) + .append(", infoDump=").append(info) + return sb.toString() + } + + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + fun getActiveNetworkInfo(cxt: Context): NetworkInfo? { + val cm = cxt.getSystemService( + Context.CONNECTIVITY_SERVICE + ) as ConnectivityManager? + if (cm == null) { + LibLogger.w(TAG, "failed to get connectivity service") + return null + } + + var netInfo: NetworkInfo? = null + try { + netInfo = cm.activeNetworkInfo + } catch (e: Exception) { + LibLogger.w(TAG, "failed to get active network info", e) + } + + return netInfo + } + + /** + * @return One of the values [NETWORK_TYPE_NONE], + * [NETWORK_TYPE_WIFI] or [NETWORK_TYPE_MOBILE] + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @NetworkType + fun getNetworkType(cxt: Context): Int { + val netInfo = getActiveNetworkInfo(cxt) ?: return NETWORK_TYPE_NONE + + @Suppress("DEPRECATION") + return getNetworkType(netInfo.type, netInfo.subtype) + } + + @Suppress("DEPRECATION", "UNUSED_PARAMETER") + @NetworkType + @VisibleForTesting + internal fun getNetworkType(type: Int, subType: Int): Int { + if (type == ConnectivityManager.TYPE_WIFI || + type == ConnectivityManager.TYPE_WIMAX || + type == ConnectivityManager.TYPE_ETHERNET + ) { + return NETWORK_TYPE_WIFI + } else if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_MMS) { + return NETWORK_TYPE_MOBILE + } else if (type == WEAR_OS_COMPANION_PROXY) { + // Wear OS + return NETWORK_TYPE_COMPANION_PROXY + } + return NETWORK_TYPE_NONE // Take unknown networks as none + } + + /** + * @return One of values [NETWORK_TYPE_2G], [NETWORK_TYPE_3G], + * [NETWORK_TYPE_4G] or [NETWORK_TYPE_NONE] + */ + @NetworkType + fun getMobileNetworkType(cxt: Context): Int { + // Code from android-5.1.1_r4: + // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java + // in NetworkControllerImpl#mapIconSets() + val tm = cxt.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager? + if (tm == null) { + LibLogger.w(TAG, "failed to get telephony service") + return NETWORK_TYPE_NONE + } + + val tmType: Int + try { + tmType = tm.networkType + } catch (e: Exception) { + LibLogger.w(TAG, "failed to get telephony network type", e) + return NETWORK_TYPE_NONE + } + + when (tmType) { + TelephonyManager.NETWORK_TYPE_UNKNOWN -> return NETWORK_TYPE_NONE + + TelephonyManager.NETWORK_TYPE_LTE -> return NETWORK_TYPE_4G + + TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyManager.NETWORK_TYPE_UMTS -> + // case TelephonyManager.NETWORK_TYPE_TD_SCDMA: + return NETWORK_TYPE_3G + + TelephonyManager.NETWORK_TYPE_HSDPA, TelephonyManager.NETWORK_TYPE_HSUPA, TelephonyManager.NETWORK_TYPE_HSPA, TelephonyManager.NETWORK_TYPE_HSPAP -> return NETWORK_TYPE_3G // H + + TelephonyManager.NETWORK_TYPE_GPRS, TelephonyManager.NETWORK_TYPE_EDGE, TelephonyManager.NETWORK_TYPE_CDMA, TelephonyManager.NETWORK_TYPE_1xRTT -> + // case TelephonyManager.NETWORK_TYPE_GSM: + return NETWORK_TYPE_2G + } + return NETWORK_TYPE_2G + } + + /** + * @return One of values [NETWORK_TYPE_WIFI], [NETWORK_TYPE_2G], + * [NETWORK_TYPE_3G], [NETWORK_TYPE_4G] or [NETWORK_TYPE_NONE] + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @NetworkType + fun getMixedNetworkType(cxt: Context): Int { + var type = getNetworkType(cxt) + if (type == NETWORK_TYPE_MOBILE) { + type = getMobileNetworkType(cxt) + } + return type + } + + /** + * Check if there is an active network connection + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + fun isNetworkAvailable(cxt: Context): Boolean { + val network = getActiveNetworkInfo(cxt) + return network != null && network.isConnected + } + + /** + * Check if the current active network may cause monetary cost + * @see ConnectivityManager.isActiveNetworkMetered + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + fun isActiveNetworkMetered(cxt: Context): Boolean { + val cm = cxt.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + if (cm == null) { + LibLogger.w(TAG, "failed to get connectivity service") + return true + } + return cm.isActiveNetworkMetered + } + + /** + * Open a HTTP connection to the specified URL. Use proxy automatically if needed. + */ + @Throws(IOException::class) + fun openHttpURLConnection(url: String): HttpURLConnection { + // check if url can be parsed successfully to prevent host == null crash + // https://code.google.com/p/android/issues/detail?id=16895 + val linkUrl = URL(url) + val host = linkUrl.host + if (TextUtils.isEmpty(host)) { + throw MalformedURLException("Malformed URL: $url") + } + // TODO if needed to support proxy + return linkUrl.openConnection() as HttpURLConnection + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.java b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.java deleted file mode 100644 index 9f361db..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.ycdev.android.lib.common.perms; - -import androidx.core.app.ActivityCompat; - -public interface PermissionCallback extends ActivityCompat.OnRequestPermissionsResultCallback { - void onRationaleDenied(int requestCode); -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.kt new file mode 100644 index 0000000..2ae217b --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionCallback.kt @@ -0,0 +1,7 @@ +package me.ycdev.android.lib.common.perms + +import androidx.core.app.ActivityCompat + +interface PermissionCallback : ActivityCompat.OnRequestPermissionsResultCallback { + fun onRationaleDenied(requestCode: Int) +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.java b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.java deleted file mode 100644 index 96f5de1..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.ycdev.android.lib.common.perms; - -import androidx.annotation.IntDef; -import androidx.annotation.StringRes; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class PermissionRequestParams { - public static final int RATIONALE_POLICY_ON_DEMOND = 1; - public static final int RATIONALE_POLICY_NEVER = 2; - public static final int RATIONALE_POLICY_ALWAYS = 3; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - RATIONALE_POLICY_ON_DEMOND, RATIONALE_POLICY_NEVER, RATIONALE_POLICY_ALWAYS - }) - public @interface RationalePolicy {} - - public int requestCode; - public String[] permissions; - public @RationalePolicy int rationalePolicy = RATIONALE_POLICY_ON_DEMOND; - public String rationaleTitle; - public String rationaleContent; - public @StringRes int positiveBtnResId = android.R.string.ok; - public @StringRes int negativeBtnResId = android.R.string.cancel; - public PermissionCallback callback; -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.kt new file mode 100644 index 0000000..8a7fee0 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionRequestParams.kt @@ -0,0 +1,29 @@ +package me.ycdev.android.lib.common.perms + +import androidx.annotation.IntDef +import androidx.annotation.StringRes + +class PermissionRequestParams { + + var requestCode: Int = 0 + var permissions: Array? = null + @RationalePolicy + var rationalePolicy = RATIONALE_POLICY_ON_DEMAND + var rationaleTitle: String? = null + var rationaleContent: String? = null + @StringRes + var positiveBtnResId = android.R.string.ok + @StringRes + var negativeBtnResId = android.R.string.cancel + var callback: PermissionCallback? = null + + @Retention(AnnotationRetention.SOURCE) + @IntDef(RATIONALE_POLICY_ON_DEMAND, RATIONALE_POLICY_NEVER, RATIONALE_POLICY_ALWAYS) + annotation class RationalePolicy + + companion object { + const val RATIONALE_POLICY_ON_DEMAND = 1 + const val RATIONALE_POLICY_NEVER = 2 + const val RATIONALE_POLICY_ALWAYS = 3 + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.java deleted file mode 100644 index 75e574a..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.java +++ /dev/null @@ -1,158 +0,0 @@ -package me.ycdev.android.lib.common.perms; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.pm.PackageManager; -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.core.content.ContextCompat; - -import java.util.ArrayList; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class PermissionUtils { - /** - * Check if the caller has been granted a set of permissions. - * @return true if all permissions are already granted, - * false if at least one permission is not yet granted. - */ - public static boolean hasPermissions(@NonNull Context cxt, - @NonNull String... permissions) { - // At least one permission must be checked. - if (permissions.length < 1) { - return false; - } - - for (String perm : permissions) { - if (ContextCompat.checkSelfPermission(cxt, perm) - == PackageManager.PERMISSION_DENIED) { - return false; - } - } - return true; - } - - /** - * Filter out the denied permission. - * @return An array with length 0 will be returned if no denied permissions. - */ - public static String[] getDeniedPermissions(@NonNull Context cxt, - @NonNull String... permissions) { - ArrayList deniedPermissions = new ArrayList<>(permissions.length); - for (String perm : permissions) { - if (ContextCompat.checkSelfPermission(cxt, perm) - == PackageManager.PERMISSION_DENIED) { - deniedPermissions.add(perm); - } - } - return deniedPermissions.toArray(new String[deniedPermissions.size()]); - } - - /** - * Check if all requested permissions have been granted. - * @see Activity#onRequestPermissionsResult(int, String[], int[]) - * @see FragmentActivity#onRequestPermissionsResult(int, String[], int[]) - */ - public static boolean verifyPermissions(@NonNull int[] grantResults) { - // At least one result must be checked. - if (grantResults.length < 1) { - return false; - } - - // Verify that each required permission has been granted, otherwise return false. - for (int result : grantResults) { - if (result == PackageManager.PERMISSION_DENIED) { - return false; - } - } - return true; - } - - /** - * Request permissions. - */ - public static void requestPermissions(@NonNull Activity caller, - @NonNull PermissionRequestParams params) { - doRequestPermissions(caller, params); - } - - /** - * Request permissions. - */ - public static void requestPermissions(@NonNull Fragment caller, - @NonNull PermissionRequestParams params) { - doRequestPermissions(caller, params); - } - - private static void doRequestPermissions(final @NonNull Object caller, - final @NonNull PermissionRequestParams params) { - checkCallerSupported(caller); - - boolean shouldShowRationale = false; - if (params.rationalePolicy == PermissionRequestParams.RATIONALE_POLICY_ON_DEMOND) { - for (String perm : params.permissions) { - if (shouldShowRequestPermissionRationale(caller, perm)) { - shouldShowRationale = true; - break; - } - } - } else if (params.rationalePolicy == PermissionRequestParams.RATIONALE_POLICY_ALWAYS) { - shouldShowRationale = true; - } - - if (shouldShowRationale) { - AlertDialog dialog = new AlertDialog.Builder(getActivity(caller)) - .setTitle(params.rationaleTitle) - .setMessage(params.rationaleContent) - .setPositiveButton(params.positiveBtnResId, (dialog1, which) -> - doRequestPermissions(caller, params.permissions, params.requestCode)) - .setNegativeButton(params.negativeBtnResId, (dialog12, which) -> { - // act as if all permissions were denied - params.callback.onRationaleDenied(params.requestCode); - }).create(); - dialog.show(); - } else { - doRequestPermissions(caller, params.permissions, params.requestCode); - } - } - - private static void checkCallerSupported(@NonNull Object caller) { - if (!(caller instanceof Activity) && !(caller instanceof Fragment)) { - throw new IllegalArgumentException("The caller must be an Activity" + - " or a Fragment: " + caller.getClass().getName()); - } - } - - private static boolean shouldShowRequestPermissionRationale(@NonNull Object caller, - @NonNull String permission) { - if (caller instanceof Activity) { - return ActivityCompat.shouldShowRequestPermissionRationale((Activity) caller, permission); - } else if (caller instanceof Fragment) { - return ((Fragment) caller).shouldShowRequestPermissionRationale(permission); - } else { - return false; - } - } - - private static Activity getActivity(@NonNull Object caller) { - if (caller instanceof Activity) { - return (Activity) caller; - } else if (caller instanceof Fragment) { - return ((Fragment) caller).getActivity(); - } else { - return null; - } - } - - private static void doRequestPermissions(@NonNull Object caller, - @NonNull String[] perms, int requestCode) { - if (caller instanceof Activity) { - ActivityCompat.requestPermissions((Activity) caller, perms, requestCode); - } else if (caller instanceof Fragment) { - ((Fragment) caller).requestPermissions(perms, requestCode); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.kt new file mode 100644 index 0000000..b5b2681 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/perms/PermissionUtils.kt @@ -0,0 +1,170 @@ +package me.ycdev.android.lib.common.perms + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import java.util.ArrayList + +@Suppress("unused") +object PermissionUtils { + /** + * Check if the caller has been granted a set of permissions. + * @return true if all permissions are already granted, + * false if at least one permission is not yet granted. + */ + fun hasPermissions( + cxt: Context, + vararg permissions: String + ): Boolean { + // At least one permission must be checked. + if (permissions.isEmpty()) { + return false + } + + for (perm in permissions) { + if (ContextCompat.checkSelfPermission(cxt, perm) == PackageManager.PERMISSION_DENIED) { + return false + } + } + return true + } + + /** + * Filter out the denied permission. + * @return An array with length 0 will be returned if no denied permissions. + */ + fun getDeniedPermissions( + cxt: Context, + vararg permissions: String + ): Array { + val deniedPermissions = ArrayList(permissions.size) + for (perm in permissions) { + if (ContextCompat.checkSelfPermission(cxt, perm) == PackageManager.PERMISSION_DENIED) { + deniedPermissions.add(perm) + } + } + return deniedPermissions.toTypedArray() + } + + /** + * Check if all requested permissions have been granted. + * @see Activity.onRequestPermissionsResult + * @see FragmentActivity.onRequestPermissionsResult + */ + fun verifyPermissions(grantResults: IntArray): Boolean { + // At least one result must be checked. + if (grantResults.isEmpty()) { + return false + } + + // Verify that each required permission has been granted, otherwise return false. + for (result in grantResults) { + if (result == PackageManager.PERMISSION_DENIED) { + return false + } + } + return true + } + + /** + * Request permissions. + */ + fun requestPermissions( + caller: Activity, + params: PermissionRequestParams + ) { + doRequestPermissions(caller, params) + } + + /** + * Request permissions. + */ + fun requestPermissions( + caller: Fragment, + params: PermissionRequestParams + ) { + doRequestPermissions(caller, params) + } + + private fun doRequestPermissions( + caller: Any, + params: PermissionRequestParams + ) { + checkCallerSupported(caller) + + var shouldShowRationale = false + if (params.rationalePolicy == PermissionRequestParams.RATIONALE_POLICY_ON_DEMAND) { + for (perm in params.permissions!!) { + if (shouldShowRequestPermissionRationale(caller, perm)) { + shouldShowRationale = true + break + } + } + } else if (params.rationalePolicy == PermissionRequestParams.RATIONALE_POLICY_ALWAYS) { + shouldShowRationale = true + } + + if (shouldShowRationale) { + val dialog = AlertDialog.Builder(getActivity(caller)) + .setTitle(params.rationaleTitle) + .setMessage(params.rationaleContent) + .setPositiveButton(params.positiveBtnResId) { _, _ -> + doRequestPermissions( + caller, + params.permissions!!, + params.requestCode + ) + } + .setNegativeButton(params.negativeBtnResId) { _, _ -> + // act as if all permissions were denied + params.callback!!.onRationaleDenied(params.requestCode) + }.create() + dialog.show() + } else { + doRequestPermissions(caller, params.permissions!!, params.requestCode) + } + } + + private fun checkCallerSupported(caller: Any) { + if (caller !is Activity && caller !is Fragment) { + throw IllegalArgumentException( + "The caller must be an Activity" + + " or a Fragment: " + caller.javaClass.name + ) + } + } + + private fun shouldShowRequestPermissionRationale( + caller: Any, + permission: String + ): Boolean { + return if (caller is Activity) { + ActivityCompat.shouldShowRequestPermissionRationale(caller, permission) + } else (caller as? Fragment)?.shouldShowRequestPermissionRationale(permission) ?: false + } + + private fun getActivity(caller: Any): Activity? { + return caller as? Activity ?: if (caller is Fragment) { + caller.activity + } else { + null + } + } + + private fun doRequestPermissions( + caller: Any, + perms: Array, + requestCode: Int + ) { + if (caller is Activity) { + ActivityCompat.requestPermissions(caller, perms, requestCode) + } else if (caller is Fragment) { + caller.requestPermissions(perms, requestCode) + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.java b/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.java deleted file mode 100644 index afe980b..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.java +++ /dev/null @@ -1,120 +0,0 @@ -package me.ycdev.android.lib.common.provider; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.text.TextUtils; - -import me.ycdev.android.lib.common.utils.LibLogger; - -public abstract class InfoProvider extends ContentProvider { - private static final String TAG = "InfoProvider"; - - public static final String METHOD_REMOVE = "remove"; - public static final String METHOD_GET = "get"; - public static final String METHOD_PUT = "put"; - - public static final String KEY_TABLE = "table"; - public static final String KEY_NAME = "name"; - public static final String KEY_VALUE = "value"; - public static final String KEY_STATUS = "status"; - - public static final String TABLE_DEFAULT = "default"; - - protected abstract boolean remove(@NonNull String table, @NonNull String name); - protected abstract String get(@NonNull String table, @NonNull String name); - protected abstract boolean put(@NonNull String table, @NonNull String name, @NonNull String value); - - @Override - public boolean onCreate() { - LibLogger.d(TAG, "onCreate"); - return false; - } - - @Nullable - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, - @Nullable String[] selectionArgs, @Nullable String sortOrder) { - LibLogger.d(TAG, "query: %s", uri); - return null; - } - - @Nullable - @Override - public String getType(@NonNull Uri uri) { - LibLogger.d(TAG, "getType: %s", uri); - return null; - } - - @Nullable - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - LibLogger.d(TAG, "insert: %s", uri); - return null; - } - - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { - LibLogger.d(TAG, "delete: %s", uri); - return 0; - } - - @Override - public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, - @Nullable String[] selectionArgs) { - LibLogger.d(TAG, "update: %s", uri); - return 0; - } - - @Nullable - @Override - public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { - if (extras == null) { - LibLogger.e(TAG, "no args for method [%s]", method); - return null; - } - - String table = extras.getString(KEY_TABLE); - String name = extras.getString(KEY_NAME); - String value = extras.getString(KEY_VALUE); - LibLogger.d(TAG, "call [%s] for [%s] in table [%s]", method, name, table); - if (TextUtils.isEmpty(method) || TextUtils.isEmpty(name)) { - LibLogger.w(TAG, "no method or name for the request"); - return null; - } - if (TextUtils.isEmpty(table)) { - table = TABLE_DEFAULT; - } - - Bundle result = new Bundle(); - switch (method) { - case METHOD_REMOVE: { - result.putString(KEY_VALUE, get(table, name)); // old value - result.putBoolean(KEY_STATUS, remove(table, name)); - break; - } - case METHOD_GET: { - result.putString(KEY_VALUE, get(table, name)); - break; - } - case METHOD_PUT: { - if (TextUtils.isEmpty(value)) { - LibLogger.w(TAG, "no value for the request"); - return null; - } - result.putString(KEY_VALUE, get(table, name)); // old value - result.putBoolean(KEY_STATUS, put(table, name, value)); - break; - } - default: { - LibLogger.e(TAG, "unknown method [%s]", method); - return null; - } - } - return result; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.kt new file mode 100644 index 0000000..9e3321f --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProvider.kt @@ -0,0 +1,116 @@ +package me.ycdev.android.lib.common.provider + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import android.text.TextUtils + +import me.ycdev.android.lib.common.utils.LibLogger + +abstract class InfoProvider : ContentProvider() { + + protected abstract fun remove(table: String, name: String): Boolean + protected abstract fun get(table: String, name: String): String? + protected abstract fun put(table: String, name: String, value: String): Boolean + + override fun onCreate(): Boolean { + LibLogger.d(TAG, "onCreate") + return false + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + LibLogger.d(TAG, "query: %s", uri) + return null + } + + override fun getType(uri: Uri): String? { + LibLogger.d(TAG, "getType: %s", uri) + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + LibLogger.d(TAG, "insert: %s", uri) + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + LibLogger.d(TAG, "delete: %s", uri) + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + LibLogger.d(TAG, "update: %s", uri) + return 0 + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + if (extras == null) { + LibLogger.e(TAG, "no args for method [%s]", method) + return null + } + + var table = extras.getString(KEY_TABLE) + val name = extras.getString(KEY_NAME) + val value = extras.getString(KEY_VALUE) + LibLogger.d(TAG, "call [%s] for [%s] in table [%s]", method, name, table) + if (TextUtils.isEmpty(method) || TextUtils.isEmpty(name)) { + LibLogger.w(TAG, "no method or name for the request") + return null + } + if (TextUtils.isEmpty(table)) { + table = TABLE_DEFAULT + } + + val result = Bundle() + when (method) { + METHOD_REMOVE -> { + result.putString(KEY_VALUE, get(table!!, name!!)) // old value + result.putBoolean(KEY_STATUS, remove(table, name)) + } + METHOD_GET -> { + result.putString(KEY_VALUE, get(table!!, name!!)) + } + METHOD_PUT -> { + if (TextUtils.isEmpty(value)) { + LibLogger.w(TAG, "no value for the request") + return null + } + result.putString(KEY_VALUE, get(table!!, name!!)) // old value + result.putBoolean(KEY_STATUS, put(table, name, value!!)) + } + else -> { + LibLogger.e(TAG, "unknown method [%s]", method) + return null + } + } + return result + } + + companion object { + private const val TAG = "InfoProvider" + + const val METHOD_REMOVE = "remove" + const val METHOD_GET = "get" + const val METHOD_PUT = "put" + + const val KEY_TABLE = "table" + const val KEY_NAME = "name" + const val KEY_VALUE = "value" + const val KEY_STATUS = "status" + + const val TABLE_DEFAULT = "default" + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.java b/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.java deleted file mode 100644 index 15bc859..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.java +++ /dev/null @@ -1,150 +0,0 @@ -package me.ycdev.android.lib.common.provider; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.text.TextUtils; - -import me.ycdev.android.lib.common.utils.LibLogger; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class InfoProviderClient { - private static final String TAG = "InfoProviderClient"; - - private ContentResolver mResolver; - private String mAuthority; - - public InfoProviderClient(@NonNull Context cxt, @NonNull String authority) { - mResolver = cxt.getApplicationContext().getContentResolver(); - mAuthority = authority; - } - - private Uri getUriFor(@Nullable String table, @NonNull String name) { - if (TextUtils.isEmpty(table)) { - table = InfoProvider.TABLE_DEFAULT; - } - return new Uri.Builder().scheme("content").authority(mAuthority) - .appendPath(table).appendPath(name) - .build(); - } - - public void registerObserver(@Nullable String table, @NonNull String name, - @NonNull ContentObserver observer) { - Uri uri = getUriFor(table, name); - mResolver.registerContentObserver(uri, true, observer); - } - - public void unregisterObserver(@NonNull ContentObserver observer) { - mResolver.unregisterContentObserver(observer); - } - - public boolean remove(@Nullable String table, @NonNull String name) { - try { - Uri uri = getUriFor(table, name); - Bundle args = new Bundle(); - args.putString(InfoProvider.KEY_TABLE, table); - args.putString(InfoProvider.KEY_NAME, name); - Bundle result = mResolver.call(uri, InfoProvider.METHOD_REMOVE, null, args); - if (result == null) { - LibLogger.e(TAG, "Cannot call method [%s]", InfoProvider.METHOD_REMOVE); - return false; - } - - boolean success = result.getBoolean(InfoProvider.KEY_STATUS); - String oldValue = result.getString(InfoProvider.KEY_VALUE); - if (success && !TextUtils.isEmpty(oldValue)) { - mResolver.notifyChange(uri, null); - } - - return success; - } catch (Exception e) { - LibLogger.w(TAG, "Failed to remove [%s] in table [%s]", name, table); - return false; - } - } - - public String getString(@Nullable String table, @NonNull String name, @Nullable String defValue) { - try { - Uri uri = getUriFor(table, name); - Bundle args = new Bundle(); - args.putString(InfoProvider.KEY_TABLE, table); - args.putString(InfoProvider.KEY_NAME, name); - Bundle result = mResolver.call(uri, InfoProvider.METHOD_GET, null, args); - if (result == null) { - LibLogger.e(TAG, "Cannot call method [%s]", InfoProvider.METHOD_GET); - return defValue; - } - - String value = result.getString(InfoProvider.KEY_VALUE); - if (value == null) { - value = defValue; - } - return value; - } catch (Exception e) { - LibLogger.w(TAG, "Failed to get value for [%s] in table [%s]", name, table); - } - return defValue; - } - - public boolean putString(@Nullable String table, @NonNull String name, @NonNull String value) { - try { - Uri uri = getUriFor(table, name); - Bundle args = new Bundle(); - args.putString(InfoProvider.KEY_TABLE, table); - args.putString(InfoProvider.KEY_NAME, name); - args.putString(InfoProvider.KEY_VALUE, value); - Bundle result = mResolver.call(uri, InfoProvider.METHOD_PUT, null, args); - if (result == null) { - LibLogger.e(TAG, "Cannot call method [%s]", InfoProvider.METHOD_PUT); - return false; - } - - boolean success = result.getBoolean(InfoProvider.KEY_STATUS); - String oldValue = result.getString(InfoProvider.KEY_VALUE); - if (success && !TextUtils.equals(oldValue, value)) { - mResolver.notifyChange(uri, null); - } - - return success; - } catch (Exception e) { - LibLogger.w(TAG, "Failed to put value for [%s] in table [%s]", name, table); - } - return false; - } - - public boolean getBoolean(@Nullable String table, @NonNull String name, boolean defValue) { - try { - String result = getString(table, name, null); - if (!TextUtils.isEmpty(result)) { - return Boolean.parseBoolean(result); - } - } catch (Exception e) { - LibLogger.w(TAG, "Failed to get boolean value for [%s] in table [%s]", name, table); - } - return defValue; - } - - public boolean putBoolean(@Nullable String table, @NonNull String name, boolean value) { - return putString(table, name, Boolean.toString(value)); - } - - public int getInt(@Nullable String table, @NonNull String name, int defValue) { - try { - String result = getString(table, name, null); - if (!TextUtils.isEmpty(result)) { - return Integer.parseInt(result); - } - } catch (Exception e) { - LibLogger.w(TAG, "Failed to get int value for [%s] in table [%s]", name, table); - } - return defValue; - } - - public boolean putInt(@Nullable String table, @NonNull String name, int value) { - return putString(table, name, Integer.toString(value)); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.kt new file mode 100644 index 0000000..4088a95 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/provider/InfoProviderClient.kt @@ -0,0 +1,151 @@ +package me.ycdev.android.lib.common.provider + +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Bundle +import android.text.TextUtils + +import me.ycdev.android.lib.common.utils.LibLogger + +class InfoProviderClient(cxt: Context, private val authority: String) { + private val resolver: ContentResolver = cxt.applicationContext.contentResolver + + private fun getUriFor(table: String?, name: String): Uri { + var tableTmp = table + if (TextUtils.isEmpty(tableTmp)) { + tableTmp = InfoProvider.TABLE_DEFAULT + } + return Uri.Builder().scheme("content").authority(authority) + .appendPath(tableTmp).appendPath(name) + .build() + } + + fun registerObserver( + table: String?, + name: String, + observer: ContentObserver + ) { + val uri = getUriFor(table, name) + resolver.registerContentObserver(uri, true, observer) + } + + fun unregisterObserver(observer: ContentObserver) { + resolver.unregisterContentObserver(observer) + } + + fun remove(table: String?, name: String): Boolean { + try { + val uri = getUriFor(table, name) + val args = Bundle() + args.putString(InfoProvider.KEY_TABLE, table) + args.putString(InfoProvider.KEY_NAME, name) + val result = resolver.call(uri, InfoProvider.METHOD_REMOVE, null, args) + if (result == null) { + LibLogger.e(TAG, "Cannot call method [%s]", InfoProvider.METHOD_REMOVE) + return false + } + + val success = result.getBoolean(InfoProvider.KEY_STATUS) + val oldValue = result.getString(InfoProvider.KEY_VALUE) + if (success && !TextUtils.isEmpty(oldValue)) { + resolver.notifyChange(uri, null) + } + + return success + } catch (e: Exception) { + LibLogger.w(TAG, "Failed to remove [%s] in table [%s]", name, table) + return false + } + } + + fun getString(table: String?, name: String, defValue: String?): String? { + try { + val uri = getUriFor(table, name) + val args = Bundle() + args.putString(InfoProvider.KEY_TABLE, table) + args.putString(InfoProvider.KEY_NAME, name) + val result = resolver.call(uri, InfoProvider.METHOD_GET, null, args) + if (result == null) { + LibLogger.e(TAG, "Cannot call method [%s]", InfoProvider.METHOD_GET) + return defValue + } + + var value = result.getString(InfoProvider.KEY_VALUE) + if (value == null) { + value = defValue + } + return value + } catch (e: Exception) { + LibLogger.w(TAG, "Failed to get value for [%s] in table [%s]", name, table) + } + + return defValue + } + + fun putString(table: String?, name: String, value: String): Boolean { + try { + val uri = getUriFor(table, name) + val args = Bundle() + args.putString(InfoProvider.KEY_TABLE, table) + args.putString(InfoProvider.KEY_NAME, name) + args.putString(InfoProvider.KEY_VALUE, value) + val result = resolver.call(uri, InfoProvider.METHOD_PUT, null, args) + if (result == null) { + LibLogger.e(TAG, "Cannot call method [%s]", InfoProvider.METHOD_PUT) + return false + } + + val success = result.getBoolean(InfoProvider.KEY_STATUS) + val oldValue = result.getString(InfoProvider.KEY_VALUE) + if (success && !TextUtils.equals(oldValue, value)) { + resolver.notifyChange(uri, null) + } + + return success + } catch (e: Exception) { + LibLogger.w(TAG, "Failed to put value for [%s] in table [%s]", name, table) + } + + return false + } + + fun getBoolean(table: String?, name: String, defValue: Boolean): Boolean { + try { + val result = getString(table, name, null) + if (!TextUtils.isEmpty(result)) { + return java.lang.Boolean.parseBoolean(result) + } + } catch (e: Exception) { + LibLogger.w(TAG, "Failed to get boolean value for [%s] in table [%s]", name, table) + } + + return defValue + } + + fun putBoolean(table: String?, name: String, value: Boolean): Boolean { + return putString(table, name, java.lang.Boolean.toString(value)) + } + + fun getInt(table: String?, name: String, defValue: Int): Int { + try { + val result = getString(table, name, null) + if (!TextUtils.isEmpty(result)) { + return Integer.parseInt(result!!) + } + } catch (e: Exception) { + LibLogger.w(TAG, "Failed to get int value for [%s] in table [%s]", name, table) + } + + return defValue + } + + fun putInt(table: String?, name: String, value: Int): Boolean { + return putString(table, name, Integer.toString(value)) + } + + companion object { + private const val TAG = "InfoProviderClient" + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.java b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.java deleted file mode 100644 index 230e11b..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.java +++ /dev/null @@ -1,126 +0,0 @@ -package me.ycdev.android.lib.common.tracker; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.BatteryManager; -import androidx.annotation.NonNull; - -import me.ycdev.android.lib.common.utils.LibLogger; -import me.ycdev.android.lib.common.wrapper.BroadcastHelper; -import me.ycdev.android.lib.common.wrapper.IntentHelper; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class BatteryInfoTracker extends WeakTracker { - private static final String TAG = "BatteryInfoTracker"; - - @SuppressLint("StaticFieldLeak") - private static BatteryInfoTracker sInstance = null; - - public static class BatteryInfo { - private int level; - private int scale; - public int percent; // percent corrected by us - public double temperature; - } - - public interface BatteryInfoListener { - /** - * @param newData Read-only, cannot be modified. - */ - void onBatteryInfoUpdated(BatteryInfo newData); - } - - private Context mContext; - private BatteryInfo mBatteryInfo; - private int mBatteryScale = 100; - - private BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - LibLogger.i(TAG, "Received: " + intent.getAction()); - updateBatteryInfo(intent); - } - }; - - public static synchronized BatteryInfoTracker getInsance(Context cxt) { - if (sInstance == null) { - sInstance = new BatteryInfoTracker(cxt); - } - return sInstance; - } - - private BatteryInfoTracker(Context cxt) { - mContext = cxt.getApplicationContext(); - } - - @Override - protected void startTracker() { - LibLogger.i(TAG, "BatteryInfo tracker is running"); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_BATTERY_CHANGED); - Intent intent = BroadcastHelper.registerForExternal(mContext, mBatteryInfoReceiver, filter); - if (intent != null) { - updateBatteryInfo(intent); - } - } - - @Override - protected void stopTracker() { - LibLogger.i(TAG, "BatteryInfo tracker is stopped"); - mContext.unregisterReceiver(mBatteryInfoReceiver); - } - - @Override - protected void onListenerAdded(@NonNull BatteryInfoListener listener) { - if (mBatteryInfo != null) { - listener.onBatteryInfoUpdated(mBatteryInfo); - } - } - - private void updateBatteryInfo(Intent intent) { - final BatteryInfo data = new BatteryInfo(); - data.level = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_LEVEL, 0); - data.scale = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_SCALE, 100); - data.temperature = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_TEMPERATURE, 0) * 0.1; - - fixData(data); - - int reportedPercent = data.scale < 1 ? data.level : (data.level * 100 / data.scale); - if (reportedPercent >= 0 && reportedPercent <= 100) - data.percent = reportedPercent; - else if (reportedPercent < 0) { - data.percent = 0; - } else if (reportedPercent > 100) { - data.percent = 100; - } - - LibLogger.d(TAG, "battery info updated, " + dump(data)); - mBatteryInfo = data; - notifyListeners(listener -> listener.onBatteryInfoUpdated(data)); - } - - private void fixData(BatteryInfo data) { - // We may need to update 'mBatteryScale' - if (data.level > data.scale) { - LibLogger.e(TAG, "Bad battery data! level: %d, scale: %d, mBatteryScale: %d", - data.level, data.scale, mBatteryScale); - if (data.level % 100 == 0) { - mBatteryScale = data.level; - } - } - - // We may need to correct the 'data.scale' - if (data.scale < mBatteryScale) { - data.scale = mBatteryScale; - } - } - - private static String dump(BatteryInfo data) { - return "level:" + data.level + ", scale:" + data.scale - + ", percent: " + data.percent; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt new file mode 100644 index 0000000..0d4e008 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt @@ -0,0 +1,130 @@ +package me.ycdev.android.lib.common.tracker + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager + +import me.ycdev.android.lib.common.utils.LibLogger +import me.ycdev.android.lib.common.wrapper.BroadcastHelper +import me.ycdev.android.lib.common.wrapper.IntentHelper + +@Suppress("unused") +class BatteryInfoTracker private constructor(cxt: Context) : + WeakTracker() { + + private val context: Context = cxt.applicationContext + private var batteryInfo: BatteryInfo? = null + private var batteryScale = 100 + + private val batteryInfoReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + LibLogger.i(TAG, "Received: ${intent.action}") + updateBatteryInfo(intent) + } + } + + data class BatteryInfo( + var level: Int = 0, + var scale: Int = 0, + var percent: Int = 0, // percent corrected by us + var status: Int = BatteryManager.BATTERY_STATUS_UNKNOWN, + var plugged: Int = 0, + var voltage: Int = 0, + var temperature: Double = 0.0 + ) + + interface BatteryInfoListener { + /** + * @param newData Read-only, cannot be modified. + */ + fun onBatteryInfoUpdated(newData: BatteryInfo) + } + + override fun startTracker() { + LibLogger.i(TAG, "BatteryInfo tracker is running") + val filter = IntentFilter() + filter.addAction(Intent.ACTION_BATTERY_CHANGED) + val intent = BroadcastHelper.registerForExternal(context, batteryInfoReceiver, filter) + if (intent != null) { + updateBatteryInfo(intent) + } + } + + override fun stopTracker() { + LibLogger.i(TAG, "BatteryInfo tracker is stopped") + context.unregisterReceiver(batteryInfoReceiver) + } + + override fun onListenerAdded(listener: BatteryInfoListener) { + batteryInfo?.let { listener.onBatteryInfoUpdated(it) } + } + + private fun updateBatteryInfo(intent: Intent) { + val data = BatteryInfo() + data.level = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_LEVEL, 0) + data.scale = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_SCALE, 100) + data.status = IntentHelper.getIntExtra( + intent, + BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN + ) + data.plugged = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_PLUGGED, 0) + data.voltage = IntentHelper.getIntExtra(intent, BatteryManager.EXTRA_VOLTAGE, 0) + data.temperature = IntentHelper.getIntExtra( + intent, BatteryManager.EXTRA_TEMPERATURE, 0 + ) * 0.1 + + fixData(data) + + val reportedPercent = if (data.scale < 1) data.level else data.level * 100 / data.scale + data.percent = when { + reportedPercent < 0 -> 0 + reportedPercent > 100 -> 100 + else -> reportedPercent + } + + LibLogger.d(TAG, "battery info updated: $data") + batteryInfo = data + notifyListeners { it.onBatteryInfoUpdated(data) } + } + + private fun fixData(data: BatteryInfo) { + // We may need to update 'batteryScale' + if (data.level > data.scale) { + LibLogger.e( + TAG, "Bad battery data! level: %d, scale: %d, batteryScale: %d", + data.level, data.scale, batteryScale + ) + if (data.level % 100 == 0) { + batteryScale = data.level + } + } + + // We may need to correct the 'data.scale' + if (data.scale < batteryScale) { + data.scale = batteryScale + } + } + + companion object { + private const val TAG = "BatteryInfoTracker" + + @SuppressLint("StaticFieldLeak") + private var instance: BatteryInfoTracker? = null + + @Synchronized + fun getInstance(cxt: Context): BatteryInfoTracker { + if (instance == null) { + synchronized(BatteryInfoTracker::class.java) { + if (instance == null) { + instance = BatteryInfoTracker(cxt) + } + } + } + return instance!! + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.java b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.java deleted file mode 100644 index 94c653a..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.java +++ /dev/null @@ -1,114 +0,0 @@ -package me.ycdev.android.lib.common.tracker; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.PowerManager; -import androidx.annotation.NonNull; - -import me.ycdev.android.lib.common.compat.PowerManagerCompat; -import me.ycdev.android.lib.common.utils.LibLogger; -import me.ycdev.android.lib.common.wrapper.BroadcastHelper; - -/** - * A tracker to track the interactive state of the device. - */ -@SuppressWarnings({"unused", "WeakerAccess"}) -public class InteractiveStateTracker extends WeakTracker { - private static final String TAG = "InteractiveStateTracker"; - - public interface InteractiveStateListener { - /** - * Will be invoked when Intent.ACTION_SCREEN_ON or Intent.ACTION_SCREEN_OFF received. - */ - void onInteractiveChanged(boolean interactive); - - /** - * Will be invoked when Intent.ACTION_USER_PRESENT received. - */ - void onUserPresent(); - } - - private Context mAppContext; - private boolean mInteractive; - private boolean mNeedRefreshInteractiveState; - - private BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - LibLogger.i(TAG, "Received: " + action); - if (Intent.ACTION_USER_PRESENT.equals(action)) { - notifyUserPresent(); - } else { - mInteractive = Intent.ACTION_SCREEN_ON.equals(action); - notifyInteractiveChanged(mInteractive); - } - } - }; - - @SuppressLint("StaticFieldLeak") - private static volatile InteractiveStateTracker sInstance; - - private InteractiveStateTracker(Context cxt) { - mAppContext = cxt.getApplicationContext(); - } - - public static InteractiveStateTracker getInstance(Context cxt) { - if (sInstance == null) { - synchronized (InteractiveStateTracker.class) { - if (sInstance == null) { - sInstance = new InteractiveStateTracker(cxt); - } - } - } - return sInstance; - } - - public boolean isInteractive() { - if (mNeedRefreshInteractiveState) { - refreshInteractiveState(); - } - return mInteractive; - } - - private void refreshInteractiveState() { - PowerManager pm = (PowerManager) mAppContext.getSystemService(Context.POWER_SERVICE); - mInteractive = PowerManagerCompat.isScreenOn(pm); - } - - @Override - protected void startTracker() { - LibLogger.i(TAG, "Screen on/off tracker is running"); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_SCREEN_OFF); - filter.addAction(Intent.ACTION_USER_PRESENT); - BroadcastHelper.registerForExternal(mAppContext, mReceiver, filter); - - refreshInteractiveState(); - mNeedRefreshInteractiveState = false; - } - - @Override - protected void stopTracker() { - LibLogger.i(TAG, "Screen on/off tracker is stopped"); - mAppContext.unregisterReceiver(mReceiver); - mNeedRefreshInteractiveState = true; - } - - @Override - protected void onListenerAdded(@NonNull InteractiveStateListener listener) { - listener.onInteractiveChanged(mInteractive); - } - - private void notifyInteractiveChanged(final boolean interactive) { - notifyListeners(listener -> listener.onInteractiveChanged(interactive)); - } - - private void notifyUserPresent() { - notifyListeners(InteractiveStateListener::onUserPresent); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.kt new file mode 100644 index 0000000..c4f81fa --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/InteractiveStateTracker.kt @@ -0,0 +1,110 @@ +package me.ycdev.android.lib.common.tracker + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager + +import me.ycdev.android.lib.common.utils.LibLogger +import me.ycdev.android.lib.common.wrapper.BroadcastHelper + +/** + * A tracker to track the interactive state of the device. + */ +@Suppress("unused") +class InteractiveStateTracker private constructor(cxt: Context) : + WeakTracker() { + + private val appContext: Context = cxt.applicationContext + private var interactive: Boolean = false + private var needRefreshInteractiveState: Boolean = false + + private val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + LibLogger.i(TAG, "Received: $action") + if (Intent.ACTION_USER_PRESENT == action) { + notifyUserPresent() + } else { + interactive = Intent.ACTION_SCREEN_ON == action + notifyInteractiveChanged(interactive) + } + } + } + + val isInteractive: Boolean + get() { + if (needRefreshInteractiveState) { + refreshInteractiveState() + } + return interactive + } + + interface InteractiveStateListener { + /** + * Will be invoked when Intent.ACTION_SCREEN_ON or Intent.ACTION_SCREEN_OFF received. + */ + fun onInteractiveChanged(interactive: Boolean) + + /** + * Will be invoked when Intent.ACTION_USER_PRESENT received. + */ + fun onUserPresent() + } + + private fun refreshInteractiveState() { + val pm = appContext.getSystemService(Context.POWER_SERVICE) as PowerManager + interactive = pm.isInteractive + } + + override fun startTracker() { + LibLogger.i(TAG, "Screen on/off tracker is running") + val filter = IntentFilter() + filter.addAction(Intent.ACTION_SCREEN_ON) + filter.addAction(Intent.ACTION_SCREEN_OFF) + filter.addAction(Intent.ACTION_USER_PRESENT) + BroadcastHelper.registerForExternal(appContext, receiver, filter) + + refreshInteractiveState() + needRefreshInteractiveState = false + } + + override fun stopTracker() { + LibLogger.i(TAG, "Screen on/off tracker is stopped") + appContext.unregisterReceiver(receiver) + needRefreshInteractiveState = true + } + + override fun onListenerAdded(listener: InteractiveStateListener) { + listener.onInteractiveChanged(interactive) + } + + private fun notifyInteractiveChanged(interactive: Boolean) { + notifyListeners { it.onInteractiveChanged(interactive) } + } + + private fun notifyUserPresent() { + notifyListeners { it.onUserPresent() } + } + + companion object { + private const val TAG = "InteractiveStateTracker" + + @SuppressLint("StaticFieldLeak") + @Volatile + private var instance: InteractiveStateTracker? = null + + fun getInstance(cxt: Context): InteractiveStateTracker { + if (instance == null) { + synchronized(InteractiveStateTracker::class.java) { + if (instance == null) { + instance = InteractiveStateTracker(cxt) + } + } + } + return instance!! + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.java b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.java deleted file mode 100644 index 9862718..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.java +++ /dev/null @@ -1,18 +0,0 @@ -package me.ycdev.android.lib.common.tracker; - -import me.ycdev.android.lib.common.utils.WeakListenerManager; - -public abstract class WeakTracker extends WeakListenerManager { - protected abstract void startTracker(); - protected abstract void stopTracker(); - - @Override - protected void onFirstListenerAdd() { - startTracker(); - } - - @Override - protected void onLastListenerRemoved() { - stopTracker(); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.kt new file mode 100644 index 0000000..8b73b43 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/WeakTracker.kt @@ -0,0 +1,16 @@ +package me.ycdev.android.lib.common.tracker + +import me.ycdev.android.lib.common.utils.WeakListenerManager + +abstract class WeakTracker : WeakListenerManager() { + protected abstract fun startTracker() + protected abstract fun stopTracker() + + override fun onFirstListenerAdd() { + startTracker() + } + + override fun onLastListenerRemoved() { + stopTracker() + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.java b/baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.java deleted file mode 100644 index 6e6c410..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.ycdev.android.lib.common.type; - -public class BooleanHolder { - public boolean value; - - public BooleanHolder(boolean value) { - this.value = value; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.kt new file mode 100644 index 0000000..d50ca04 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/type/BooleanHolder.kt @@ -0,0 +1,3 @@ +package me.ycdev.android.lib.common.type + +class BooleanHolder(var value: Boolean) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.java b/baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.java deleted file mode 100644 index 9ed67d2..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.ycdev.android.lib.common.type; - -public class IntegerHolder { - public int value; - - public IntegerHolder(int value) { - this.value = value; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.kt new file mode 100644 index 0000000..e5f3232 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/type/IntegerHolder.kt @@ -0,0 +1,3 @@ +package me.ycdev.android.lib.common.type + +class IntegerHolder(var value: Int) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.java b/baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.java deleted file mode 100644 index d61c8a6..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.java +++ /dev/null @@ -1,9 +0,0 @@ -package me.ycdev.android.lib.common.type; - -public class LongHolder { - public long value; - - public LongHolder(long value) { - this.value = value; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.kt new file mode 100644 index 0000000..c242bfb --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/type/LongHolder.kt @@ -0,0 +1,3 @@ +package me.ycdev.android.lib.common.type + +class LongHolder(var value: Long) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/AndroidVersionUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/AndroidVersionUtils.java deleted file mode 100644 index 0df30ec..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/AndroidVersionUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.os.Build; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class AndroidVersionUtils { - /** - * Ice Cream Sandwich MR1 (4.0.3) and higher version (API 15+) - */ - public static boolean hasIceCreamSandwichMR1() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; - } - - /** - * Jelly Bean (4.1) and higher version (API 16+) - */ - public static boolean hasJellyBean() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - } - - /** - * Jelly Bean (4.2) and higher version (API 17+) - */ - public static boolean hasJellyBeanMR1() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; - } - - - /** - * Jelly Bean (4.3) and higher version (API 18+) - */ - public static boolean hasJellyBeanMR2() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.java deleted file mode 100644 index edfd502..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.java +++ /dev/null @@ -1,81 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.Application; -import android.content.Context; -import android.os.Process; -import androidx.annotation.Nullable; -import android.text.TextUtils; - -import java.io.IOException; -import java.util.List; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ApplicationUtils { - private static final String TAG = "ApplicationUtils"; - - @SuppressLint("StaticFieldLeak") - private static Application sApp; - private static String sProcessName; - - /** - * Must be called in Application#onCreate() ASAP. - */ - public static void initApplication(Application app) { - sApp = app; - getCurrentProcessName(); // init process name in UI thread - } - - public static Context getApplicationContext() { - Preconditions.checkNotNull(sApp); - return sApp; - } - - public static String getCurrentProcessName() { - Preconditions.checkNotNull(sApp); - - if (!TextUtils.isEmpty(sProcessName)) { - return sProcessName; - } - - // try AMS first - int pid = Process.myPid(); - sProcessName = getProcessNameFromAMS(sApp, pid); - if (!TextUtils.isEmpty(sProcessName)) { - return sProcessName; - } - - // try "/proc" - sProcessName = getProcessNameFromProc(pid); - return sProcessName; - } - - @Nullable - private static String getProcessNameFromAMS(Context cxt, int pid) { - ActivityManager am = SystemServiceHelper.getActivityManager(cxt); - List runningApps = - SystemServiceHelper.getRunningAppProcesses(am); - for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) { - if (procInfo.pid == pid) { - return procInfo.processName; - } - } - return null; - } - - @Nullable - private static String getProcessNameFromProc(int pid) { - String processName = null; - try { - String cmdlineFile = "/proc/" + pid + "/cmdline"; - processName = IoUtils.readAllLines(cmdlineFile); - } catch (IOException e) { - LibLogger.w(TAG, "failed to read process name from /proc for pid [%d]", pid); - } - if (processName != null) { - processName = processName.trim(); - } - return processName; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.kt new file mode 100644 index 0000000..d5f8961 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ApplicationUtils.kt @@ -0,0 +1,74 @@ +package me.ycdev.android.lib.common.utils + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import android.os.Process +import android.text.TextUtils +import java.io.IOException + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ApplicationUtils { + private const val TAG = "ApplicationUtils" + + @SuppressLint("StaticFieldLeak") + private lateinit var app: Application + private var processName: String? = null + + val application: Application + get() { + Preconditions.checkNotNull(app) + return app + } + + // try AMS first + // try "/proc" + val currentProcessName: String? + get() { + Preconditions.checkNotNull(app) + + if (!TextUtils.isEmpty(processName)) { + return processName + } + val pid = Process.myPid() + processName = getProcessNameFromAMS(app, pid) + if (!TextUtils.isEmpty(processName)) { + return processName + } + processName = getProcessNameFromProc(pid) + return processName + } + + /** + * Must be called in Application#onCreate() ASAP. + */ + fun initApplication(app: Application) { + this.app = app + } + + private fun getProcessNameFromAMS(cxt: Context, pid: Int): String? { + val am = SystemServiceHelper.getActivityManager(cxt) ?: return null + val runningApps = SystemServiceHelper.getRunningAppProcesses(am) + for (procInfo in runningApps) { + if (procInfo.pid == pid) { + return procInfo.processName + } + } + return null + } + + private fun getProcessNameFromProc(pid: Int): String? { + var processName: String? = null + try { + val cmdlineFile = "/proc/$pid/cmdline" + processName = IoUtils.readAllLines(cmdlineFile) + } catch (e: IOException) { + LibLogger.w(TAG, "failed to read process name from /proc for pid [%d]", pid) + } + + if (processName != null) { + processName = processName.trim { it <= ' ' } + } + return processName + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.java deleted file mode 100644 index 86e6f2c..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class DateTimeUtils { - /** - * Generate file name from system time in the format "yyyyMMdd-HHmmss-SSS", - * @param sysTime System time in milliseconds - */ - @NonNull - public static String generateFileName(long sysTime) { - return new SimpleDateFormat("yyyyMMdd-HHmmss-SSS", Locale.US).format(new Date(sysTime)); - } - - /** - * Parse system time from string in the format "yyyyMMdd-HHmmss-SSS", - * @param timeStr Time string in the format "yyyyMMdd-HHmmss-SSS" - */ - public static long parseFileName(@NonNull String timeStr) throws ParseException { - return new SimpleDateFormat("yyyyMMdd-HHmmss-SSS", Locale.US).parse(timeStr).getTime(); - } - - /** - * Generate file name from system time in the format "yyyy-MM-dd HH:mm:ss:SSS", - * @param timeStamp System time in milliseconds - */ - @NonNull - public static String getReadableTimeStamp(long timeStamp) { - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS", Locale.US).format(new Date(timeStamp)); - } - - /** - * Format the time usage to string like "1d17h37m3s728ms" - */ - @NonNull - public static String getReadableTimeUsage(long timeUsageMs) { - long millisecondsLeft = timeUsageMs % 1000; - if (timeUsageMs == millisecondsLeft) { - return millisecondsLeft + "ms"; - } - - long seconds = timeUsageMs / 1000; - long secondsLeft = seconds % 60; - if (secondsLeft == seconds) { - return secondsLeft + "s" + millisecondsLeft + "ms"; - } - - long minutes = seconds / 60; - long minutesLeft = minutes % 60; - if (minutesLeft == minutes) { - return minutesLeft + "m" + secondsLeft + "s" + millisecondsLeft + "ms"; - } - - long hours = minutes / 60; - long hoursLeft = hours % 24; - if (hoursLeft == hours) { - return hoursLeft + "h" + minutesLeft + "m" + secondsLeft + "s" + millisecondsLeft + "ms"; - } - - long days = hours / 24; - return days + "d" + hoursLeft + "h" + minutesLeft + "m" + secondsLeft + "s" + millisecondsLeft + "ms"; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.kt new file mode 100644 index 0000000..eb833cb --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DateTimeUtils.kt @@ -0,0 +1,65 @@ +package me.ycdev.android.lib.common.utils + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Suppress("unused") +object DateTimeUtils { + /** + * Generate file name from system time in the format "yyyyMMdd-HHmmss-SSS", + * @param sysTime System time in milliseconds + */ + fun generateFileName(sysTime: Long): String { + return SimpleDateFormat("yyyyMMdd-HHmmss-SSS", Locale.US).format(Date(sysTime)) + } + + /** + * Parse system time from string in the format "yyyyMMdd-HHmmss-SSS", + * @param timeStr Time string in the format "yyyyMMdd-HHmmss-SSS" + */ + @Throws(ParseException::class) + fun parseFileName(timeStr: String): Long { + return SimpleDateFormat("yyyyMMdd-HHmmss-SSS", Locale.US).parse(timeStr).time + } + + /** + * Generate file name from system time in the format "yyyy-MM-dd HH:mm:ss:SSS", + * @param timeStamp System time in milliseconds + */ + fun getReadableTimeStamp(timeStamp: Long): String { + return SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS", Locale.US).format(Date(timeStamp)) + } + + /** + * Format the time usage to string like "1d17h37m3s728ms" + */ + fun getReadableTimeUsage(timeUsageMs: Long): String { + val millisecondsLeft = timeUsageMs % 1000 + if (timeUsageMs == millisecondsLeft) { + return millisecondsLeft.toString() + "ms" + } + + val seconds = timeUsageMs / 1000 + val secondsLeft = seconds % 60 + if (secondsLeft == seconds) { + return secondsLeft.toString() + "s" + millisecondsLeft + "ms" + } + + val minutes = seconds / 60 + val minutesLeft = minutes % 60 + if (minutesLeft == minutes) { + return minutesLeft.toString() + "m" + secondsLeft + "s" + millisecondsLeft + "ms" + } + + val hours = minutes / 60 + val hoursLeft = hours % 24 + if (hoursLeft == hours) { + return hoursLeft.toString() + "h" + minutesLeft + "m" + secondsLeft + "s" + millisecondsLeft + "ms" + } + + val days = hours / 24 + return days.toString() + "d" + hoursLeft + "h" + minutesLeft + "m" + secondsLeft + "s" + millisecondsLeft + "ms" + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.java deleted file mode 100644 index a27b539..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.annotation.TargetApi; -import android.os.Build; -import android.os.StrictMode; - -public class DebugUtils { - /** - * Should only be invoked in debug version. Never invoke this method in release version! - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static void enableStrictMode() { - // thread policy - StrictMode.ThreadPolicy.Builder threadPolicyBuilder = - new StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog(); - threadPolicyBuilder.penaltyFlashScreen(); - threadPolicyBuilder.penaltyDeathOnNetwork(); - StrictMode.setThreadPolicy(threadPolicyBuilder.build()); - - // VM policy - StrictMode.VmPolicy.Builder vmPolicyBuilder = - new StrictMode.VmPolicy.Builder() - .detectAll() - .penaltyLog() - .penaltyDeath(); - StrictMode.setVmPolicy(vmPolicyBuilder.build()); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.kt new file mode 100644 index 0000000..7363a7d --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DebugUtils.kt @@ -0,0 +1,25 @@ +package me.ycdev.android.lib.common.utils + +import android.os.StrictMode + +object DebugUtils { + /** + * Should only be invoked in debug version. Never invoke this method in release version! + */ + fun enableStrictMode() { + // thread policy + val threadPolicyBuilder = StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + threadPolicyBuilder.penaltyFlashScreen() + threadPolicyBuilder.penaltyDeathOnNetwork() + StrictMode.setThreadPolicy(threadPolicyBuilder.build()) + + // VM policy + val vmPolicyBuilder = StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .penaltyDeath() + StrictMode.setVmPolicy(vmPolicyBuilder.build()) + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.java deleted file mode 100644 index 8f17bb5..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import static me.ycdev.android.lib.common.utils.EncodingUtils.encodeWithHex; - -@SuppressWarnings({"WeakerAccess", "unused"}) -public class DigestUtils { - public static String md5(final String text) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - return hash(text, "MD5"); - } - - public static String md5(byte[] data) throws NoSuchAlgorithmException { - return hash(data, "MD5"); - } - - public static String sha1(String text) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - return hash(text, "SHA-1"); - } - - public static String sha1(byte[] data) - throws NoSuchAlgorithmException { - return hash(data, "SHA-1"); - } - - public static String hash(String text, String algorithm) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - return hash(text.getBytes("UTF-8"), algorithm); - } - - public static String hash(byte[] data, String algorithm) - throws NoSuchAlgorithmException { - MessageDigest digest = MessageDigest.getInstance(algorithm); - digest.update(data); - byte messageDigest[] = digest.digest(); - return encodeWithHex(messageDigest, false); - } - - /** - * The caller should close the stream. - */ - public static String md5(final InputStream stream) - throws NoSuchAlgorithmException, IOException { - if (stream == null) { - throw new IllegalArgumentException("Invalid input stream!"); - } - byte[] buffer = new byte[1024]; - MessageDigest complete = MessageDigest.getInstance("MD5"); - int numRead; - do { - numRead = stream.read(buffer); - if (numRead > 0) { - complete.update(buffer, 0, numRead); - } - } while (numRead != -1); - byte[] digest = complete.digest(); - return encodeWithHex(digest, false); - } - - public static String md5(final File file) throws NoSuchAlgorithmException, IOException { - FileInputStream stream = null; - try { - stream = new FileInputStream(file); - return md5(stream); - } finally { - IoUtils.closeQuietly(stream); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.kt new file mode 100644 index 0000000..7530a92 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/DigestUtils.kt @@ -0,0 +1,78 @@ +package me.ycdev.android.lib.common.utils + +import me.ycdev.android.lib.common.utils.EncodingUtils.encodeWithHex +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.UnsupportedEncodingException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +@Suppress("unused") +object DigestUtils { + @Throws(NoSuchAlgorithmException::class, UnsupportedEncodingException::class) + fun md5(text: String): String { + return hash(text, "MD5") + } + + @Throws(NoSuchAlgorithmException::class) + fun md5(data: ByteArray): String { + return hash(data, "MD5") + } + + @Throws(NoSuchAlgorithmException::class, UnsupportedEncodingException::class) + fun sha1(text: String): String { + return hash(text, "SHA-1") + } + + @Throws(NoSuchAlgorithmException::class) + fun sha1(data: ByteArray): String { + return hash(data, "SHA-1") + } + + @Throws(NoSuchAlgorithmException::class, UnsupportedEncodingException::class) + fun hash(text: String, algorithm: String): String { + return hash(text.toByteArray(charset("UTF-8")), algorithm) + } + + @Throws(NoSuchAlgorithmException::class) + fun hash(data: ByteArray, algorithm: String): String { + val digest = MessageDigest.getInstance(algorithm) + digest.update(data) + val messageDigest = digest.digest() + return encodeWithHex(messageDigest, false) + } + + /** + * The caller should close the stream. + */ + @Throws(NoSuchAlgorithmException::class, IOException::class) + fun md5(stream: InputStream?): String { + if (stream == null) { + throw IllegalArgumentException("Invalid input stream!") + } + val buffer = ByteArray(1024) + val complete = MessageDigest.getInstance("MD5") + var numRead: Int + do { + numRead = stream.read(buffer) + if (numRead > 0) { + complete.update(buffer, 0, numRead) + } + } while (numRead != -1) + val digest = complete.digest() + return encodeWithHex(digest, false) + } + + @Throws(NoSuchAlgorithmException::class, IOException::class) + fun md5(file: File): String { + var stream: FileInputStream? = null + try { + stream = FileInputStream(file) + return md5(stream) + } finally { + IoUtils.closeQuietly(stream) + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.java deleted file mode 100644 index 0d6d615..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; - -@SuppressWarnings({"WeakerAccess", "unused"}) -public class EncodingUtils { - private static final char[] HEX_ARRAY_UPPERCASE = {'0', '1', '2', '3', '4', '5', '6', - '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; - private static final char[] HEX_ARRAY_LOWERCASE = {'0', '1', '2', '3', '4', '5', '6', - '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - - /** - * Encode the data with HEX (Base16) encoding and with uppercase letters. - */ - public static String encodeWithHex(byte[] bytes) { - return encodeWithHex(bytes, true); - } - - public static String encodeWithHex(byte[] bytes, boolean uppercase) { - if (bytes == null) { - return "null"; - } - return encodeWithHex(bytes, 0, bytes.length, uppercase); - } - - /** - * Encode the data with HEX (Base16) encoding and with uppercase letters. - */ - public static String encodeWithHex(@NonNull byte[] bytes, int startPos, int endPos) { - return encodeWithHex(bytes, startPos, endPos, true); - } - - public static String encodeWithHex(@NonNull byte[] bytes, int startPos, int endPos, boolean uppercase) { - if (endPos > bytes.length) { - endPos = bytes.length; - } - final int N = endPos - startPos; - final char[] HEX_ARRAY = uppercase ? HEX_ARRAY_UPPERCASE : HEX_ARRAY_LOWERCASE; - char[] hexChars = new char[N * 2]; - for (int i = startPos, j = 0; i < endPos; i++, j += 2) { - int v = bytes[i] & 0xFF; - hexChars[j] = HEX_ARRAY[v >>> 4]; - hexChars[j + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - public static byte[] fromHexString(@NonNull String hexStr) { - hexStr = hexStr.replace(" ", ""); // support spaces - if (hexStr.length() % 2 != 0) { - throw new IllegalArgumentException("Bad length: " + hexStr); - } - - byte[] result = new byte[hexStr.length() / 2]; - for (int i = 0; i < result.length; i++) { - int high = fromHexChar(hexStr, i * 2) << 4; - int low = fromHexChar(hexStr, i * 2 + 1); - result[i] = (byte) ((high | low) & 0xFF); - } - return result; - } - - private static int fromHexChar(String hexStr, int index) { - char ch = hexStr.charAt(index); - if (ch >= '0' && ch <= '9') { - return ch - '0'; - } else if (ch >= 'a' && ch <= 'f') { - return 10 + (ch - 'a'); - } else if (ch >= 'A' && ch <= 'F') { - return 10 + (ch - 'A'); - } else { - throw new IllegalArgumentException("Not hex string: " + hexStr); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.kt new file mode 100644 index 0000000..5fd6da3 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/EncodingUtils.kt @@ -0,0 +1,69 @@ +package me.ycdev.android.lib.common.utils + +object EncodingUtils { + private val HEX_ARRAY_UPPERCASE = + charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + private val HEX_ARRAY_LOWERCASE = + charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') + + /** + * Encode the data with HEX (Base16) encoding + */ + fun encodeWithHex(bytes: ByteArray?, uppercase: Boolean = true): String { + return if (bytes == null) { + "null" + } else encodeWithHex(bytes, 0, bytes.size, uppercase) + } + + /** + * Encode the data with HEX (Base16) encoding + */ + fun encodeWithHex( + bytes: ByteArray, + startPos: Int, + endPos: Int, + uppercase: Boolean = true + ): String { + var endPosTmp = endPos + if (endPosTmp > bytes.size) { + endPosTmp = bytes.size + } + val size = endPosTmp - startPos + val charsArray = if (uppercase) HEX_ARRAY_UPPERCASE else HEX_ARRAY_LOWERCASE + val hexChars = CharArray(size * 2) + var i = startPos + var j = 0 + while (i < endPosTmp) { + val v = bytes[i].toInt() and 0xFF + hexChars[j] = charsArray[v.ushr(4)] + hexChars[j + 1] = charsArray[v and 0x0F] + i++ + j += 2 + } + return String(hexChars) + } + + fun fromHexString(hexStr: String): ByteArray { + val hexStrTmp = hexStr.replace(" ", "") // support spaces + if (hexStrTmp.length % 2 != 0) { + throw IllegalArgumentException("Bad length: $hexStrTmp") + } + + val result = ByteArray(hexStrTmp.length / 2) + for (i in result.indices) { + val high = fromHexChar(hexStrTmp, i * 2) shl 4 + val low = fromHexChar(hexStrTmp, i * 2 + 1) + result[i] = (high or low and 0xFF).toByte() + } + return result + } + + private fun fromHexChar(hexStr: String, index: Int): Int { + return when (val ch = hexStr[index]) { + in '0'..'9' -> ch - '0' + in 'a'..'f' -> 10 + (ch - 'a') + in 'A'..'F' -> 10 + (ch - 'A') + else -> throw IllegalArgumentException("Not hex string: $hexStr") + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.java deleted file mode 100644 index 8ee5c08..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.java +++ /dev/null @@ -1,141 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.annotation.SuppressLint; -import android.os.Process; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import me.ycdev.android.lib.common.annotation.GuardedBy; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class FileLogger { - private static final String TAG = "FileLogger"; - - private Writer mFileWriter = null; - // Each log file every day. - private String mCurrentDay; - - private SimpleDateFormat mDayFormat = new SimpleDateFormat("yyMMdd", Locale.US); - private SimpleDateFormat mTimeFormat = new SimpleDateFormat("MM-dd HH:mm:ss:SSS", Locale.US); - - private String mLogDir; - private String mLogFileNamePrefix; - private String mProcessNameSuffix; - - public FileLogger(String logDir, String logFileNamePrefix) { - this(logDir, logFileNamePrefix, null); - } - - public FileLogger(@NonNull String logDir, @NonNull String logFileNamePrefix, - @Nullable String processNameSuffix) { - mLogDir = logDir; - mLogFileNamePrefix = logFileNamePrefix; - mProcessNameSuffix = processNameSuffix; - } - - @GuardedBy("this") - public synchronized void close() { - IoUtils.closeQuietly(mFileWriter); - mFileWriter = null; - } - - @GuardedBy("this") - public void logToFile(String tag, String msg, Throwable tr) { - StringBuilder builder = new StringBuilder(); - builder.append(mTimeFormat.format(new Date())); - builder.append(" "); - builder.append(tag); - builder.append("\t"); - builder.append(Process.myPid()).append(" ").append(Process.myTid()).append(" "); - if (!TextUtils.isEmpty(msg)) { - builder.append(msg); - } - if (tr != null) { - builder.append("\n\t"); - builder.append(Log.getStackTraceString(tr)); - } - builder.append("\n"); - - writeLog(builder.toString()); - } - - @GuardedBy("this") - private synchronized void writeLog(String logLine) { - if (null == mFileWriter) { - if (!openFile()) { - return; - } - } - - try { - String day = getCurrentDay(); - // If is another day, then create a new log file. - if (!day.equals(mCurrentDay)) { - mFileWriter.flush(); - mFileWriter.close(); - mFileWriter = null; - - boolean success = openFile(); - if (!success) { - return; - } - } - - mFileWriter.write(logLine); - mFileWriter.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @SuppressLint("LogNotTimber") - @GuardedBy("this") - private boolean openFile() { - if (mLogDir == null) { - return false; - } - - File logDirFile = new File(mLogDir); - if (!logDirFile.exists()) { - if (!logDirFile.mkdirs()) { - Log.w(TAG, "Cannot create dir: " + mLogDir); - return false; - } - } - - mCurrentDay = getCurrentDay(); - try { - File logFile = new File(mLogDir, composeFileName(mCurrentDay)); - mFileWriter = new FileWriter(logFile, true); - return true; - } catch (IOException e) { - e.printStackTrace(); - } - return false; - } - - private String composeFileName(String currentDay) { - StringBuilder sb = new StringBuilder(); - sb.append(mLogFileNamePrefix).append("_log_").append(currentDay); - if (!TextUtils.isEmpty(mProcessNameSuffix)) { - sb.append("_").append(mProcessNameSuffix); - } - sb.append(".txt"); - return sb.toString(); - } - - private String getCurrentDay() { - return mDayFormat.format(new Date()); - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.kt new file mode 100644 index 0000000..186abe9 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/FileLogger.kt @@ -0,0 +1,129 @@ +package me.ycdev.android.lib.common.utils + +import android.annotation.SuppressLint +import android.os.Process +import android.text.TextUtils +import android.util.Log +import androidx.annotation.GuardedBy + +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.Writer +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class FileLogger constructor( + private val logDir: String?, + private val logFileNamePrefix: String, + private val processNameSuffix: String? = null +) { + + private var fileWriter: Writer? = null + // Each log file every day. + private var currentDay: String? = null + + private val dayFormat = SimpleDateFormat("yyMMdd", Locale.US) + private val timeFormat = SimpleDateFormat("MM-dd HH:mm:ss:SSS", Locale.US) + + private fun getCurrentDay(): String = dayFormat.format(Date()) + + @GuardedBy("this") + @Synchronized + fun close() { + IoUtils.closeQuietly(fileWriter) + fileWriter = null + } + + @GuardedBy("this") + fun logToFile(tag: String, msg: String?, tr: Throwable?) { + val builder = StringBuilder() + builder.append(timeFormat.format(Date())) + builder.append(" ") + builder.append(tag) + builder.append("\t") + builder.append(Process.myPid()).append(" ").append(Process.myTid()).append(" ") + if (!TextUtils.isEmpty(msg)) { + builder.append(msg) + } + if (tr != null) { + builder.append("\n\t") + builder.append(Log.getStackTraceString(tr)) + } + builder.append("\n") + + writeLog(builder.toString()) + } + + @GuardedBy("this") + @Synchronized + private fun writeLog(logLine: String) { + if (null == fileWriter) { + if (!openFile()) { + return + } + } + + try { + val day = getCurrentDay() + // If is another day, then create a new log file. + if (day != currentDay) { + fileWriter!!.flush() + fileWriter!!.close() + fileWriter = null + + val success = openFile() + if (!success) { + return + } + } + + fileWriter!!.write(logLine) + fileWriter!!.flush() + } catch (e: IOException) { + e.printStackTrace() + } + } + + @SuppressLint("LogNotTimber") + @GuardedBy("this") + private fun openFile(): Boolean { + if (logDir == null) { + return false + } + + val logDirFile = File(logDir) + if (!logDirFile.exists()) { + if (!logDirFile.mkdirs()) { + Log.w(TAG, "Cannot create dir: $logDir") + return false + } + } + + currentDay = getCurrentDay() + try { + val logFile = File(logDir, composeFileName(currentDay)) + fileWriter = FileWriter(logFile, true) + return true + } catch (e: IOException) { + e.printStackTrace() + } + + return false + } + + private fun composeFileName(currentDay: String?): String { + val sb = StringBuilder() + sb.append(logFileNamePrefix).append("_log_").append(currentDay) + if (!TextUtils.isEmpty(processNameSuffix)) { + sb.append("_").append(processNameSuffix) + } + sb.append(".txt") + return sb.toString() + } + + companion object { + private const val TAG = "FileLogger" + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.java deleted file mode 100644 index 39e6cc4..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import me.ycdev.android.lib.common.type.BooleanHolder; -import timber.log.Timber; - -public class GcHelper { - private static final String TAG = "GcHelper"; - - public static void forceGc(BooleanHolder gcState) { - // Now, 'objPartner' can be collected by GC! - final long timeStart = System.currentTimeMillis(); - - // create a lot of objects to force GC - final int MEM_ALLOC_SIZE = 1024 * 1024; // 1MB - long memAllocCount = 0; - while (true) { - System.gc(); - ThreadUtils.sleep(100); // wait for GC - if (gcState.value) { - break; // GC happened - } - Timber.tag(TAG).d("Allocating mem..."); - @SuppressWarnings("unused") - byte[] gcObj = new byte[MEM_ALLOC_SIZE]; - memAllocCount++; - } - - long timeUsed = System.currentTimeMillis() - timeStart; - Timber.tag(TAG).d("Force GC, time used: %d, memAlloc: %dMB", timeUsed, memAllocCount); - } - - public static void forceGc() { - BooleanHolder gcState = new BooleanHolder(false); - // Must use another method to create the GC object. Don't know why! - createGcObject(gcState); - forceGc(gcState); - } - - private static void createGcObject(BooleanHolder gcState) { - @SuppressWarnings("unused") - Object objPartner = new Object() { - @Override - protected void finalize() throws Throwable { - Timber.tag(TAG).d("GC Partner object was collected"); - gcState.value = true; - } - }; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.kt new file mode 100644 index 0000000..30ab53a --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GcHelper.kt @@ -0,0 +1,48 @@ +package me.ycdev.android.lib.common.utils + +import me.ycdev.android.lib.common.type.BooleanHolder +import timber.log.Timber + +@Suppress("unused") +object GcHelper { + private const val TAG = "GcHelper" + + fun forceGc(gcState: BooleanHolder) { + // Now, 'objPartner' can be collected by GC! + val timeStart = System.currentTimeMillis() + + // create a lot of objects to force GC + val memAllocSize = 1024 * 1024 // 1MB + var memAllocCount: Long = 0 + while (true) { + System.gc() + ThreadUtils.sleep(100) // wait for GC + if (gcState.value) { + break // GC happened + } + Timber.tag(TAG).d("Allocating mem...") + ByteArray(memAllocSize) + memAllocCount++ + } + + val timeUsed = System.currentTimeMillis() - timeStart + Timber.tag(TAG).d("Force GC, time used: %d, memAlloc: %dMB", timeUsed, memAllocCount) + } + + fun forceGc() { + val gcState = BooleanHolder(false) + // Must use another method to create the GC object. Don't know why! + createGcObject(gcState) + forceGc(gcState) + } + + private fun createGcObject(gcState: BooleanHolder) { + object : Any() { + @Throws(Throwable::class) + protected fun finalize() { + Timber.tag(TAG).d("GC Partner object was collected") + gcState.value = true + } + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.java deleted file mode 100644 index 436b3ed..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; - -import com.google.gson.JsonObject; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class GsonHelper { - public static String optString(@NonNull JsonObject json, @NonNull String key, String defValue) { - if (json.has(key)) { - return json.get(key).getAsString(); - } - return defValue; - } - - public static boolean optBoolean(@NonNull JsonObject json, @NonNull String key, boolean defValue) { - if (json.has(key)) { - return json.get(key).getAsBoolean(); - } - return defValue; - } - - public static int optInt(@NonNull JsonObject json, @NonNull String key, int defValue) { - if (json.has(key)) { - return json.get(key).getAsInt(); - } - return defValue; - } - - public static long optLong(@NonNull JsonObject json, @NonNull String key, long defValue) { - if (json.has(key)) { - return json.get(key).getAsLong(); - } - return defValue; - } - - public static float optFloat(@NonNull JsonObject json, @NonNull String key, float defValue) { - if (json.has(key)) { - return json.get(key).getAsFloat(); - } - return defValue; - } - - public static double optDouble(@NonNull JsonObject json, @NonNull String key, double defValue) { - if (json.has(key)) { - return json.get(key).getAsDouble(); - } - return defValue; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.kt new file mode 100644 index 0000000..439b3a5 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/GsonHelper.kt @@ -0,0 +1,41 @@ +package me.ycdev.android.lib.common.utils + +import com.google.gson.JsonObject + +object GsonHelper { + fun optString(json: JsonObject, key: String, defValue: String?): String? { + return if (json.has(key)) { + json.get(key).asString + } else defValue + } + + fun optBoolean(json: JsonObject, key: String, defValue: Boolean): Boolean { + return if (json.has(key)) { + json.get(key).asBoolean + } else defValue + } + + fun optInt(json: JsonObject, key: String, defValue: Int): Int { + return if (json.has(key)) { + json.get(key).asInt + } else defValue + } + + fun optLong(json: JsonObject, key: String, defValue: Long): Long { + return if (json.has(key)) { + json.get(key).asLong + } else defValue + } + + fun optFloat(json: JsonObject, key: String, defValue: Float): Float { + return if (json.has(key)) { + json.get(key).asFloat + } else defValue + } + + fun optDouble(json: JsonObject, key: String, defValue: Double): Double { + return if (json.has(key)) { + json.get(key).asDouble + } else defValue + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ImageUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ImageUtils.kt similarity index 58% rename from baseLib/src/main/java/me/ycdev/android/lib/common/utils/ImageUtils.java rename to baseLib/src/main/java/me/ycdev/android/lib/common/utils/ImageUtils.kt index fdbc6d9..590ddb7 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ImageUtils.java +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ImageUtils.kt @@ -1,18 +1,16 @@ -package me.ycdev.android.lib.common.utils; +package me.ycdev.android.lib.common.utils -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.annotation.DrawableRes -import java.io.FileDescriptor; +import java.io.FileDescriptor -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ImageUtils { - public interface IReusableBitmapProvider { - Bitmap getReusableBitmap(@NonNull BitmapFactory.Options options); +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ImageUtils { + interface IReusableBitmapProvider { + fun getReusableBitmap(options: BitmapFactory.Options): Bitmap? } /** @@ -23,29 +21,33 @@ public interface IReusableBitmapProvider { * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @param provider The IReusableBitmapProvider used to find candidate bitmaps for use with inBitmap. - * Can be null if no bitmap reuse needed. + * Can be null if no bitmap reuse needed. * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height + * that are equal to or greater than the requested width and height */ - @Nullable - public static Bitmap decodeSampledBitmapFromResource(@NonNull Resources res, @DrawableRes int resId, - int reqWidth, int reqHeight, @Nullable IReusableBitmapProvider provider) { + fun decodeSampledBitmapFromResource( + res: Resources, + @DrawableRes resId: Int, + reqWidth: Int, + reqHeight: Int, + provider: IReusableBitmapProvider? + ): Bitmap? { // Based on https://github.com/yongce/BitmapFun/blob/master/src/com/example/android/bitmapfun/util/ImageResizer.java // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeResource(res, resId, options); + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeResource(res, resId, options) // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) // Try to use inBitmap - addInBitmapOptionsIfPossible(options, provider); + addInBitmapOptionsIfPossible(options, provider) // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - return BitmapFactory.decodeResource(res, resId, options); + options.inJustDecodeBounds = false + return BitmapFactory.decodeResource(res, resId, options) } /** @@ -55,29 +57,32 @@ public static Bitmap decodeSampledBitmapFromResource(@NonNull Resources res, @Dr * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @param provider The IReusableBitmapProvider used to find candidate bitmaps for use with inBitmap. - * Can be null if no bitmap reuse needed. + * Can be null if no bitmap reuse needed. * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height + * that are equal to or greater than the requested width and height */ - @Nullable - public static Bitmap decodeSampledBitmapFromFile(@NonNull String filename, - int reqWidth, int reqHeight, @Nullable IReusableBitmapProvider provider) { + fun decodeSampledBitmapFromFile( + filename: String, + reqWidth: Int, + reqHeight: Int, + provider: IReusableBitmapProvider? + ): Bitmap? { // Based on https://github.com/yongce/BitmapFun/blob/master/src/com/example/android/bitmapfun/util/ImageResizer.java // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(filename, options); + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(filename, options) // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) // Try to use inBitmap - addInBitmapOptionsIfPossible(options, provider); + addInBitmapOptionsIfPossible(options, provider) // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; - return BitmapFactory.decodeFile(filename, options); + options.inJustDecodeBounds = false + return BitmapFactory.decodeFile(filename, options) } /** @@ -87,83 +92,90 @@ public static Bitmap decodeSampledBitmapFromFile(@NonNull String filename, * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @param provider The IReusableBitmapProvider used to find candidate bitmaps for use with inBitmap. - * Can be null if no bitmap reuse needed. + * Can be null if no bitmap reuse needed. * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height + * that are equal to or greater than the requested width and height */ - @Nullable - public static Bitmap decodeSampledBitmapFromDescriptor( - @NonNull FileDescriptor fileDescriptor, int reqWidth, int reqHeight, - @Nullable IReusableBitmapProvider provider) { + fun decodeSampledBitmapFromDescriptor( + fileDescriptor: FileDescriptor, + reqWidth: Int, + reqHeight: Int, + provider: IReusableBitmapProvider? + ): Bitmap? { // Based on https://github.com/yongce/BitmapFun/blob/master/src/com/example/android/bitmapfun/util/ImageResizer.java // First decode with inJustDecodeBounds=true to check dimensions - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options) // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false; + options.inJustDecodeBounds = false // Try to use inBitmap - addInBitmapOptionsIfPossible(options, provider); + addInBitmapOptionsIfPossible(options, provider) - return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options) } - private static void addInBitmapOptionsIfPossible(@NonNull BitmapFactory.Options options, - @Nullable IReusableBitmapProvider provider) { + private fun addInBitmapOptionsIfPossible( + options: BitmapFactory.Options, + provider: IReusableBitmapProvider? + ) { // Based on https://github.com/yongce/BitmapFun/blob/master/src/com/example/android/bitmapfun/util/ImageResizer.java if (provider == null) { - return; + return } // inBitmap only works with mutable bitmaps so force the decoder to // return mutable bitmaps. - options.inMutable = true; + options.inMutable = true // Try and find a bitmap to use for inBitmap - Bitmap inBitmap = provider.getReusableBitmap(options); + val inBitmap = provider.getReusableBitmap(options) if (inBitmap != null) { - options.inBitmap = inBitmap; + options.inBitmap = inBitmap } } /** - * Calculate an inSampleSize for use in a {@link android.graphics.BitmapFactory.Options} object when decoding - * bitmaps using the decode* methods from {@link android.graphics.BitmapFactory}. This implementation calculates + * Calculate an inSampleSize for use in a [android.graphics.BitmapFactory.Options] object when decoding + * bitmaps using the decode* methods from [android.graphics.BitmapFactory]. This implementation calculates * the closest inSampleSize that will result in the final decoded bitmap having a width and * height equal to or larger than the requested width and height. This implementation does not * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but * results in a larger bitmap which isn't as useful for caching purposes. * * @param options An options object with out* params already populated (run through a decode* - * method with #inJustDecodeBounds==true) + * method with #inJustDecodeBounds==true) * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @return The value to be used for inSampleSize */ - public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, - int reqWidth, int reqHeight) { + fun calculateInSampleSize( + options: BitmapFactory.Options, + reqWidth: Int, + reqHeight: Int + ): Int { // Based on https://github.com/yongce/BitmapFun/blob/master/src/com/example/android/bitmapfun/util/ImageResizer.java // Raw height and width of image - final int height = options.outHeight; - final int width = options.outWidth; - int inSampleSize = 1; + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { // Calculate ratios of height and width to requested height and width - final int heightRatio = Math.round((float) height / (float) reqHeight); - final int widthRatio = Math.round((float) width / (float) reqWidth); + val heightRatio = Math.round(height.toFloat() / reqHeight.toFloat()) + val widthRatio = Math.round(width.toFloat() / reqWidth.toFloat()) // Choose the smaller ratio as inSampleSize value, this will guarantee a final image // with both dimensions larger than or equal to the requested height and width. - inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio /* * @policy Please pay attention to the following policy. @@ -175,26 +187,24 @@ public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, // end up being too large to fit comfortably in memory, so we should // be more aggressive with sample down the image (=larger inSampleSize). - final float totalPixels = width * height; + val totalPixels = (width * height).toFloat() // Anything more than 2x the requested pixels we'll sample down further - final float totalReqPixelsCap = reqWidth * reqHeight * 2; + val totalReqPixelsCap = (reqWidth * reqHeight * 2).toFloat() while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { - inSampleSize++; + inSampleSize++ } } - return inSampleSize; + return inSampleSize } - /** * Get the size in bytes of a bitmap. * @param bitmap The bitmap to calculate. * @return size in bytes */ - public static int getBitmapSize(@NonNull Bitmap bitmap) { - return bitmap.getByteCount(); + fun getBitmapSize(bitmap: Bitmap): Int { + return bitmap.byteCount } - } diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.java deleted file mode 100644 index 5f3de79..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import androidx.annotation.NonNull; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class IntentUtils { - public static boolean canStartActivity(@NonNull Context cxt, @NonNull Intent activityIntent) { - // Use PackageManager.MATCH_DEFAULT_ONLY to behavior same as Context#startAcitivty() - ResolveInfo resolveInfo = cxt.getPackageManager().resolveActivity(activityIntent, - PackageManager.MATCH_DEFAULT_ONLY); - return resolveInfo != null; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt new file mode 100644 index 0000000..5d792f8 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt @@ -0,0 +1,16 @@ +package me.ycdev.android.lib.common.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager + +object IntentUtils { + fun canStartActivity(cxt: Context, activityIntent: Intent): Boolean { + // Use PackageManager.MATCH_DEFAULT_ONLY to behavior same as Context#startAcitivty() + val resolveInfo = cxt.packageManager.resolveActivity( + activityIntent, + PackageManager.MATCH_DEFAULT_ONLY + ) + return resolveInfo != null + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.java deleted file mode 100644 index 0d169f9..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.java +++ /dev/null @@ -1,191 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -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.util.zip.ZipFile; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class IoUtils { - private static final int IO_BUF_SIZE = 1024 * 16; // 16KB - - private IoUtils() { - } - - /** - * Close the closeable target and eat possible exceptions. - * @param target The target to close. Can be null. - */ - public static void closeQuietly(@Nullable Closeable target) { - try { - if (target != null) { - target.close(); - } - } catch (Exception e) { - // ignore - } - } - - /** - * Before Android 4.4, ZipFile doesn't implement the interface "java.io.Closeable". - * @param target The target to close. Can be null. - */ - public static void closeQuietly(@Nullable ZipFile target) { - try { - if (target != null) target.close(); - } catch (IOException e) { - // ignore - } - } - - public static byte[] readAllBytes(@NonNull InputStream is) throws IOException { - ByteArrayOutputStream bytesBuf = new ByteArrayOutputStream(1024); - int bytesReaded; - byte[] buf = new byte[1024]; - while ((bytesReaded = is.read(buf, 0, buf.length)) != -1) { - bytesBuf.write(buf, 0, bytesReaded); - } - return bytesBuf.toByteArray(); - } - - /** - * Read all lines of the stream as a String. - * Use the "UTF-8" character converter when reading. - * @return May be empty String, but never null. - */ - @NonNull - public static String readAllLines(@NonNull InputStream is) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); - StringBuilder sb = new StringBuilder(); - String line; - boolean first = true; - - while ((line = reader.readLine()) != null) { - if (!first) { - sb.append('\n'); - } else { - first = false; - } - sb.append(line); - } - - return sb.toString(); - } - - /** - * Read all lines of the text file as a String. - * @param filePath The file to read - */ - @NonNull - public static String readAllLines(@NonNull String filePath) throws IOException { - FileInputStream fis = new FileInputStream(filePath); - try { - return readAllLines(fis); - } finally { - closeQuietly(fis); - } - } - - /** - * @param lineNumber Start from 1 - */ - @Nullable - public static String readOneLine(@NonNull InputStream is, int lineNumber) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); - String line = null; - - for (int i = 0; i < lineNumber; i++) { - line = reader.readLine(); - if (line == null) { - break; - } - } - - return line; - } - - /** - * @param lineNumber Start from 1 - */ - @Nullable - public static String readOneLine(@NonNull String filePath, int lineNumber) throws IOException { - FileInputStream fis = new FileInputStream(filePath); - try { - return readOneLine(fis, lineNumber); - } finally { - closeQuietly(fis); - } - } - - public static void createParentDirsIfNeeded(@NonNull File file) { - File dirFile = file.getParentFile(); - if (dirFile != null && !dirFile.exists()) { - //noinspection ResultOfMethodCallIgnored - dirFile.mkdirs(); - } - } - - public static void createParentDirsIfNeeded(@NonNull String filePath) { - createParentDirsIfNeeded(new File(filePath)); - } - - public static void saveAsFile(@NonNull String content, @NonNull String filePath) - throws IOException { - FileWriter fw = new FileWriter(filePath); - try { - fw.write(content); - fw.flush(); - } finally { - closeQuietly(fw); - } - } - - /** - * Save the input stream into a file.
    - * Note: This method will not close the input stream. - */ - public static void saveAsFile(@NonNull InputStream is, @NonNull String filePath) - throws IOException { - FileOutputStream fos = new FileOutputStream(filePath); - try { - copyStream(is, fos); - } finally { - closeQuietly(fos); - } - } - - /** - * Copy data from the input stream to the output stream.
    - * Note: This method will not close the input stream and output stream. - */ - public static void copyStream(@NonNull InputStream is, @NonNull OutputStream os) - throws IOException { - byte[] buffer = new byte[IO_BUF_SIZE]; - int len; - while ((len = is.read(buffer)) != -1) { - os.write(buffer, 0, len); - } - os.flush(); - } - - public static void copyFile(@NonNull String srcFilePath, @NonNull String destFilePath) - throws IOException { - FileInputStream fis = new FileInputStream(srcFilePath); - try { - saveAsFile(fis, destFilePath); - } finally { - closeQuietly(fis); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.kt new file mode 100644 index 0000000..9969906 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IoUtils.kt @@ -0,0 +1,156 @@ +package me.ycdev.android.lib.common.utils + +import java.io.Closeable +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.zip.ZipFile + +@Suppress("unused") +object IoUtils { + /** + * Close the closeable target and eat possible exceptions. + * @param target The target to close. Can be null. + */ + fun closeQuietly(target: Closeable?) { + try { + target?.close() + } catch (e: Exception) { + // ignore + } + } + + /** + * Before Android 4.4, ZipFile doesn't implement the interface "java.io.Closeable". + * @param target The target to close. Can be null. + */ + fun closeQuietly(target: ZipFile?) { + try { + target?.close() + } catch (e: IOException) { + // ignore + } + } + + @Deprecated("Not needed anymore", ReplaceWith("Use InputStream#readBytes()")) + @Throws(IOException::class) + fun readAllBytes(input: InputStream): ByteArray { + return input.readBytes() + } + + /** + * Read all lines of the stream as a String. + * Use the "UTF-8" character converter when reading. + * @return May be empty String, but never null. + */ + @Throws(IOException::class) + fun readAllLines(input: InputStream): String { + return input.bufferedReader().use { it.readText() } + } + + /** + * Read all lines of the text file as a String. + * @param filePath The file to read + */ + @Throws(IOException::class) + fun readAllLines(filePath: String): String { + val fis = FileInputStream(filePath) + try { + return fis.bufferedReader().readText() + } finally { + closeQuietly(fis) + } + } + + /** + * @param lineNumber Start from 1 + */ + @Throws(IOException::class) + fun readOneLine(input: InputStream, lineNumber: Int): String? { + val reader = input.bufferedReader() + var line: String? = null + + for (i in 0 until lineNumber) { + line = reader.readLine() + if (line == null) { + break + } + } + + return line + } + + /** + * @param lineNumber Start from 1 + */ + @Throws(IOException::class) + fun readOneLine(filePath: String, lineNumber: Int): String? { + val fis = FileInputStream(filePath) + try { + return readOneLine(fis, lineNumber) + } finally { + closeQuietly(fis) + } + } + + @Suppress("MemberVisibilityCanBePrivate") + fun createParentDirsIfNeeded(file: File) { + val dirFile = file.parentFile + if (dirFile != null && !dirFile.exists()) { + dirFile.mkdirs() + } + } + + fun createParentDirsIfNeeded(filePath: String) { + createParentDirsIfNeeded(File(filePath)) + } + + @Throws(IOException::class) + fun saveAsFile(content: String, filePath: String) { + val fw = FileWriter(filePath) + try { + fw.write(content) + fw.flush() + } finally { + closeQuietly(fw) + } + } + + /** + * Save the input stream into a file. + * Note: This method will not close the input stream. + */ + @Throws(IOException::class) + fun saveAsFile(input: InputStream, filePath: String) { + val fos = FileOutputStream(filePath) + try { + input.copyTo(fos) + } finally { + closeQuietly(fos) + } + } + + /** + * Copy data from the input stream to the output stream. + * Note: This method will not close the input stream and output stream. + */ + @Deprecated("Not needed anymore", ReplaceWith(("Use InputStream#copyTo()"))) + @Throws(IOException::class) + fun copyStream(input: InputStream, os: OutputStream) { + input.copyTo(os) + } + + @Throws(IOException::class) + fun copyFile(srcFilePath: String, destFilePath: String) { + val fis = FileInputStream(srcFilePath) + try { + saveAsFile(fis, destFilePath) + } finally { + closeQuietly(fis) + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibConfigs.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibConfigs.java deleted file mode 100644 index 16421c1..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibConfigs.java +++ /dev/null @@ -1,8 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.RestrictTo; - -@RestrictTo(RestrictTo.Scope.LIBRARY) -public class LibConfigs { - public static final boolean DEBUG_LOG = false; -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.java deleted file mode 100644 index 2142494..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.java +++ /dev/null @@ -1,155 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.annotation.SuppressLint; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import android.util.Log; - -import java.util.Locale; - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class LibLogger { - private static final String TAG = "AndroidLib"; - private static boolean sJvmLogger = false; - - @RestrictTo(RestrictTo.Scope.SUBCLASSES) - protected LibLogger() { - // nothing to do - } - - public static void enableJvmLogger() { - sJvmLogger = true; - } - - public static void setFileLogger(FileLogger fileLogger) { - if (!sJvmLogger) { - AndroidLogger.setFileLogger(fileLogger); - } - } - - /** - * Log enabled by default - */ - public static void setLogEnabled(boolean enabled) { - if (!sJvmLogger) { - AndroidLogger.setLogEnabled(enabled); - } - } - - public static boolean isLogEnabled() { - return AndroidLogger.isLogEnabled(); - } - - public static void v(@NonNull String tag, @NonNull String msg, Object... args) { - log(Log.VERBOSE, tag, msg, null, args); - } - - public static void d(@NonNull String tag, @NonNull String msg, Object... args) { - log(Log.DEBUG, tag, msg, null, args); - } - - public static void i(@NonNull String tag, @NonNull String msg, Object... args) { - log(Log.INFO, tag, msg, null, args); - } - - public static void w(@NonNull String tag, @NonNull String msg, Object... args) { - log(Log.WARN, tag, msg, null, args); - } - - public static void w(@NonNull String tag, @NonNull String msg, @NonNull Throwable e, - Object... args) { - log(Log.WARN, tag, msg, e, args); - } - - public static void w(@NonNull String tag, @NonNull Throwable e, Object... args) { - log(Log.WARN, tag, null, e, args); - } - - public static void e(@NonNull String tag, @NonNull String msg, Object... args) { - log(Log.ERROR, tag, msg, null, args); - } - - public static void e(@NonNull String tag, @NonNull String msg, @NonNull Throwable e, - Object... args) { - log(Log.ERROR, tag, msg, e, args); - } - - public static void log(int level, @NonNull String tag, @Nullable String msg, - @Nullable Throwable tr, Object... args) { - if (sJvmLogger) { - if (msg != null && args != null && args.length > 0) { - msg = String.format(Locale.US, msg, args); - } - System.out.println("[" + tag + "] " + msg); - if (tr != null) { - tr.printStackTrace(); - } - } else { - AndroidLogger.log(level, tag, msg, tr, args); - } - } - - private static class AndroidLogger { - private static boolean sLogEnabled = true; - private static FileLogger sFileLogger; - - static void setFileLogger(FileLogger fileLogger) { - sFileLogger = fileLogger; - } - - /** - * Log enabled by default - */ - static void setLogEnabled(boolean enabled) { - sLogEnabled = enabled; - if (!enabled && sFileLogger != null) { - sFileLogger.close(); - } - } - - static boolean isLogEnabled() { - return sLogEnabled; - } - - static void log(int level, @NonNull String tag, @Nullable String msg, - @Nullable Throwable tr, Object... args) { - if (showLog(level, tag)) { - if (msg != null && args != null && args.length > 0) { - msg = String.format(Locale.US, msg, args); - } - if (tr == null) { - Log.println(level, tag, msg); - } else { - Log.println(level, tag, msg + '\n' + Log.getStackTraceString(tr)); - } - logToFile(tag, msg, tr); - } - } - - private static boolean showLog(int level, String tag) { - return isLoggable(tag, level) || sLogEnabled; - } - - @SuppressLint("LogNotTimber") - private static boolean isLoggable(String tag, int level) { - try { - return Log.isLoggable(tag, level); - } catch (Exception e) { - if (sLogEnabled) { - throw e; - } else { - Log.e(TAG, "please check the tag length?", e); - } - } - return false; - } - - private static void logToFile(String tag, String msg, Throwable tr) { - if (sLogEnabled && sFileLogger != null) { - sFileLogger.logToFile(tag, msg, tr); - } - } - - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.kt new file mode 100644 index 0000000..178d1ca --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/LibLogger.kt @@ -0,0 +1,148 @@ +package me.ycdev.android.lib.common.utils + +import android.annotation.SuppressLint +import android.util.Log +import androidx.annotation.RestrictTo +import java.util.Locale + +@Suppress("unused") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +object LibLogger { + private const val TAG = "AndroidLib" + private var jvmLogger = false + + /** + * Log enabled by default + */ + var isLogEnabled: Boolean + get() = AndroidLogger.isLogEnabled + set(enabled) { + if (!jvmLogger) { + AndroidLogger.isLogEnabled = enabled + } + } + + fun enableJvmLogger() { + jvmLogger = true + } + + fun setFileLogger(fileLogger: FileLogger) { + if (!jvmLogger) { + AndroidLogger.setFileLogger(fileLogger) + } + } + + fun v(tag: String, msg: String, vararg args: Any?) { + log(Log.VERBOSE, tag, null, msg, *args) + } + + fun d(tag: String, msg: String, vararg args: Any?) { + log(Log.DEBUG, tag, null, msg, *args) + } + + fun d(tag: String, e: Throwable, msg: String, vararg args: Any?) { + log(Log.DEBUG, tag, e, msg, *args) + } + + fun i(tag: String, msg: String, vararg args: Any?) { + log(Log.INFO, tag, null, msg, *args) + } + + fun i(tag: String, e: Throwable, msg: String, vararg args: Any?) { + log(Log.INFO, tag, e, msg, *args) + } + + fun w(tag: String, msg: String, vararg args: Any?) { + log(Log.WARN, tag, null, msg, *args) + } + + fun w(tag: String, e: Throwable, msg: String, vararg args: Any?) { + log(Log.WARN, tag, e, msg, *args) + } + + fun w(tag: String, e: Throwable) { + log(Log.WARN, tag, e, null) + } + + fun e(tag: String, msg: String, vararg args: Any?) { + log(Log.ERROR, tag, null, msg, *args) + } + + fun e(tag: String, e: Throwable, msg: String, vararg args: Any?) { + log(Log.ERROR, tag, e, msg, *args) + } + + fun e(tag: String, e: Throwable) { + log(Log.ERROR, tag, e, null) + } + + fun log(level: Int, tag: String, tr: Throwable?, msg: String?, vararg args: Any?) { + var msgFull = msg + if (jvmLogger) { + if (msgFull != null && args.isNotEmpty()) { + msgFull = String.format(Locale.US, msgFull, *args) + } + println("[$tag] $msgFull") + tr?.printStackTrace() + } else { + AndroidLogger.log(level, tag, tr, msgFull, *args) + } + } + + private object AndroidLogger { + /** + * Log enabled by default + */ + internal var isLogEnabled = true + set(enabled) { + field = enabled + if (!enabled && fileLogger != null) { + fileLogger!!.close() + } + } + private var fileLogger: FileLogger? = null + + fun setFileLogger(fileLogger: FileLogger) { + this.fileLogger = fileLogger + } + + fun log(level: Int, tag: String, tr: Throwable?, msg: String?, vararg args: Any?) { + var msgFull = msg + if (showLog(level, tag)) { + if (msgFull != null && args.isNotEmpty()) { + msgFull = String.format(Locale.US, msgFull, *args) + } + if (tr == null) { + Log.println(level, tag, msgFull) + } else { + Log.println(level, tag, msgFull + "\n" + Log.getStackTraceString(tr)) + } + logToFile(tag, msgFull, tr) + } + } + + private fun showLog(level: Int, tag: String): Boolean { + return isLoggable(tag, level) || isLogEnabled + } + + @SuppressLint("LogNotTimber") + private fun isLoggable(tag: String, level: Int): Boolean { + try { + return Log.isLoggable(tag, level) + } catch (e: Exception) { + if (isLogEnabled) { + throw e + } else { + Log.e(TAG, "please check the tag length?", e) + } + } + return false + } + + private fun logToFile(tag: String, msg: String?, tr: Throwable?) { + if (isLogEnabled && fileLogger != null) { + fileLogger!!.logToFile(tag, msg, tr) + } + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.java deleted file mode 100644 index 0ca8506..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.os.Handler; -import android.os.Looper; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class MainHandler { - private static Handler sHandler = new Handler(Looper.getMainLooper()); - - public static Handler getMainHandler() { - return sHandler; - } - - public static void post(Runnable r) { - sHandler.post(r); - } - - public static void postDelayed(Runnable r, long delayMs) { - sHandler.postDelayed(r, delayMs); - } - - public static void remove(Runnable r) { - sHandler.removeCallbacks(r); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.kt new file mode 100644 index 0000000..c59a7ef --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MainHandler.kt @@ -0,0 +1,7 @@ +package me.ycdev.android.lib.common.utils + +import android.os.Handler +import android.os.Looper + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object MainHandler : Handler(Looper.getMainLooper()) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.java deleted file mode 100644 index cbb1ea1..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -public class MiscUtils { - public static int calcProgressPercent(int percentStart, int percentEnd, int i, int n) { - return percentStart + i * (percentEnd - percentStart) / n; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.kt new file mode 100644 index 0000000..84f8dd6 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/MiscUtils.kt @@ -0,0 +1,7 @@ +package me.ycdev.android.lib.common.utils + +object MiscUtils { + fun calcProgressPercent(percentStart: Int, percentEnd: Int, i: Int, n: Int): Int { + return percentStart + i * (percentEnd - percentStart) / n + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.java deleted file mode 100644 index 04a2e1f..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.java +++ /dev/null @@ -1,176 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.os.Build; -import androidx.annotation.NonNull; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -@SuppressWarnings("unused") -public class PackageUtils { - private static final String TAG = "PackageUtils"; - private static final boolean DEBUG = LibConfigs.DEBUG_LOG; - - /** - * Value for {@link android.content.pm.ApplicationInfo#flags}: set to {@code true} if the application - * is permitted to hold privileged permissions. - */ - private static final int FLAG_PRIVILEGED = 1<<30; - - public static boolean isPkgEnabled(@NonNull Context cxt, @NonNull String pkgName) { - try { - int state = cxt.getPackageManager().getApplicationEnabledSetting(pkgName); - return (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || - state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED); - } catch (IllegalArgumentException e) { - // the app had been uninstalled already - } - return true; // by default - } - - public static boolean isPkgEnabled(@NonNull ApplicationInfo appInfo) { - return appInfo.enabled; - } - - public static boolean isPkgSystem(@NonNull ApplicationInfo appInfo) { - return (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - } - - /** - * Check if an app is residing in "/system" (Android 4.3 and old versions) - * or "/system/priv-app" (Android 4.4 and new versions) and has "signatureOrSystem" permission. - */ - public static boolean isPkgPrivileged(@NonNull ApplicationInfo appInfo) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return (appInfo.flags & FLAG_PRIVILEGED) != 0; - } else { - return (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - } - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) - public static boolean isPkgStopped(@NonNull ApplicationInfo appInfo) { - return (appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0; - } - - /** - * @return An empty list if no launcher apps. - */ - @NonNull - public static List getLauncherApps(@NonNull Context cxt) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_HOME); - List apps = cxt.getPackageManager().queryIntentActivities(intent, - PackageManager.MATCH_DEFAULT_ONLY); - List pkgNames = new ArrayList<>(apps.size()); - for (ResolveInfo info : apps) { - pkgNames.add(info.activityInfo.packageName); - } - return pkgNames; - } - - /** - * @return An empty list if no input method apps. - */ - @NonNull - public static List getInputMethodApps(@NonNull Context cxt) { - InputMethodManager imm = (InputMethodManager) cxt.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm == null) { - return Collections.emptyList(); - } - List apps = imm.getEnabledInputMethodList(); - List pkgNames = new ArrayList<>(apps.size()); - for (InputMethodInfo info : apps) { - pkgNames.add(info.getPackageName()); - } - return pkgNames; - } - - @TargetApi(Build.VERSION_CODES.N) - public static ActivityInfo[] getAllReceivers(Context cxt, String pkgName, boolean onlyExported) { - try { - PackageManager pm = cxt.getPackageManager(); - int flags = PackageManager.GET_RECEIVERS | PackageManager.MATCH_DISABLED_COMPONENTS; - PackageInfo pkgInfo = pm.getPackageInfo(pkgName, flags); - if (onlyExported) { - ActivityInfo[] tmpArray = new ActivityInfo[pkgInfo.receivers.length]; - int size = 0; - for (ActivityInfo item : pkgInfo.receivers) { - if (!item.exported) continue; - tmpArray[size] = item; - size++; - } - if (size == 0) return null; - return Arrays.copyOf(tmpArray, size); - } else { - return pkgInfo.receivers; - } - } catch (PackageManager.NameNotFoundException e) { - if (DEBUG) LibLogger.w(TAG, "app not found", e); - } - return null; - } - - @TargetApi(Build.VERSION_CODES.N) - public static ServiceInfo[] getAllServices(Context cxt, String pkgName, boolean onlyExported) { - try { - PackageManager pm = cxt.getPackageManager(); - int flags = PackageManager.GET_SERVICES | PackageManager.MATCH_DISABLED_COMPONENTS; - PackageInfo pkgInfo = pm.getPackageInfo(pkgName, flags); - if (onlyExported) { - ServiceInfo[] tmpArray = new ServiceInfo[pkgInfo.services.length]; - int size = 0; - for (ServiceInfo item : pkgInfo.services) { - if (!item.exported) continue; - tmpArray[size] = item; - size++; - } - if (size == 0) return null; - return Arrays.copyOf(tmpArray, size); - } else { - return pkgInfo.services; - } - } catch (PackageManager.NameNotFoundException e) { - if (DEBUG) LibLogger.w(TAG, "app not found", e); - } - return null; - } - - @TargetApi(Build.VERSION_CODES.N) - public static ActivityInfo[] getAllActivities(Context cxt, String pkgName, boolean onlyExported) { - try { - PackageManager pm = cxt.getPackageManager(); - int flags = PackageManager.GET_ACTIVITIES | PackageManager.MATCH_DISABLED_COMPONENTS; - PackageInfo pkgInfo = pm.getPackageInfo(pkgName, flags); - if (onlyExported) { - ActivityInfo[] tmpArray = new ActivityInfo[pkgInfo.activities.length]; - int size = 0; - for (ActivityInfo item : pkgInfo.activities) { - if (!item.exported) continue; - tmpArray[size] = item; - size++; - } - if (size == 0) return null; - return Arrays.copyOf(tmpArray, size); - } else { - return pkgInfo.activities; - } - } catch (PackageManager.NameNotFoundException e) { - if (DEBUG) LibLogger.w(TAG, "app not found", e); - } - return null; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt new file mode 100644 index 0000000..523f0eb --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt @@ -0,0 +1,174 @@ +package me.ycdev.android.lib.common.utils + +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.os.Build +import android.view.inputmethod.InputMethodManager +import java.util.ArrayList + +@Suppress("unused") +object PackageUtils { + private const val TAG = "PackageUtils" + + /** + * Value for [android.content.pm.ApplicationInfo.flags]: set to `true` if the application + * is permitted to hold privileged permissions. + */ + private const val FLAG_PRIVILEGED = 1 shl 3 + + fun isPkgEnabled(cxt: Context, pkgName: String): Boolean { + try { + val state = cxt.packageManager.getApplicationEnabledSetting(pkgName) + return state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || + state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } catch (e: IllegalArgumentException) { + // the app had been uninstalled already + } + + return true // by default + } + + fun isPkgEnabled(appInfo: ApplicationInfo): Boolean { + return appInfo.enabled + } + + fun isPkgSystem(appInfo: ApplicationInfo): Boolean { + return appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0 + } + + /** + * Check if an app is residing in "/system" (Android 4.3 and old versions) + * or "/system/priv-app" (Android 4.4 and new versions) and has "signatureOrSystem" permission. + */ + fun isPkgPrivileged(appInfo: ApplicationInfo): Boolean { + return appInfo.flags and FLAG_PRIVILEGED != 0 + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) + fun isPkgStopped(appInfo: ApplicationInfo): Boolean { + return appInfo.flags and ApplicationInfo.FLAG_STOPPED != 0 + } + + /** + * @return An empty list if no launcher apps. + */ + fun getLauncherApps(cxt: Context): List { + val intent = Intent(Intent.ACTION_MAIN) + intent.addCategory(Intent.CATEGORY_HOME) + val apps = cxt.packageManager.queryIntentActivities( + intent, + PackageManager.MATCH_DEFAULT_ONLY + ) + val pkgNames = hashSetOf() + for (info in apps) { + pkgNames.add(info.activityInfo.packageName) + } + return pkgNames.toList() + } + + /** + * @return An empty list if no input method apps. + */ + fun getInputMethodApps(cxt: Context): List { + val imm = cxt.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + ?: return emptyList() + + val apps = imm.enabledInputMethodList + val pkgNames = ArrayList(apps.size) + for (info in apps) { + pkgNames.add(info.packageName) + } + return pkgNames + } + + @TargetApi(Build.VERSION_CODES.N) + fun getAllReceivers( + cxt: Context, + pkgName: String, + onlyExported: Boolean + ): Array { + try { + val pm = cxt.packageManager + val flags = PackageManager.GET_RECEIVERS or PackageManager.MATCH_DISABLED_COMPONENTS + val pkgInfo = pm.getPackageInfo(pkgName, flags) + if (onlyExported) { + val tmpArray = arrayOfNulls(pkgInfo.receivers.size) + var size = 0 + for (item in pkgInfo.receivers) { + if (!item.exported) continue + tmpArray[size] = item + size++ + } + @Suppress("UNCHECKED_CAST") + return if (size == 0) emptyArray() else tmpArray.copyOf(size) as Array + } else { + return pkgInfo.receivers + } + } catch (e: PackageManager.NameNotFoundException) { + LibLogger.w(TAG, "app not found", e) + } + + return emptyArray() + } + + @TargetApi(Build.VERSION_CODES.N) + fun getAllServices(cxt: Context, pkgName: String, onlyExported: Boolean): Array { + try { + val pm = cxt.packageManager + val flags = PackageManager.GET_SERVICES or PackageManager.MATCH_DISABLED_COMPONENTS + val pkgInfo = pm.getPackageInfo(pkgName, flags) + if (onlyExported) { + val tmpArray = arrayOfNulls(pkgInfo.services.size) + var size = 0 + for (item in pkgInfo.services) { + if (!item.exported) continue + tmpArray[size] = item + size++ + } + @Suppress("UNCHECKED_CAST") + return if (size == 0) emptyArray() else tmpArray.copyOf(size) as Array + } else { + return pkgInfo.services + } + } catch (e: PackageManager.NameNotFoundException) { + LibLogger.w(TAG, "app not found", e) + } + + return emptyArray() + } + + @TargetApi(Build.VERSION_CODES.N) + fun getAllActivities( + cxt: Context, + pkgName: String, + onlyExported: Boolean + ): Array { + try { + val pm = cxt.packageManager + val flags = PackageManager.GET_ACTIVITIES or PackageManager.MATCH_DISABLED_COMPONENTS + val pkgInfo = pm.getPackageInfo(pkgName, flags) + if (onlyExported) { + val tmpArray = arrayOfNulls(pkgInfo.activities.size) + var size = 0 + for (item in pkgInfo.activities) { + if (!item.exported) continue + tmpArray[size] = item + size++ + } + @Suppress("UNCHECKED_CAST") + return if (size == 0) emptyArray() else tmpArray.copyOf(size) as Array + } else { + return pkgInfo.activities + } + } catch (e: PackageManager.NameNotFoundException) { + LibLogger.w(TAG, "app not found", e) + } + + return emptyArray() + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.java deleted file mode 100644 index da8c750..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class Preconditions { - public static void checkMainThread() { - if (!ThreadUtils.isMainThread()) { - throw new RuntimeException("Not in main thread"); - } - } - - public static void checkNonMainThread() { - if (ThreadUtils.isMainThread()) { - throw new RuntimeException("In main thread"); - } - } - - public static void checkArgument(boolean expression) { - if (!expression) { - throw new IllegalArgumentException(); - } - } - - public static T checkNotNull(T object) { - if (object == null) { - throw new NullPointerException(); - } - return object; - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.kt new file mode 100644 index 0000000..58dc35b --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/Preconditions.kt @@ -0,0 +1,28 @@ +package me.ycdev.android.lib.common.utils + +object Preconditions { + fun checkMainThread() { + if (!ThreadUtils.isMainThread) { + throw RuntimeException("Not in main thread") + } + } + + fun checkNonMainThread() { + if (ThreadUtils.isMainThread) { + throw RuntimeException("In main thread") + } + } + + fun checkArgument(expression: Boolean) { + if (!expression) { + throw IllegalArgumentException() + } + } + + fun checkNotNull(obj: T?): T { + if (obj == null) { + throw NullPointerException() + } + return obj + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.java deleted file mode 100644 index 9abc7e2..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ReflectionUtils { - public static Method findMethod(@NonNull Class classObj, @NonNull String methodName, - Class... parameterTypes) throws NoSuchMethodException { - // first, search public methods - try { - return classObj.getMethod(methodName, parameterTypes); - } catch (NoSuchMethodException e) { - // ignore - } - - // next, search the non-public methods - for (Class c = classObj; c != null; c = c.getSuperclass()) { - try { - Method method = c.getDeclaredMethod(methodName, parameterTypes); - method.setAccessible(true); - return method; - } catch (NoSuchMethodException e) { - // ignore - } - } - - throw new NoSuchMethodException(methodName + " not found"); - } - - public static Field findField(@NonNull Class classObj, @NonNull String fieldName) - throws NoSuchFieldException { - // first, search public fields - try { - return classObj.getField(fieldName); - } catch (NoSuchFieldException e) { - // ignore - } - - // next, search non-public fields - for (Class c = classObj; c != null; c = c.getSuperclass()) { - try { - Field field = c.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } catch (NoSuchFieldException e) { - // ignore - } - } - - throw new NoSuchFieldException(fieldName + " not found"); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.kt new file mode 100644 index 0000000..d273bea --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ReflectionUtils.kt @@ -0,0 +1,62 @@ +package me.ycdev.android.lib.common.utils + +import java.lang.reflect.Field +import java.lang.reflect.Method + +object ReflectionUtils { + @Throws(NoSuchMethodException::class) + fun findMethod( + classObj: Class<*>, + methodName: String, + vararg parameterTypes: Class<*> + ): Method { + // first, search public methods + try { + return classObj.getMethod(methodName, *parameterTypes) + } catch (e: NoSuchMethodException) { + // ignore + } + + // next, search the non-public methods + var c: Class<*>? = classObj + while (c != null) { + try { + val method = c.getDeclaredMethod(methodName, *parameterTypes) + method.isAccessible = true + return method + } catch (e: NoSuchMethodException) { + // ignore + } + + c = c.superclass + } + + throw NoSuchMethodException("$methodName not found") + } + + @Throws(NoSuchFieldException::class) + fun findField(classObj: Class<*>, fieldName: String): Field { + // first, search public fields + try { + return classObj.getField(fieldName) + } catch (e: NoSuchFieldException) { + // ignore + } + + // next, search non-public fields + var c: Class<*>? = classObj + while (c != null) { + try { + val field = c.getDeclaredField(fieldName) + field.isAccessible = true + return field + } catch (e: NoSuchFieldException) { + // ignore + } + + c = c.superclass + } + + throw NoSuchFieldException("$fieldName not found") + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.java deleted file mode 100644 index 1269092..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.content.Context; -import android.os.Environment; -import androidx.annotation.NonNull; - -import java.io.File; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class StorageUtils { - /** - * Returns the number of usable free bytes on the partition containing this path. - * Returns 0 if this path does not exist. - * @see File#getUsableSpace() - */ - @SuppressWarnings("deprecation") - public static long getUsableSpace(@NonNull File path) { - return path.getUsableSpace(); - } - - /** - * Returns the number of free bytes on the partition containing this path. - * Returns 0 if this path does not exist. - * @see File#getFreeSpace() - */ - @SuppressWarnings("deprecation") - public static long getFreeSpace(@NonNull File path) { - return path.getFreeSpace(); - } - - /** - * Returns the total size in bytes of the partition containing this path. - * Returns 0 if this path does not exist. - * @see File#getTotalSpace() - */ - @SuppressWarnings("deprecation") - public static long getTotalSpace(@NonNull File path) { - return path.getTotalSpace(); - } - - /** - * Check if the external storage is built-in or removable. - * @return true if the external storage is removable (like an SD card), false - * otherwise. - * @see Environment#isExternalStorageRemovable() - */ - public static boolean isExternalStorageRemovable() { - return Environment.isExternalStorageRemovable(); - } - - /** - * Check if the external storage is emulated by a portion of the internal storage. - * @return true if the external storage is emulated, false otherwise. - * @see Environment#isExternalStorageEmulated() - */ - public static boolean isExternalStorageEmulated() { - return Environment.isExternalStorageEmulated(); - } - - /** - * Get the external app cache directory. - * @param context The context to use - * @return The external cache dir - * @see Context#getExternalCacheDir() - */ - public static File getExternalCacheDir(@NonNull Context context) { - return context.getExternalCacheDir(); - } - - public static boolean isExternalStorageAvailable() { - return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); - } - - public static String getExternalStoragePath() { - return Environment.getExternalStorageDirectory().getAbsolutePath(); - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.kt new file mode 100644 index 0000000..a5b2991 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StorageUtils.kt @@ -0,0 +1,67 @@ +package me.ycdev.android.lib.common.utils + +import android.content.Context +import android.os.Environment + +import java.io.File + +@Suppress("unused") +object StorageUtils { + + /** + * Check if the external storage is built-in or removable. + * @return true if the external storage is removable (like an SD card), false + * otherwise. + * @see Environment.isExternalStorageRemovable + */ + fun isExternalStorageRemovable(): Boolean = Environment.isExternalStorageRemovable() + + /** + * Check if the external storage is emulated by a portion of the internal storage. + * @return true if the external storage is emulated, false otherwise. + * @see Environment.isExternalStorageEmulated + */ + fun isExternalStorageEmulated(): Boolean = Environment.isExternalStorageEmulated() + + fun isExternalStorageAvailable(): Boolean = + Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED + + fun getExternalStoragePath(): String = Environment.getExternalStorageDirectory().absolutePath + + /** + * Returns the number of usable free bytes on the partition containing this path. + * Returns 0 if this path does not exist. + * @see File.getUsableSpace + */ + fun getUsableSpace(path: File): Long { + return path.usableSpace + } + + /** + * Returns the number of free bytes on the partition containing this path. + * Returns 0 if this path does not exist. + * @see File.getFreeSpace + */ + fun getFreeSpace(path: File): Long { + return path.freeSpace + } + + /** + * Returns the total size in bytes of the partition containing this path. + * Returns 0 if this path does not exist. + * @see File.getTotalSpace + */ + fun getTotalSpace(path: File): Long { + return path.totalSpace + } + + /** + * Get the external app cache directory. + * @param context The context to use + * @return The external cache dir + * @see Context.getExternalCacheDir + */ + fun getExternalCacheDir(context: Context): File? { + return context.externalCacheDir + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.java deleted file mode 100644 index 696911b..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.java +++ /dev/null @@ -1,34 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class StringUtils { - public static String trimPrefixSpaces(String str) { - final int N = str.length(); - int index = 0; - while (index < N && (str.charAt(index) <= '\u0020' || str.charAt(index) == '\u00a0')) { - index++; - } - if (index > 0) { - return str.substring(index); - } - return str; - } - - public static int parseInt(@NonNull String value, int defValue) { - try { - return Integer.parseInt(value); - } catch (Exception e) { - return defValue; - } - } - - public static long parseLong(@NonNull String value, long defValue) { - try { - return Long.parseLong(value); - } catch (Exception e) { - return defValue; - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.kt new file mode 100644 index 0000000..dc59dc5 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/StringUtils.kt @@ -0,0 +1,31 @@ +package me.ycdev.android.lib.common.utils + +@Suppress("unused") +object StringUtils { + fun trimPrefixSpaces(str: String): String { + val size = str.length + var index = 0 + while (index < size && (str[index] <= '\u0020' || str[index] == '\u00a0')) { + index++ + } + return if (index > 0) { + str.substring(index) + } else str + } + + fun parseInt(value: String, defValue: Int): Int { + return try { + value.toInt() + } catch (e: Exception) { + defValue + } + } + + fun parseLong(value: String, defValue: Long): Long { + return try { + value.toLong() + } catch (e: Exception) { + defValue + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.java deleted file mode 100644 index 4160d72..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.java +++ /dev/null @@ -1,94 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.List; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class SystemServiceHelper { - private static final String TAG = "SystemServiceHelper"; - - @Nullable - public static ActivityManager getActivityManager(@NonNull Context context) { - ActivityManager am = null; - try { - am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - } catch (Throwable e) { - // Exception may be thrown on some devices - LibLogger.w(TAG, "unexpected when get AM", e); - } - return am; - } - - @Nullable - public static PackageManager getPackageManager(@NonNull Context context) { - PackageManager pm = null; - try { - pm = context.getPackageManager(); - } catch (Throwable e) { - // Exception may be thrown on some devices - LibLogger.w(TAG, "unexpected when get PM", e); - } - return pm; - } - - @NonNull - public static List getRunningServices( - @Nullable ActivityManager am, int maxNum) { - List runServiceList = null; - try { - if (am != null) { - runServiceList = am.getRunningServices(maxNum); - } - } catch (Exception e) { - // Exception may be thrown on some devices - LibLogger.w(TAG, "unexpected when get running services", e); - } - if (runServiceList == null) { - runServiceList = new ArrayList<>(); - } - return runServiceList; - } - - @NonNull - public static List getRunningAppProcesses( - @Nullable ActivityManager am) { - List runProcessList = null; - try { - if (am != null) { - runProcessList = am.getRunningAppProcesses(); - } - } catch (Exception e) { - // Exception may be thrown on some devices - LibLogger.w(TAG, "unexpected when get running processes", e); - } - if (runProcessList == null) { - runProcessList = new ArrayList<>(); - } - return runProcessList; - } - - @NonNull - public static List getInstalledPackages(@Nullable PackageManager pm, int flags) { - List installedPackages = null; - try { - if (pm != null) { - installedPackages = pm.getInstalledPackages(flags); - } - } catch (Exception e) { - // Exception may be thrown on some devices - LibLogger.w(TAG, "unexpected when get installed packages", e); - } - if (installedPackages == null) { - installedPackages = new ArrayList<>(); - } - return installedPackages; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.kt new file mode 100644 index 0000000..072f2d9 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/SystemServiceHelper.kt @@ -0,0 +1,83 @@ +package me.ycdev.android.lib.common.utils + +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.app.ActivityManager.RunningServiceInfo +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager + +@Suppress("unused") +object SystemServiceHelper { + private const val TAG = "SystemServiceHelper" + + fun getActivityManager(context: Context): ActivityManager? { + var am: ActivityManager? = null + try { + am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + } catch (e: Throwable) { + // Exception may be thrown on some devices + LibLogger.w(TAG, "unexpected when get AM", e) + } + + return am + } + + fun getPackageManager(context: Context): PackageManager? { + var pm: PackageManager? = null + try { + pm = context.packageManager + } catch (e: Throwable) { + // Exception may be thrown on some devices + LibLogger.w(TAG, "unexpected when get PM", e) + } + + return pm + } + + fun getRunningServices(am: ActivityManager, maxNum: Int): List { + var runServiceList: List? = null + try { + @Suppress("DEPRECATION") + runServiceList = am.getRunningServices(maxNum) + } catch (e: Exception) { + // Exception may be thrown on some devices + LibLogger.w(TAG, "unexpected when get running services", e) + } + + if (runServiceList == null) { + runServiceList = emptyList() + } + return runServiceList + } + + fun getRunningAppProcesses(am: ActivityManager): List { + var runProcessList: List? = null + try { + runProcessList = am.runningAppProcesses + } catch (e: Exception) { + // Exception may be thrown on some devices + LibLogger.w(TAG, "unexpected when get running processes", e) + } + + if (runProcessList == null) { + runProcessList = emptyList() + } + return runProcessList + } + + fun getInstalledPackages(pm: PackageManager, flags: Int): List { + var installedPackages: List? = null + try { + installedPackages = pm.getInstalledPackages(flags) + } catch (e: Exception) { + // Exception may be thrown on some devices + LibLogger.w(TAG, "unexpected when get installed packages", e) + } + + if (installedPackages == null) { + installedPackages = emptyList() + } + return installedPackages + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.java deleted file mode 100644 index 670f3f5..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import android.os.Looper; - -import java.util.Set; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ThreadUtils { - public static boolean isMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } - - public static boolean isThreadRunning(long tid) { - Set threadSet = Thread.getAllStackTraces().keySet(); - for (Thread t : threadSet) { - if (t.getId() == tid) { - return true; - } - } - return false; - } - - public static void sleep(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.kt new file mode 100644 index 0000000..2d78fd9 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/ThreadUtils.kt @@ -0,0 +1,26 @@ +package me.ycdev.android.lib.common.utils + +import android.os.Looper + +object ThreadUtils { + val isMainThread: Boolean + get() = Looper.myLooper() == Looper.getMainLooper() + + fun isThreadRunning(tid: Long): Boolean { + val threadSet = Thread.getAllStackTraces().keys + for (t in threadSet) { + if (t.id == tid) { + return true + } + } + return false + } + + fun sleep(millis: Long) { + try { + Thread.sleep(millis) + } catch (e: InterruptedException) { + e.printStackTrace() + } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.java deleted file mode 100644 index 9d69366..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import java.lang.ref.WeakReference; - -import android.os.Handler; -import android.os.Message; -import androidx.annotation.NonNull; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class WeakHandler extends Handler { - private WeakReference mTargetHandler; - - public WeakHandler(@NonNull Handler.Callback msgHandler) { - mTargetHandler = new WeakReference<>(msgHandler); - } - - @Override - public void handleMessage(Message msg) { - Handler.Callback realHandler = mTargetHandler.get(); - if (realHandler != null) { - realHandler.handleMessage(msg); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.kt new file mode 100644 index 0000000..ae924a7 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakHandler.kt @@ -0,0 +1,16 @@ +package me.ycdev.android.lib.common.utils + +import java.lang.ref.WeakReference + +import android.os.Handler +import android.os.Message + +@Suppress("unused") +class WeakHandler(msgHandler: Callback) : Handler() { + private val targetHandler: WeakReference = WeakReference(msgHandler) + + override fun handleMessage(msg: Message) { + val realHandler = targetHandler.get() + realHandler?.handleMessage(msg) + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.java b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.java deleted file mode 100644 index 87ed558..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.java +++ /dev/null @@ -1,100 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import androidx.annotation.NonNull; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -public class WeakListenerManager { - private static final String TAG = "WeakListenerManager"; - - public interface NotifyAction { - void notify(IListener listener); - } - - private class ListenerInfo { - String className; - WeakReference holder; - - ListenerInfo(IListener listener) { - className = listener.getClass().getName(); - holder = new WeakReference<>(listener); - } - } - - private final List mListeners = new ArrayList<>(); - - /** - * Only invoked when invoke {@link #addListener(Object)} - */ - protected void onFirstListenerAdd() { - // nothing to do - } - - /** - * Only invoked when invoke {@link #removeListener(Object)} - */ - protected void onLastListenerRemoved() { - // nothing to do - } - - /** - * Override this method to notify the listener when registered. - */ - protected void onListenerAdded(@NonNull IListener listener) { - // nothing to do - } - - public void addListener(@NonNull IListener listener) { - synchronized (mListeners) { - if (mListeners.size() == 0) { - onFirstListenerAdd(); - } - - for (ListenerInfo l : mListeners) { - if (l.holder.get() == listener) return; // skip duplicate listeners - } - mListeners.add(new ListenerInfo(listener)); - } - - // Notify the listener to get initialized - onListenerAdded(listener); - } - - public void removeListener(@NonNull IListener listener) { - synchronized (mListeners) { - final int N = mListeners.size(); - boolean removed = false; - for (int i = 0; i < N; i++) { - ListenerInfo listenerInfo = mListeners.get(i); - if (listenerInfo.holder.get() == listener) { - mListeners.remove(i); - removed = true; - break; - } - } - if (mListeners.size() == 0 && removed) { - onLastListenerRemoved(); - } - } - } - - public void notifyListeners(@NonNull NotifyAction action) { - synchronized (mListeners) { - for (int i = 0; i < mListeners.size();) { - ListenerInfo listenerInfo = mListeners.get(i); - IListener l = listenerInfo.holder.get(); - if (l == null) { - LibLogger.e(TAG, "listener leak found: " + listenerInfo.className); - mListeners.remove(i); - } else { - LibLogger.d(TAG, "notify: " + listenerInfo.className); - action.notify(l); - i++; - } - } - LibLogger.d(TAG, "notify done, cur size: " + mListeners.size()); - } - } -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.kt new file mode 100644 index 0000000..30b1d3a --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/WeakListenerManager.kt @@ -0,0 +1,99 @@ +package me.ycdev.android.lib.common.utils + +import java.lang.ref.WeakReference +import java.util.ArrayList + +@Suppress("unused") +open class WeakListenerManager { + private val listeners = ArrayList>() + + interface NotifyAction { + fun notify(listener: IListener) + } + + private class ListenerInfo internal constructor(listener: IListener) { + internal var className: String = listener::class.java.name + internal var holder: WeakReference = WeakReference(listener) + } + + /** + * Only invoked when invoke [.addListener] + */ + protected open fun onFirstListenerAdd() { + // nothing to do + } + + /** + * Only invoked when invoke [.removeListener] + */ + protected open fun onLastListenerRemoved() { + // nothing to do + } + + /** + * Override this method to notify the listener when registered. + */ + protected open fun onListenerAdded(listener: IListener) { + // nothing to do + } + + fun addListener(listener: IListener) { + synchronized(listeners) { + if (listeners.size == 0) { + onFirstListenerAdd() + } + + for (l in listeners) { + if (l.holder.get() === listener) return // skip duplicate listeners + } + listeners.add(ListenerInfo(listener)) + } + + // Notify the listener to get initialized + onListenerAdded(listener) + } + + fun removeListener(listener: IListener) { + synchronized(listeners) { + var removed = false + for (i in 0 until listeners.size) { + val listenerInfo = listeners[i] + if (listenerInfo.holder.get() === listener) { + listeners.removeAt(i) + removed = true + break + } + } + if (listeners.size == 0 && removed) { + onLastListenerRemoved() + } + } + } + + fun notifyListeners(action: NotifyAction) { + notifyListeners { action.notify(it) } + } + + fun notifyListeners(action: (IListener) -> Unit) { + synchronized(listeners) { + var i = 0 + while (i < listeners.size) { + val listenerInfo = listeners[i] + val l = listenerInfo.holder.get() + if (l == null) { + LibLogger.e(TAG, "listener leak found: " + listenerInfo.className) + listeners.removeAt(i) + } else { + LibLogger.d(TAG, "notify: " + listenerInfo.className) + action(l) + i++ + } + } + LibLogger.d(TAG, "notify done, cur size: " + listeners.size) + } + } + + companion object { + private const val TAG = "WeakListenerManager" + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.java b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.java deleted file mode 100644 index acf019c..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.java +++ /dev/null @@ -1,65 +0,0 @@ -package me.ycdev.android.lib.common.wrapper; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import androidx.annotation.NonNull; - -/** - * A wrapper class to avoid security issues when sending/receiving broadcast. - */ -@SuppressWarnings({"unused", "WeakerAccess"}) -public class BroadcastHelper { - public static final String PERM_INTERNAL_BROADCAST_SUFFIX = ".permission.INTERNAL"; - - private BroadcastHelper() { - // nothing to do - } - - public static String getInternalBroadcastPerm(@NonNull Context cxt) { - return cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX; - } - - /** - * Register a receiver for internal broadcast. - */ - public static Intent registerForInternal(@NonNull Context cxt, - @NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) { - String perm = cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX; - return cxt.registerReceiver(receiver, filter, perm, null); - } - - /** - * Register a receiver for external broadcast (includes system broadcast). - */ - public static Intent registerForExternal(@NonNull Context cxt, - @NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) { - return cxt.registerReceiver(receiver, filter); - } - - /** - * Send a broadcast to internal receivers. - */ - public static void sendToInternal(@NonNull Context cxt, @NonNull Intent intent) { - String perm = cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX; - intent.setPackage(cxt.getPackageName()); // only works on Android 4.0 and higher versions - cxt.sendBroadcast(intent, perm); - } - - /** - * Send a broadcast to external receivers. - */ - public static void sendToExternal(@NonNull Context cxt, @NonNull Intent intent, - @NonNull String perm) { - cxt.sendBroadcast(intent, perm); - } - - /** - * Send a broadcast to external receivers. - */ - public static void sendToExternal(@NonNull Context cxt, @NonNull Intent intent) { - cxt.sendBroadcast(intent); - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt new file mode 100644 index 0000000..e060636 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt @@ -0,0 +1,68 @@ +package me.ycdev.android.lib.common.wrapper + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter + +/** + * A wrapper class to avoid security issues when sending/receiving broadcast. + */ +@Suppress("unused") +object BroadcastHelper { + private const val PERM_INTERNAL_BROADCAST_SUFFIX = ".permission.INTERNAL" + + fun getInternalBroadcastPerm(cxt: Context): String { + return cxt.packageName + PERM_INTERNAL_BROADCAST_SUFFIX + } + + /** + * Register a receiver for internal broadcast. + */ + fun registerForInternal( + cxt: Context, + receiver: BroadcastReceiver, + filter: IntentFilter + ): Intent? { + val perm = cxt.packageName + PERM_INTERNAL_BROADCAST_SUFFIX + return cxt.registerReceiver(receiver, filter, perm, null) + } + + /** + * Register a receiver for external broadcast (includes system broadcast). + */ + fun registerForExternal( + cxt: Context, + receiver: BroadcastReceiver, + filter: IntentFilter + ): Intent? { + return cxt.registerReceiver(receiver, filter) + } + + /** + * Send a broadcast to internal receivers. + */ + fun sendToInternal(cxt: Context, intent: Intent) { + val perm = cxt.packageName + PERM_INTERNAL_BROADCAST_SUFFIX + intent.setPackage(cxt.packageName) // only works on Android 4.0 and higher versions + cxt.sendBroadcast(intent, perm) + } + + /** + * Send a broadcast to external receivers. + */ + fun sendToExternal( + cxt: Context, + intent: Intent, + perm: String + ) { + cxt.sendBroadcast(intent, perm) + } + + /** + * Send a broadcast to external receivers. + */ + fun sendToExternal(cxt: Context, intent: Intent) { + cxt.sendBroadcast(intent) + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.java b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.java deleted file mode 100644 index d09a8d1..0000000 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.java +++ /dev/null @@ -1,330 +0,0 @@ -package me.ycdev.android.lib.common.wrapper; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.Serializable; -import java.util.ArrayList; - -import me.ycdev.android.lib.common.utils.LibLogger; - -/** - * A wrapper class to avoid security issues when parsing Intent extras. - *

    See details of the issue: http://code.google.com/p/android/issues/detail?id=177223.

    - */ -@SuppressWarnings("unused") -public class IntentHelper { - private static final String TAG = "IntentUtils"; - - private IntentHelper() { - // nothing to do - } - - private static void onIntentAttacked(@NonNull Intent intent, Throwable e) { - // prevent OOM for Android 5.0~? - intent.replaceExtras((Bundle) null); - LibLogger.w(TAG, "attacked?", e); - } - - public static boolean hasExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return false; - } - - try { - return intent.hasExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return false; - } - - public static boolean getBooleanExtra(@Nullable Intent intent, @NonNull String key, - boolean defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getBooleanExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static byte getByteExtra(@Nullable Intent intent, @NonNull String key, - byte defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getByteExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static short getShortExtra(@Nullable Intent intent, @NonNull String key, - short defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getShortExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static int getIntExtra(@Nullable Intent intent, @NonNull String key, - int defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getIntExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static long getLongExtra(@Nullable Intent intent, @NonNull String key, - long defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getLongExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static float getFloatExtra(@Nullable Intent intent, @NonNull String key, - float defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getFloatExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static double getDoubleExtra(@Nullable Intent intent, @NonNull String key, - double defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getDoubleExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - public static char getCharExtra(@Nullable Intent intent, @NonNull String key, - char defValue) { - if (intent == null) { - return defValue; - } - - try { - return intent.getCharExtra(key, defValue); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return defValue; - } - - @Nullable - public static String getStringExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getStringExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static CharSequence getCharSequenceExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getCharSequenceExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static Serializable getSerializableExtra(@Nullable Intent intent, - @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getSerializableExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static T getParcelableExtra(@Nullable Intent intent, - @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getParcelableExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static boolean[] getBooleanArrayExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getBooleanArrayExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static int[] getIntArrayExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getIntArrayExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static long[] getLongArrayExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getLongArrayExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static String[] getStringArrayExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getStringArrayExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static Parcelable[] getParcelableArrayExtra(@Nullable Intent intent, - @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getParcelableArrayExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static ArrayList getStringArrayListExtra(@Nullable Intent intent, - @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getStringArrayListExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static ArrayList getParcelableArrayListExtra( - @Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getParcelableArrayListExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - - @Nullable - public static Bundle getBundleExtra(@Nullable Intent intent, @NonNull String key) { - if (intent == null) { - return null; - } - - try { - return intent.getBundleExtra(key); - } catch (Exception e) { - onIntentAttacked(intent, e); - } - return null; - } - -} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.kt new file mode 100644 index 0000000..7893d42 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/IntentHelper.kt @@ -0,0 +1,318 @@ +package me.ycdev.android.lib.common.wrapper + +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import me.ycdev.android.lib.common.utils.LibLogger +import java.io.Serializable +import java.util.ArrayList + +/** + * A wrapper class to avoid security issues when parsing Intent extras. + * + * See details of the issue: http://code.google.com/p/android/issues/detail?id=177223. + */ +@Suppress("unused") +object IntentHelper { + private const val TAG = "IntentUtils" + + private fun onIntentAttacked(intent: Intent, e: Throwable) { + // prevent OOM for Android 5.0~? + intent.replaceExtras((null as Bundle?)!!) + LibLogger.w(TAG, "attacked?", e) + } + + fun hasExtra(intent: Intent?, key: String): Boolean { + if (intent == null) { + return false + } + + try { + return intent.hasExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return false + } + + fun getBooleanExtra(intent: Intent?, key: String, defValue: Boolean): Boolean { + if (intent == null) { + return defValue + } + + try { + return intent.getBooleanExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getByteExtra(intent: Intent?, key: String, defValue: Byte): Byte { + if (intent == null) { + return defValue + } + + try { + return intent.getByteExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getShortExtra(intent: Intent?, key: String, defValue: Short): Short { + if (intent == null) { + return defValue + } + + try { + return intent.getShortExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getIntExtra(intent: Intent?, key: String, defValue: Int): Int { + if (intent == null) { + return defValue + } + + try { + return intent.getIntExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getLongExtra(intent: Intent?, key: String, defValue: Long): Long { + if (intent == null) { + return defValue + } + + try { + return intent.getLongExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getFloatExtra(intent: Intent?, key: String, defValue: Float): Float { + if (intent == null) { + return defValue + } + + try { + return intent.getFloatExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getDoubleExtra(intent: Intent?, key: String, defValue: Double): Double { + if (intent == null) { + return defValue + } + + try { + return intent.getDoubleExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getCharExtra(intent: Intent?, key: String, defValue: Char): Char { + if (intent == null) { + return defValue + } + + try { + return intent.getCharExtra(key, defValue) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return defValue + } + + fun getStringExtra(intent: Intent?, key: String): String? { + if (intent == null) { + return null + } + + try { + return intent.getStringExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getCharSequenceExtra(intent: Intent?, key: String): CharSequence? { + if (intent == null) { + return null + } + + try { + return intent.getCharSequenceExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getSerializableExtra(intent: Intent?, key: String): Serializable? { + if (intent == null) { + return null + } + + try { + return intent.getSerializableExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getParcelableExtra(intent: Intent?, key: String): T? { + if (intent == null) { + return null + } + + try { + return intent.getParcelableExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getBooleanArrayExtra(intent: Intent?, key: String): BooleanArray? { + if (intent == null) { + return null + } + + try { + return intent.getBooleanArrayExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getIntArrayExtra(intent: Intent?, key: String): IntArray? { + if (intent == null) { + return null + } + + try { + return intent.getIntArrayExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getLongArrayExtra(intent: Intent?, key: String): LongArray? { + if (intent == null) { + return null + } + + try { + return intent.getLongArrayExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getStringArrayExtra(intent: Intent?, key: String): Array? { + if (intent == null) { + return null + } + + try { + return intent.getStringArrayExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getParcelableArrayExtra(intent: Intent?, key: String): Array? { + if (intent == null) { + return null + } + + try { + return intent.getParcelableArrayExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getStringArrayListExtra(intent: Intent?, key: String): ArrayList? { + if (intent == null) { + return null + } + + try { + return intent.getStringArrayListExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getParcelableArrayListExtra(intent: Intent?, key: String): ArrayList? { + if (intent == null) { + return null + } + + try { + return intent.getParcelableArrayListExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } + + fun getBundleExtra(intent: Intent?, key: String): Bundle? { + if (intent == null) { + return null + } + + try { + return intent.getBundleExtra(key) + } catch (e: Exception) { + onIntentAttacked(intent, e) + } + + return null + } +} diff --git a/baseLib/src/test/java/me/ycdev/android/lib/common/net/NetworkUtilsTestBasic.kt b/baseLib/src/test/java/me/ycdev/android/lib/common/net/NetworkUtilsTestBasic.kt index d3f6065..33b73d4 100644 --- a/baseLib/src/test/java/me/ycdev/android/lib/common/net/NetworkUtilsTestBasic.kt +++ b/baseLib/src/test/java/me/ycdev/android/lib/common/net/NetworkUtilsTestBasic.kt @@ -3,9 +3,9 @@ package me.ycdev.android.lib.common.net import android.net.ConnectivityManager import android.telephony.TelephonyManager import com.google.common.truth.Truth.assertThat -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_COMPANION_PROXY -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_MOBILE -import me.ycdev.android.lib.common.net.NetworkUtils.NetworkType.NETWORK_TYPE_WIFI +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_COMPANION_PROXY +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_MOBILE +import me.ycdev.android.lib.common.net.NetworkUtils.NETWORK_TYPE_WIFI import me.ycdev.android.lib.common.net.NetworkUtils.WEAR_OS_COMPANION_PROXY import org.junit.Test diff --git a/baseLib/src/test/java/me/ycdev/android/lib/common/packets/TinyPacketsWorkerTest.kt b/baseLib/src/test/java/me/ycdev/android/lib/common/packets/TinyPacketsWorkerTest.kt index e0886ee..8707774 100644 --- a/baseLib/src/test/java/me/ycdev/android/lib/common/packets/TinyPacketsWorkerTest.kt +++ b/baseLib/src/test/java/me/ycdev/android/lib/common/packets/TinyPacketsWorkerTest.kt @@ -424,6 +424,7 @@ class TinyPacketsWorkerTest : PacketsWorkerTestBase() { @Test fun setDebug_true() { val tree = TimberJvmTree() + tree.keepLogs() Timber.plant(tree) val packetsWorker = TinyPacketsWorker(parserCallback) diff --git a/baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.java b/baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.java deleted file mode 100644 index b61f8a5..0000000 --- a/baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package me.ycdev.android.lib.common.utils; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; - -public class EncodingUtilsTest { - @Rule - public ExpectedException thrownRule = ExpectedException.none(); - - @Test - public void encodeWithHex() { - byte[] data = new byte[] {0x1a, (byte)0x2b, 0x3c, 0x4d, (byte)0x5c, 0x6d, (byte)0x7e}; - String result = EncodingUtils.encodeWithHex(data, 0, data.length); - assertThat(result, equalTo("1A2B3C4D5C6D7E")); - result = EncodingUtils.encodeWithHex(data, 1, 4, true); - assertThat(result, equalTo("2B3C4D")); - result = EncodingUtils.encodeWithHex(data, 3, 20); - assertThat(result, equalTo("4D5C6D7E")); - - // lowercase - result = EncodingUtils.encodeWithHex(data, 0, data.length, false); - assertThat(result, equalTo("1a2b3c4d5c6d7e")); - result = EncodingUtils.encodeWithHex(data, 1, 4, false); - assertThat(result, equalTo("2b3c4d")); - result = EncodingUtils.encodeWithHex(data, 3, 20, false); - assertThat(result, equalTo("4d5c6d7e")); - } - - @Test - public void test_fromHexString() { - String hexStr = "01020304050607"; - String hexStr2 = " 010 20 30 405 060 7 "; - String hexStr3 = "010 203 040 506 07"; - byte[] data = new byte[] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; - assertThat(EncodingUtils.fromHexString(hexStr), equalTo(data)); - assertThat(EncodingUtils.fromHexString(hexStr2), equalTo(data)); - assertThat(EncodingUtils.fromHexString(hexStr3), equalTo(data)); - } - - @Test - public void test_illegalLength() { - thrownRule.expect(IllegalArgumentException.class); - thrownRule.expectMessage(startsWith("Bad length: 10101")); - - String hexStr = "10101"; - EncodingUtils.fromHexString(hexStr); - } - - @Test - public void test_illegalCharacter() { - thrownRule.expect(IllegalArgumentException.class); - thrownRule.expectMessage(startsWith("Not hex string: 10101X")); - - String hexStr = "10101X"; - EncodingUtils.fromHexString(hexStr); - } -} diff --git a/baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.kt b/baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.kt new file mode 100644 index 0000000..6f553af --- /dev/null +++ b/baseLib/src/test/java/me/ycdev/android/lib/common/utils/EncodingUtilsTest.kt @@ -0,0 +1,62 @@ +package me.ycdev.android.lib.common.utils + +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException + +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.startsWith +import org.hamcrest.MatcherAssert.assertThat + +class EncodingUtilsTest { + @get:Rule + var thrownRule = ExpectedException.none() + + @Test + fun encodeWithHex() { + val data = byteArrayOf(0x1a, 0x2b.toByte(), 0x3c, 0x4d, 0x5c.toByte(), 0x6d, 0x7e.toByte()) + var result = EncodingUtils.encodeWithHex(data, 0, data.size) + assertThat(result, equalTo("1A2B3C4D5C6D7E")) + result = EncodingUtils.encodeWithHex(data, 1, 4, true) + assertThat(result, equalTo("2B3C4D")) + result = EncodingUtils.encodeWithHex(data, 3, 20) + assertThat(result, equalTo("4D5C6D7E")) + + // lowercase + result = EncodingUtils.encodeWithHex(data, 0, data.size, false) + assertThat(result, equalTo("1a2b3c4d5c6d7e")) + result = EncodingUtils.encodeWithHex(data, 1, 4, false) + assertThat(result, equalTo("2b3c4d")) + result = EncodingUtils.encodeWithHex(data, 3, 20, false) + assertThat(result, equalTo("4d5c6d7e")) + } + + @Test + fun test_fromHexString() { + val hexStr = "01020304050607" + val hexStr2 = " 010 20 30 405 060 7 " + val hexStr3 = "010 203 040 506 07" + val data = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07) + assertThat(EncodingUtils.fromHexString(hexStr), equalTo(data)) + assertThat(EncodingUtils.fromHexString(hexStr2), equalTo(data)) + assertThat(EncodingUtils.fromHexString(hexStr3), equalTo(data)) + } + + @Test + fun test_illegalLength() { + thrownRule.expect(IllegalArgumentException::class.java) + thrownRule.expectMessage(startsWith("Bad length: 10101")) + + val hexStr = "10101" + EncodingUtils.fromHexString(hexStr) + } + + @Test + fun test_illegalCharacter() { + thrownRule.expect(IllegalArgumentException::class.java) + thrownRule.expectMessage(startsWith("Not hex string: 10101X")) + + val hexStr = "10101X" + EncodingUtils.fromHexString(hexStr) + } +} diff --git a/baseLib/src/test/java/me/ycdev/android/lib/common/utils/ReflectionUtilsTest.kt b/baseLib/src/test/java/me/ycdev/android/lib/common/utils/ReflectionUtilsTest.kt index a327b46..4a9e6cd 100644 --- a/baseLib/src/test/java/me/ycdev/android/lib/common/utils/ReflectionUtilsTest.kt +++ b/baseLib/src/test/java/me/ycdev/android/lib/common/utils/ReflectionUtilsTest.kt @@ -101,7 +101,7 @@ class ReflectionUtilsTest { TestB::class.java, "a11", String::class.java, - Long::class.javaPrimitiveType + Long::class.java ) assertTrue(a11.declaringClass == TestA::class.java && a11.name == "a11") assertTrue(a11.invoke(objB, "a", 11L) == "a11") @@ -111,14 +111,14 @@ class ReflectionUtilsTest { assertTrue(a13.invoke(null) == "a13") val a14 = - ReflectionUtils.findMethod(TestB::class.java, "a14", Long::class.javaPrimitiveType) + ReflectionUtils.findMethod(TestB::class.java, "a14", Long::class.java) assertTrue(a14.declaringClass == TestA::class.java && a14.name == "a14") assertTrue(a14.invoke(null, 14L) as Long == 14L) val a15 = ReflectionUtils.findMethod( TestB::class.java, "a15", - Int::class.javaPrimitiveType, + Int::class.java, String::class.java ) assertTrue(a15.declaringClass == TestA::class.java && a15.name == "a15") @@ -130,7 +130,7 @@ class ReflectionUtilsTest { assertTrue(a9.invoke(objB) == "a9") val a10 = - ReflectionUtils.findMethod(TestB::class.java, "a10", Int::class.javaPrimitiveType) + ReflectionUtils.findMethod(TestB::class.java, "a10", Int::class.java) assertTrue(a10.declaringClass == TestB::class.java && a10.name == "a10") assertTrue(a10.invoke(objB, 10) as Int == 10) @@ -138,7 +138,7 @@ class ReflectionUtilsTest { TestB::class.java, "b11", String::class.java, - Long::class.javaPrimitiveType + Long::class.java ) assertTrue(b11.declaringClass == TestB::class.java && b11.name == "b11") assertTrue(b11.invoke(objB, "b", 11L) == "b11") @@ -148,14 +148,14 @@ class ReflectionUtilsTest { assertTrue(b13.invoke(null) == "b13") val b14 = - ReflectionUtils.findMethod(TestB::class.java, "b14", Long::class.javaPrimitiveType) + ReflectionUtils.findMethod(TestB::class.java, "b14", Long::class.java) assertTrue(b14.declaringClass == TestB::class.java && b14.name == "b14") assertTrue(b14.invoke(null, 14L) as Long == 14L) val b15 = ReflectionUtils.findMethod( TestB::class.java, "b15", - Int::class.javaPrimitiveType, + Int::class.java, String::class.java ) assertTrue(b15.declaringClass == TestB::class.java && b15.name == "b15") diff --git a/build.gradle b/build.gradle index fcf5d2f..49a48e0 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ allprojects { } ext { - versions.minSdk = 15 + versions.minSdk = 21 // For bintray upload bintrayMaven = [ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13536770052936a92b204cc34e72284a03a6903c..87b738cbd051603d91cc39de6cb000dd98fe6b02 100644 GIT binary patch delta 46897 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 delta 47854 zcmY(KQ*DWo<_WjQ}7Y{XRz3dwMVU4-Q zn%}IHTCm#|FbG8%FmSZ^JoI=RR8%w&kni6?KtO~*?Cq>Tg#T3<@q%*b#-shs8K>Ad zr`UH+w@%+c{`&~&xBr}5L;ue^5&RoI=KuWw38x$_I|K*_BMb-#ePSFbMxy*Te87D{ z9O*B{gu1cl_hD#GL1RfGHwO0Nq?DkNU&Lg);5Z^>(8W|w>&T-Ht)1=jBnuC$yK|!S;vP}>MV^c_r5GVcLs=nH^?rC-*=KA7NJXV?cuDb zS0XP*_AqA;t}qDGK_~w2gbd*IVL+s@XEB%-ar>e_Id-a96hu9C?RXo?piLW%$X3{o z6thF_ILtq7nlQ%HZieLF;a5y`uYWPL6cf4i#ThY!$5@7#D=@tn--Z8>zpi^Ws~JMrK>!OEzJ*l%TcY-YNr$DuvZ&5SeAQcQ`&2vpVPD^i~w>A3>y#3 zrPkuZ>hzg9iEw`#R+-_3Qs!3{ajlg#qK%nNbTX8(T*x!hvzA@>&khT<#UkpOqnSiz z;kisws8VZ&2jH@6o|>$cp12n3Ug>#?E03JkRqwNcke`G4t`38PU9zR|7?hS{!i;3} zY$eEqVL$O&H70pQ46^-)U;*x#H7N~=l&#8xMtrDfSdx&zk=RUwu;t7hXv^|m9!t5~+VRoN)j*{uEY6gE+W6zvCY z4#_@qY<5c#W_iWMD6M)YpLH%QD~Zvgm&QqmFBty5yi})XlHatyTj=KBRr@Snbo(%05sx7WMs$AydwDW?C

    ohmj(wKVBMg2V`Or6Dh zY(8IG;tK2>&RQkxrlW`(B>Zh10(q(zjGgesZVRlxxvdFC?}=}zKetWP^cws&I_m^K z=kOy}3<}o9hR>&$=@#fytQsz@Dq>d%_efe4ipnjrD^+%5~%9N~XNVeysgFbS)>q+Jp7 zq?-oE*R$EC^&lFkzu_w{bLa>6LgbR}ON}`OviJ$pi423;V_PY`;p03WLFt2#5oBNVb$jVhWTr63EG?f;!`v`Nk_# z7CaN^5d%~qEVVhf&LsYUNO%b3D==(`8eslyCd>Ji$Q(235&xhfzYNEyMD{kmX!(`d z05f|(>HA{SF7P)zeC%u})IiRmBqb;5)4el{oY zTu+O8{@nIE)SpfC%fXuV^sIOhTuKR)PrmMX;jboA-}XrnDv>8g4632PZ^_JlXlXmx zY~FCIJEW37oMJpuMXmB1*r9aCf`0<8iTjz&u{LfQJ&n>_aZVn9|H$tDQD}97(xJkC z3cd92A^nfQ0#MM2#gHhV+O&!7kc5Eeq&IKctN!%}YA+5nm`ND)?b^03#dB~<(Csq2 zxK2Eyj3Z{y*tF^TTYp($RV}*(7P9#=bxqc2^Ie-N?Lu*qmR#Cj|DyX}r@K5`L_rw&n9RZgFF$PYewNvt?SlQ!Em8k#kgPbFb|Zp< zfCz$vfUy4eHOT;b6d^P|u%sa~D@V1uWobJby^4m#TFW04;3mexvWeEH3#HVuhEr~# zaDhn%ru&KAtKz7@FM)9ns4^63?XA#u_di_E)9ua{z~8qguwGx@mU!{LkidZ8WVD~_ znnT(!7u0k(&neOHn=Qzp7DfyG_#ualL@*D|A#EC)W|F|7smc$!#X!lReFJ(mppAr3sK4=Dpf}X-92Bq4TEbjrceQCCPt?$$)nGkaX~R@%WR~u9A~;0$ zp3$#eg6<4P{m`Vhbp`bD`s-FvyfWwCmDwok1jXTO^y~lT0K0x>Zk|EJ`Or|hQrp_^$uAzMAf12jMdgcQ9*tQt~1SA$81cdm%d*(a9U}9_J?3}Fy zZHTtw`Pa{?nMFG~NYjxXOA~HYxU>(;))9-0!JjM$LkN8Mi72`#c&Ub$Co(RZlOl;- z0@sU7URZ@kev{iS3$4^BSk8yc$}ywm&sLM1SCO69@3XC@X#68@d&Dbg+$HEdj_a45 z_v`CV1)rl1#!hg0Ac5~=qZ z;^zG&5acfsP?S+ilOdo!l?(0w^>?v%17x#x2h|q^82;kqNIHh+SpyvwohWi3wFN`1V z=-ts@(7V7bKq0<3>AomsUzW}R*I6F)T4Pgc(;^`!@5IS`g8Cfmsab~b<4l=a3qlW$@ zoyBrmcrCM0YZaisq>AZKU1b?^mhnPX%YNMMd~+k2?6Ji5M{!@0xJrhs6?^eelDcaz zeuyorX(F*Z3{f5Tf#b+k*xFc8A{w~&*5K>d7Pb9ixc8jUhD=*P#&-9>`}>nP38D+$ z=`|NwoV<{$1WC@rWt;nCOmUWznS_v<_gA;C_d7wDOAfHAgRtm4PAC%<#mQ?XrqPdY zh3|NkpSHlgw|;OCv$0-`*$NF8<9*2#O5&1TKeJZN#KS+2bGvEj6EyLSfzWx=loc6s zF{UFNc}0q%qd;mVymBB@2EUt<2|1ShGJjj^kawns4=2`(2jc4SK8#Hxp`8Uc2HFQd zoLIU$j~2jj_fj?~#eXjI$OVV9ChSq z&e}R8vTW1lZ%b7M255}OtjTvnT%!$$0w2*Zu#5b{33zUD|iH@Vvja52x9z~b(;%wEAwCQ3TJq^7sO}|VQjH`57 zjRNK(f0#`piFM%Hbu7O4(0Hv7c|L!t)>)^_aZIw)(W9~zMGMcqT)8oGb8gh*O266F zu#X4&_kE;wYb;+yAyS5smR=P#n%!@|zNZcIEf zux^%Vm-DbLh+owhbfy7_CB`i$4=2%%HdeW4HZ{?hD{&Sv_nYBCGQk02BWI)NbQO%A z!h%!y&if)TMU~8fbfOGdTH`aXsAF*b>J$mVi5*))kUm5SlI?*p(j3E{45K=WTmS~e zO_?=8qF4*fqlcwFWljznc+Fiv2+=i_Ld=K?mW<=HMOz*9@*h|(! zeHpj0pW;2_SMx?=1Zy^Xd~KF{s2=MHz74q^$ugO4*$dN(Movt$u*5|UQuUm98S>^y zL!JDld)Pkn{gEEwA&UDPHWMsYbii4{PZ_VnI`S^3AT3L&Tv0eKYpEW*#=|z0OGi&! zJ|s24y^>)*i_y4mGV-HRLVEnIh$mq3NaI>WyhfA*5V$%?x|pWFuI2?MV*R%2<3 zNirku&yOxomx#}l*y?*=60uJqY_dU-jwD%}Cv8SN@>Yq{M2z7#aA-kYqMV^)Mn7S$ zSNi5PZ~eq-6E zz96IXftk@}u4(d-)Rjr-4+6^7ymdy|m*;C7!`G~(q^IY)Sbm8lyjH>XUh)z|INt zD!hN;&wv|2)#psdAyiNd$^UUe0gbg6IP%+dlwbOEoXk^(eM}AKcAh%tatbv2Zi|QB zQY#zPN0gq{q; zXon;-A_cOBbd}pQT`iD>ILm&Vz#Pc~eyyw_0I%|vKo1_}@y*ky#2r0&dQORhDZYG2 z9j#x;l=;uv32cC&<5*-BCo`oMbAk?fV$iJYRHu}0_Ecp91DyFo`lHu(GJN}37TZ!| z*-3N=4in^T{zC2gW<7axS&ht&XY+Hxuhq5uvdqP$Q|OYk)LGzZRAoBJ5BB0@IXC}p zJ~&OgGXM9jumoMMtGSXMW|6w@v-T!KQ9LVG5=(a(xXkd>q1cZ!q-0H{)6rh&9#>gASpE_aKEk5PJ^J=07Vp_)SM$obQ#xK1oRGc{tO# zeZg~lpBw>nr(;F{oO>~A+jQvCniWAXS~OLCNWY04c1N7D`m~-*K*oOzJ9Zjo2`cj8 zts+*m2LAP0V>CeefRyC%zqh62I+uX(`A}zPl~%b&JLK}69LT7PvcsZS6X@3$2|=0r^#XHY zhv9bAeyrAzx4QD$^I1FKV(hsEDaIzR=9#T!aWOGYRCLl+Hf5OoKC#wPRggM5pxaT< zDo3!P_7@Yn=cchQIEDlTV-Bvks7sIrAJ6vx+Qv!`ujoOll?N#>UChQ%Bs zF z$Flfd_E3&g`H%++e@fS7D3rED3}2MVnHXh$K?Z)0D!2|8?{E%$U5c{v`Dd)quF(0o>FD>r{{jaKv9g=S^@dq~B!AjTSTJtsS9-X+%c7?21HLTpye@?YMlwzy*Zt2>S4!LCjP++E%U|0Iw zUYOYpxEr(6J@}`C`VYp0B&)F|oV|3Hro;HAVfe;Wt_#$BZIg<>g)2%O!Dr^#dW6pz z6W-Y1b}KpkYY)h}{Z(El{*h=o);BQQk3-?$`cgk*_bJz4<5oytsLxf!Voq*ZF4a^LHEzH8C>B?$p7e)z z*>cvWLYFd^_aD3Co&?3+cDqD*^wJQ)6A*v2)(nCKhBVo@ye-W7ND1TGTq!-QRrM{C2ta<)TNj3lh4pg z;;4KIlV;|JT8brW+x*;(@eg5Thsl)tXipT#e09bIT4aqbmVBn)9z$3lu<9TM6cVTF zULchD0~O4*e7Iq&d2n$`9ozwh`^p=!PWUrpY7w$YhfSf$%)ZlXPq?-8k+|#^G43>RAY47Y>N$l6uCro)16_PqMJFl-ffv z8u68rfT~tRBdF1?+niX)9f!{jo!Fo(pmGcBD7tUJ>LDTT1Rbw^yc)Ur@wBR%I5&yJ z{(ejFOYkNwp!R@KMKTe3Wuyn$_zl93Mcq2jPOw@I@|8KnlNQ~ERQrjkohq^hE*mB@}Nj=z| z)%s*+r-gxc=nZc*US_)))?h`R6C+PCkOXsnv0&-XC@=)gH$gvh5PXU58ccdL9o^S@ z8*z?+Pu#Glg*tbGN+y)W{@?uL9r~$KaFI0EC4<7zJ!5 z2WYOPrTP`dm}Af-y@Yhn&_bGFP#t z;xDkW#s*{wc=d$1a*KipO^oi*Yyx9h5c&IP_N=Nm{G$Z5wmvvxQkyO4h9_yYTZxEw zVYOS4o{&yeeUZ!we8an7avC9e#&dF$dP=x<2Fn-<#(zxOfHYGq0^#DwF9iSDeG_j+ zu@hIZmj53Mp4zKupc4`VBs{SXn+H(eaK_cZ{=(OqU%%sa+Z?RU*x;7Z84Is&f>1U^ z-EVV@GvTp1O#2NjwK5;xb!)9rrS3RcttbiI3R)b@p42is&;KLa%Azb`*Z&5bg?Hcv z^JW!OKh6K!v%4GlNNZ5J1i$yRd-wbEd-qn)$F(~Ea`Sy#ewR4QsxOeUG6i6?{IPHGe=u&ETdAU$6RDCOwoc@XJM$g(gnq@LseCq-(?+;Gpsb;iEsin z2rB(a1N~CQevmNFu@f~>t=XH3^1|Gm(5rFVjJ0U+9zRg+0co&u0}nrA{Zb-0avGk0 za1!cVC^2)QfQWdKgLaD7WeMmrv$UK|n%;F=aIt)X+KJSt+{%sQvR+l7S>9GBxK#et z6caoIXHVAKnC9w!U`THv8HS=8xkH2=RBqSxQYPX)#Gqc~x8`!{)XqrR4&t(tF`J=J zdn!89I@hHX|I~`qW-hGZNKC(-ETCt(&(UI#VRU|Q>Z?(&l5tvXI06jwiD;9V7MLuG zBqR>Z1(C31gB>-0ir+qlmR8uWr3YM~Rpe0O3$U23w=}Hg>LWjfpG7w8(wf~lt=(Z@ z?SI#6*y_*feTtjQ-|nwE-?e*r{V-c#+ewVfqgwYN6R7A{DzU=RSSohiP68roYfF7J>Yq%>Fu)Nxs_aMh@4-z zF;U9`YFcKJ|5e!C&X7Z`@+_A5xiyag3<)EOzSC|q~rx(0;`*{slG#arQk z$zk=iN|H;KC$MGL0EqvM27BgHn#FmgSE6+kL4yTI{hDyVwn54*?l-1C~S~axb;VH zgOXwl_A5dR&OA^(;qA{}*lHRyTMG@4-L6M6XfB(Z0ZX)1M~xS@Ki7*U(G9koV(OlJ zOc(9<`O^Dt%%b;Wd6GscJj!Ryq~fF#ZCd+BDXLzDRsxSrs*Ulewjs4W+deGJmHB$=^mS&M4zvoX#Pmc>kR*opC&(d@%u|s=`NOmH zV~=ILgWt4`VAm>wSMD4v?}S@)rUen{Z0+y23}72Eh!A10K)0j)k2vg;A?nke4CD`C zCf*2zAOj?SY~;(N{O`}XgQcTlEb%*}A?$*@En%mN-8jaPVG4So!*7wyfCI`Byju>N9e+d1`zcwfWx2_$sbz(^F_9?iM0XIXWb`sB4@eYxDOyqBUty zY(Tu7RLci;V8n89*9#2-gYku`y~T>_W@a?G3EO6BH01#s8Q5@f5BULy1He2SOp{Tw z+1uit^0!R&p@y|9n>{%k$Ia!>q2CS-$J5RrgTF0_Q~)wnk%qrKx5tJc^R0f1t4YqX zj+QS(p7hjhp)!*bdla5&Y=mXtC$j;qlIBa5(6g1UxL20`Txi`vujYsd&b_rQJx zr6o^Hgn=_ANECjAElQj%>f2XeJipAq3rdIhMjRrX3m)aU?>z{ZO7t!G*K17W^DJo} z{;Ut!mQG#A!yl*nL9n+oB5bcw6BR_>{A~}{S7#sb0D=$Hb+4bswY@xHG2IGINl|vJ z8)#)0!gQ8vRMT;OF&q|au?n?{J26r|ISR?p@bOIW3d{m%N(6k5$p637oBcEf?)8rj zg#4oe|C`lJh8-mNJZJ;Q`;m9h5-h@6ggs(cy+; z@IoW})EH6nNFM7EKT9gkt5B)@c@1S)8Ve7cr>oq7ug_c5UN{W2hyC!VQW%OK)WkJJ zK|xVPngWq|P&4GR)M4KhrA_sQVdG25CneL&uyongG29n;f|n-u0jG7UOY>-1BRiV` z!`XSL>_e zz3DvC{0l1L%^Pp>oxYv?EsxTU%cs$;X`~C^dFHr3REPB{>rBgd%QED{w{kpAvO`~s z(c66N_>FR=O0}&NsD+%^qIq84B+YT#8qf}|a!<{CgX_8#^%b31(xA&kEup>092$Uc zL>ZOz7$cg&yb4hwzgX@h!PE@Vq)kuSRhf(ZR#1CUY#k(dg(})li(bn#lBb`Fe!~IY z)2>3ZZ7e^;X9)+!0St{~n5DLjAhj20+#;|BRF$6h(;VLd{zi8(JfaAHBWt`gxkS7|sE_X69?@Kvy# zt|o7RZDHKl)1MFX_|_7#wm;OE9+W6}m+T&fk-AzCP!qy;q#fh9xy{7eNj)K-*nSOC zg#?lWVr&6d2M|@xQM_?np>QkPF4V+EEbv|PVkJhCvH(7)E7)HG zbnl1vYrkxN7{h>i7||L**b1MoLc%q`ucl!o#EJ0R`d6{chtE~juv%p@%$Lahw#cZ7 zYf9eYgg%qa{IgZ!eciPY^>-(m%m#1Q0MNYt<9oev;X8sl!}~z>tJyd*qPk6HNV`*> zp*D453+9iK5=~#RM_>Sl*FUSDahuNFUg=i7b1Jgc`xXJ>T9rx1cxt%gaIZscGxC|L zO+hRg%&#`W&Lj8m)vw)0kbzLuQDB=S(9tt*)X)tER=Qq7ie5`Fca(4JUf=LNUy67?YD^!BFn1E8y03>2(LNN{ zolg3ARH2tG*{b~zX?;Aq|2P6mO5H;mTbV>+ zh+tk=XQNGC7ti8S`jJYu_*!FahYb;|UKSKR3C#vdxL8#+DET9NAZQMPbK1OlC<#*` z2bbGw7~*MYVTkHH+{K&_A3^*(>#Rw%I0|T`h`A3LC(24_As~KZs!K@7vEO&~#2m}d^}nTH1e{uCGQmGZiHrc!M}0)_|w-iEgi%fo9^Lp6!0 z!rOV!hmd~FX*rujS`&F+|W?LW-d7%Vlz2!7MtSeA)hN z&xG%-Eqlr>@`XjKI2aM-uL%L;u4s~)TV=pV^);)ZsgL(b)+5*pKCr^4ZdPaEo7 zVr)HSK!r|`ApFX%HauyY)_`1_(ty(DXo_pS^}-FuM|KYF1H-_`4qj*zm(=93l6g!B zu$v3>w|H;o1<^4EHi;wQ+M9 z>gsy_h;a!z7W-)o6NNAl-V$oKqZhlwU=-B+IiyDyc+B8by&; znQe|FAfruHm!dA7cfOhusnJ`3i0Nk#sRYTe0}%=j;ul5>s$A7j^W`4HR!IECkHn&h zXT@PkD^L3QRo4j3+HiPd(g`KqnlQPH)u;`$Ihu9_}=_|=S4oyMvP zO(PNsCgPL7)2y|+osKzCu%(8r4k`*_^lYO#m1VBV%}bWsV(__4-Fgm-*^M>ZTmJ-1 zZfWp_QJrx~?N(rKGI8mys&3%G08dVmGTK)SMsMe^j#ifuk+C$YDQ2BL4o(g@TW-DVIPCiN>@q3i9z4bUBb0;0D5Bu-}Ud1bist%9Nrv;>q8x=bKyAWtVBc^$hI zuR1X};(x;E&7Sr*+(c~msfLdWRb+5^++sI!23~G19aQm99v`iBby~nz0vf3Ur5GH; zg}<{b_Mr)ED{_^vwjg_dr^nBKtS!tUc%E>w&vQq%e&-P^J@dh=9#z!|sE?GWTIdtT zi_r5jf*V1+qn%FQc=@I|==zrQfsu6`)fJT`yoSxgZ;ySxUn7Yc#as0^E49W2=JCmoaK48zf8MsEIa)AUUkcq?Z0uk{~5@@`ZGTBMA!y1 zI-6;^3^lh)?A|93cgxeuGejQZX0Ab5dsXCxR? zLn+W4%fr!(fk^{u?p;SWxfAR9lC9OtJnf^Q&}RPYdXIhg=gd1e0+4=Of5^$F59Apk zP1IxPeDl~HA$k(owfQ-r+x?k{ny0&t~}PXb|wqf6bVhSCYZG)i%mHZK~`Se?A_MN_|MPf#drRlJ}J zPn0N%BzaO#D&#NEBtlf{_q2MV__b@TYImEXL~5(`taAnriTu5zrrSH$;SM)L{Vr?t z8n?sakB-`A3PU?)r3O=3dbJHrxT5BAZ>_B@;m($a5v>_O0aT6PTnfTI&01+3Fu$4O z!wD-Gk^bzX*o;u$^rzPU~}OOIu7P5s`E(9D|t}c@d>OY9J+;gW%o)AA0*LB#SweP^?#RG&GPd1vq*xW9%DcuL!i8g5-uh z_|4JEN6(JhyCE+6YH@mr_wFr39lxC$D*K;cNB2wUBA;0GtE(MgY+h`hnqLB_b-Dc~ zVAdWF>nf)j4qCl4w$St_AGfulo2jd0jC^5ml~>y?0DaYvk~*t0E6~gtCGpBy`e6EJ zLx1qMnj7ALY7z42&%`z-&>5~&^%7rDjqGN#seF5e(o}>sh(d=eO53>EZ;OV6Di#8X zr`GD%-*g5*98YfCVRJ_G-~*q1I)7!QNLb1P(GPsrJbC%3Z$jzOqt|P7L+@_2NFX32|B3A&GN1tNxEknxbJ*so z?%EtTDa7RFan30kly$T+f;eHNu97mw<*i4Uq`+M0-JECI^w{Imsmqy+DSUW`yZ->^Xy~` zY2CY0+N9OkCf3--9_l!ZcOP*e+$PodV?PAuQETdvo7LTePS^{jb4$!iH5OX<5~j*UU9cnuu z6CDkAH_JSh;&@qTUUPV!ppeIN)z+>$z0{Zz&%JjZ9Pp3(2iEuGB%d5aTZ`Yn4NvKa z%cy|j`!mb5Is@fO%&S}kVIH21tIhg`YAp1DZgUItGIv)$>=ri29Sn?_hF^s&*(=|? z*5v7%e!u7an!h&1ek`_HU=NW|K@IU1%#HIrm7Hk8ThvpTrj3kvPzC{A&$CkrEXIt4 z)Tl8SOs|1|(8>QnQNcI2m9~?{p;l%lkd!R-lc@u|1{%P^_N8Sz#eV2_9Ag-5?xZkr z*x63RE>s-Twv~3Lgy%q7GRGBf;oQxtqnDVu+chdEEhA)u#F;Dflqa^sTX)0 zmQU2szUGdhtTXnLl}E2|JhSC&FzHV0penFiTwqBQ@#+)ke^a|PLpM;mRZD@Bt9?~c zy=C(wHm1qM}$$$I8+hU26}O&vJs{T+NX_(i1LlT2jv~PK)O$)Q7~g6Ec|s z!mN^m9;Mlhjcc>(vq<8ZO+7p*EBB*9Y7cO6-1gXT*i9lssuMbKHdr5wIzsSQ%Z)g? zrL-O^Sbbn?QQKOO{aaIHX>HnY2>M#te4t`cK!C34qL4XOWUp5j4DHQwSqsF(Z@!aXZ=f#n-i-om|q>rzPW zftI=80OJkbLf&4g@^tMFp)5G8lszfpK~k`bwok)m=2|*^)u>HNd^q!_iDIL<+=RAwPMZ5#39LL7Z4dHd| z+oj{(9LE{9i{Y;;67C06*kjVCxX#NzKxYd3B#P@cr$neq@x6DXOR-O0GeBz>e71FB+lrGWBZ=r~ZkcLvD zs#Dt!JySn9J8w!NGMeZ0;!l721y^`>aoT2?y3(`=sKU32;mkUdL(aZ6r)up8i7gs* zyUzG?eW=;3(b=`%GuxI!^&O$v0cw31<%aKHpD_1gbNR&M^Uki14UZ9lRGGT7ETPe; zPSDvtl=l0y7Z{@J8%J`m4x8H8o;z=(wYtDz^+jxF$}1Sj;Mdmbz(VVtjevHZlWw9e z2UvWkCnWt?4t7w2qZy<$GeM3JCwA;4Q35aGBa>50UM_3@eXkc>ob}&l0OxY(D$ZVD zTj8+aK@rP{TCdXsc}`@^*^=61Ol%M#Tf*kY!nox{TSQS*@yTn$zI9oY*@9+QO;n6k z)owssWZ&>oW81N;es&gZqb7f+HBMsWxt7wxLg`>Ka}9p#yv1z+$ik(adNyqE-sVD?g;7r9E>YUNzGiR z2y?B8DJznEHg%7-jDHc2p=4wqO-xX;j=X=2BhELAjyOXiR(~-99PBkB6&OT?G5rlv z@sVBCJo^%qL(1F;n_@-w46>X=3HLO+nUsZbfQ5o+tjQbUyYCnkR?0+^Gw|&e6ya@^XPmI4l==d}*dV{ib@6xb z=?9N{=#Qu$y(EGX~LvWp)YKZPI2i_|ukT68+$6v{a7m{Te}0wXy7qJEC3cAgo)^=+A|d1%`2 z4D6?Uj{u{t8SRH=vBRBaP}bUuSYl%;!p;?d9UzP)Ik`SEqS$Z#h={i#{pk4u{r|n! zd$UJdhJQcon9_oP5dHVKhWbA#TPQ=-BmA!~=L5YMC}E;rCIwSd6m}Fecw$Lba56YB zH6JPcgh1TeKlo(UmCc$}ddq5a+~@s5YN{wdK$rb-H>^{ewJoYUnrk9TdcL0jFr*$L z#l>B}Z*%>5ttf1C_lt>n#hw(FJTgK{y*(jk7Cll@#cymG z2`8LQOH?!3aR^U}>wTh9V^2KwX%0{i??tV~i_&*uCp`U!(Fyd+ZlxMb;!`Ocizjn0 z_f(2%QJgR*0PW_~kXsZ)#^k??w)(?t`ufwfomkb29?7ZZV3^DDi6!b8OQ1&4f-f+V&x|FDo`rPrjMND0rqjESLHh6J++>B~>UX1kndBenv^cbg+oR@oH zK24G=1_zCussl>VCi>dMT}Q=gK2f0u^Y(G(-KiW-KxOC#uTKk-w>*39#148&_sQOU zk$?f!Yxgjw$5;^7+(#av{XT5*YyNO%S5u6JjB~v0NBl6R&4&r2n5lglKmz4Qqx*Mq zLf}N^@E!&GQy9%Dd^nXEIZ+=QA#?674>>ZOiDD-pBG`*xc0f;LsP zR!_SH@Ih9QU*a|iR6=3xmm_CWXGQv%F|pXgixQ(HzN&2neozw1XyOD#Dko>2WM!&G zwyrvl#)cDMJ~mu7H*YL1Q?W=Q?}bBxJJ1(4*UL(=Fgy+s?%FqRtT|;y#H5OeHhRE} zubI&e6)K)q{n+P0SJ$VD^*zJv@zI>FW%WBQ5Ydt#i$sIq1+u$20@44?hG5~J9QLko zI6GN3+-op%O_0JML1Ku3#e0#q&4)oA@-Uq>Wq{3YmPMtBar6&WZKj6AyFfzhtZrbtfb!|;-Ya{gS=S+ zc?=9Yv;{&X-`HR3tdgrV)x+uA&vdLaqODjgmIg&+0|WZs0-D648An{yuB}S2LzC`a z_)C9jp-#er&Y>x7@m|2Zo(o3$h@{k^Hj2dzv37%6_Zj>0e!ne2=4Gg*O!|=BKCooh zN$32vBW>BBMndY zN+yPM*XyiTlI63R^6}8o!)>zHgj8XVTSxl=e=V})<0_4&XM+8Bfr(2$K8?hPw87S` zd|+Vh0#(2XN>Noa)->J^PdjCqS5L2(H$j4owVtFed`cV0|HMYO1%i=Q|7Mv73@ukS z5+gy(fL)<#BBrQ9B>f0^dWdXEDYAd@zT&H)Z1Efw*Vuc3%=`#SP2tFVyf7&kNcZ%G z<|h+zFr+uNfo)(vuWmzD#fZm`94ve+X?XWDw~5rUPlvZ2LT!>m+ zw9sDAahcA;9u`(N>=cg$Gmy&zsBJZG%hoiIyZWAx;bck8wb85V1Y}Ix7o$Qz>(f?w zJ8L+ak^|UMm?w#XM$DA$#2m&eX`BWl+ z4vzYPr#uoa$+{O(p}$&C5rCAycgH)3A65+$6uY8s+OcV-Xfl&;iFFZKo)ze68!1#0pf5shBqzXHB@&WH zR)q~(`66jp{fD1d1tHf9D>Rn%!@ODkdYDoIuE_cONsX4O zdQ=8fvj6nP3x~tq1H4r?3{ex|QsA`v?H2dWz=msg>a|GGm~McjPtP<@o8Ae+cKJPn zpJ0Z3HX#~9&AcZ^SGeq)uZI?|ErhT+O#w@`XM*u)uD)+YC)L59+_6Y+wJyz*&I>*QHCCBW8PYsXv>|MNrUeA!6*gs)Qr z{3&j_=O|mYlrSj_{129*=tsW2g#R?+Jt?=I#O!t9N80o|5&BPc2(3K*ArqizE;l?x z91O;q(@WR`5N^X@&u~;2wY=xR#gzrJtwKIikgem3{_J4YWl$=7uYBXjxzm{n2_Ak> z7}xAlxN8+9<+N;mB|c;CGt<#_?4PHgT<&Wzur>sUAooOrC8$b-VqT)DBxOWdR>Oo- zIse#4i3|G;*oz%7#i#07$YBhBoUxS?7HgCP!nVQyCpN~XGUs>#Uv`3rZvi-+S_CK- zkX~`{MtAzrrg?zMEik5F_0cpv6Skjw^eMT$WzpQ z%UsQyi(l%_!mICJ5HZVPZi!Nb7m-D7Zh)-fi{p#3JdXb{ZJJQT|HFVON~z1WrAQ*5 zotJOBgfaX#Kk}7tMPZ%kqB=J)ymJ1*WiPSI>(MK5o0)|Sh!=BQjoL3tfhkJMssR7W zwv`fGIj>ueJd1AuB)?-V9^yQ;nZ{YkRKiySe;-ZFjf%hbhxO}K9CeOMpHbo;FF?Ym zZw7ih3H)H}dEOYl(a)UMhYdM2^C7$;l>Q@Y6MIO1qQe(;_&}ngXQF)RirJ@sDbp2i z`?id(p-_&`?wl?o)EeKWnQYcc0#Mrs;e;^lcbg?A1#&V45!AA|=|cs6_U7#q=^`SO zntR}l3Jg|8qqeLNJvE#A`H`1Jwz6WSs4At^$xUy?-GE!@cXw{UonzB1ZI}trHk7?X zQ*zWY7hbhFURX6`B+J1v#HZ_;N*pIYZ{>PDQgd@^q_=oiNhToBBUOHH1~l7Df8(lY zD{AEO_bxvq+lt>gvmlei7S{K!?U&7p5pJtwIAvN8w~TlRNfjhkw)>K{;JtSr@c^!%o~^&7MwX5*-K}<2(Wi}R^GRgH0RsLR6GAXX(Me&=dYDS3BsT> z*oi<~NK0gt=?>f;IzlF%Hx2z&9B!OqwQtmyo7qIo<1ZDI>|TS;29yhw7tv3u{n7gu zgnS9HF2;roL`*~w0{lkLpX zy4Z)rDL!^O-#PrO19%xPxK@hMEoGXH?`S@Eq*dpu89jwExiAz6YURCb)F?uJ8Xpe?5``BfgwwYINU4FtcL|8Zb;UsQYO}K zh2ak-RcNa}&L|&LNZ3#^G^2wm=-5zxPr#nm|mR}y_|Kb&}C z+qONiZQI6)lM~w~n%K$2p4hf+XEL!hdGp>|x9a|HRe$N~{?xU5ueE<`Jq4#bdK747ABb2}&L!-}blj=k#$xrXc*}TMjM^MV$kDLQI>BpAB6vy* zIO1!pDN`8|(&CTHJemX;t~{tD%^ZzFgNlB8bg3*Vn#uMZt*(E+O}CBL$<06xAod1R89y3o`! z6pL6Z!K_)$sx)Dhx=^WG&uUi`$*3cXaL(2*Lb*VlQs_!eSPObU-C!0r(ug`_@Fx_? zhQ=8yEAD`iS}crlfC3b_lch2gqaH-t;Y5S&b{D7I5klQC9P;&S=ugKAaJItH6?OJ3kaW!4+BC+UusO=eF>+isn4xE-m6N$?@(uk@|lWy}f`b zELx9XFZM(zuKyx8F;jxf(6$1AtM64XZA+lz{vp_w0ol6UW_Qr`EUTzDf_72DeCu^X zon8G++ghI*%&--v3qz-P7wjF$*EQ?bzVQpgV0kj74sdt)1B>`5vu!QpswDaAnGvY5 z=a_uJ`p*WdYM`jukxeBB6BTq|tUDz;M(&MWkS`SKo}tWX#MLpbif)4ozthnIuwr)P z+cHZBic4$8f0oiJ!rR&AWMmksozvC)0=#AS>-;)5%p}F)U1$SYIw<{|;wf212y{k& z2vLn7$$dtV1ICBdX2Uw^{LrHNegghdpZ`Yce9%A5d#}f*r3@a_7uV&h2pE%1n8V|GElK!`^ zztW`oduB_~?RTQ#q1#pAL4)-Y753(SYN2CwoC?_QK;S@SL9-nVa(Nlp1wpJ*pbT}9 z1d;v;)zOVudE(>TLB1zX_A#7AKY76gHLEL}3$5V<8r~yV<|!wAiI(rN$Rz9x`Pf_- zmBn@kr%yC#;anc%?gCx8V91V@O%GvbBLN9H{@t5#?1&TT$)h5krd|enZHS`w7L^ra-hdb~7%xp4A>=M#*i`M?h*_!Tm+1%1yKdvPu5`kf@eV<7jD zZm6YKTB}bcuV_c4(26YSBljPe{S-%LZiI|apbc)<4`67t+!@9z`Ufsv8;l_%1|sEro7Edv3o}P1?fi}n-*AW|90Z_V#g54{ zDDSjqYY!ia_b!S_EQ@DS=F;Kt!HaDO$xc2<!^MYao z5e)uHjrx7(J;TlXh6EeM9_R?+@e#8Pjt;r&p6o`z!=AKV@`M4|sOFQQa78Z&3xxa_ zyTL@7J%uFtfDSYt+o3>fdzB%X6wJW{ zBI3ODesqKZm60DwiWsCYEcu!o0<-3vzFlPbQa)nWzoH=(r=#W=1vwjrT^kl1oB_no zeuJGk^!mWqyc_)Rfob8-Eu(zoFn(+@o0a@Jf zEmR0va+HqpQ0}Zz9IGQmNNbo0kkU1SAoU*n4~9t``kRf>+>f~K#nWDJI^f)CB~zxX zZl?bPd@1K^X5v;ycmCfhChLvV_22|Z+g}KHZV!BL7-Ll`Vmh}(8KZbBrizp9{Up?~ zwSW3eQ}mk~vsY#V3r#hg9ah&Lgb-cH`$0=N4}bmsv92qTZp|p)d?0IFr6*1WrLaO+ z%c7z4AcP@RCPAWs{Dd?yxq#m@{;+sd$h)N9G9J2!zrB4|X7WTAoE>2DppLBRrA=zm zRE`w5JY#a$f*+EW2;SL-h+3tv{wqTxc3`ieT6{|r=-X!0?;6UDn2SMyOtK~s3**`ngM*B$f+}RN-zLCO#Mj7H^kNAx%qdl@Pc;z` z0I~3}{|$kKjNb=+2n0`Swvi(At^D((#GYr4`b7RO{@8dUQwj0EWDcP=!e9;HU|^b{ zT6r1Z)>7+I>x1S6r7rQl<`$X18k7nY>8fZ9vJeK<7z(w4=YV<9b1epPP&c zTjSR^B6EHA4;j0c7DzG~RTb()fEioWPZUIo(4{J-WV3?Qu{Vho97_4MZJ~{oisdPd zG_>FWj#M9JRE8;Xqi|ilmxRzAEG~N@`I9b_{%nhL{XPgRxYx2$k5;#`P8 zrXQ#W;1CSZI0M#T#6?1Ba9PYxqmEUn;>Aj;H-cze`OXjNn~f3LZl{_I(2UtelLit zeVu|kb$ZtuQT$ggl=m?INt0FcTcEMOioYgbos(4mMU#al(SfO&He&cIn1YbWl9AwM zxPJplQ&|;4p~^k*e{)9ZwXDjbwA#~ppt0rG1Q#+8g z6~y&CaRc~0EfWJHsa{yFww{i=eWwk!b6!q>+g|9sI-7CM+D?7A8L0Xraj+$wi1=*U zbnp!ZDMJf%e}S-M+UYsT(uz11Kb<7=_tWg1#_LM<(NPHosL7vGI{8DCB5r=d3dPJw zTq6oG&tt3msT^;hW)C*936$;2(RC9HYmr6nRhsxIr1UTia1YLMOT230?4-TCuymsj zdy+pBbu;fWhu(y+7^Z-j%byFPiD`NG473;SK$c_mQ=nrd8Q|lDTVsN}0>h$W^NGD+ z7B`k$xc{#@QoLN;#zddhNqi1epV=n)li=Xv=iTJsUU5>nX7Ytni^&vjO(b6tqbdAg z=EHRNjg;mdT<2wlhRZyr)wUFv)OL&Wp+b`~rt|4noUZl!Qic4Yn~&LG4+<-uw}W~# zC-ZYwr_$dmlb{-9CLnX`Z3(@lN}v6E^_nks0M}@zZp&Tz{X8G#@GR)kE^z+Zc_Unf z?u^%LeiD~bN+hpLoo#u%<;=c>&DTgxxV8OA8o*^hsFgoEQX{BY#40~(k1MQLmLeep_{ht8LT!tz)dPwXUM+P(ULiziicmWB*ERuNtB86?9*;_E( z+XuO0`FWhd8JV0P$B%{(S_DoNHUg;E!dtMEBS| zM^G4JI5m(_-kq3L)nw>Ir&i-@?qQ7$ZZm}rR`IiU_-tvrJvvOig#~=_cMb^<5DRD* zjeKwFaryn1?*Pf}Bfnv`1PEbKpC7(6cl1;#ulrMN^^%SzM=AZm?<`QT)?Nyx_HXTGH89!C-bKuhkj%Ns1()iA7 zC&^9J%S-c2qNNb&_E8{s4n_PZGH!BN&*o6 zd`!Dngmb{~ZP*_YSxwEI+>|8Ak5BP&}>D`bf4ipK*P z8~O4N(N-D$R@u3%*n?OBA%UKvJ4}a|hP&g+=9x&OS#}NLJ}kKpgiJC~KUKKF3X^xS zUCq`o#=wB?UeBq-abZ-Mqv|=qfGY%+u*Q!QoJ_dzThwq8X?_Q<6xGPgcb=C%8duC+ zfkcvlWBB$89wD`-L2}{0NjpM`z3lRvI_Y}-jVcFRySK{vHscC~;l7D3ZJcA=qf)@I zlfy&l`$0lbnK~)3#_k0}?H|#4<%nbK0gvvP8w_Ym>2P5XD}Xrvlk|R{O-S-xbJ*(p zoq%uVEd-W}(aEBc4WG*cv<-GPayBXIyUeNE>JJ@=-?{)l!?^yrjWLHMydf$@PZ15D zbx$T1TkeVy_eHcsyh12xI0Fekcm%)sBhCxQ5qZp((R`F!RhA9s!_a$0P#VDP4XD0P3P!-+4e=`#2xxve90nLSW{7LLURoDhsvvUgwWDwLrQP;?0=pT3dxeq_7*nsVi@X>7?b{&fhbGCWayY*|^t7@`HpP0R zTKewTAioy=@BhTEi#H8##y>6@9#R7FAB)=R(VG2? z_1Kk^(t4P-G*GfMkzK1|JXBHxyk}!kKb#%|YL#-PS0lOsf91>AA1S|ElnQy23XkzM z?+8Va!p-shtoqulb_{(loT|H$dp63VXyDHZfOe;FwgSf6<{Ir`=qHPmMKR^hLOE?EnGi6 z#gN+J5b7H>BR{6h+PbS`Le;u1SLH_1pwIN2mZg0R!pN+f6(`6ti`2c?y=%Cf%s^g1O~h!JBE=UQA6`h!;Bm(Lpzzz}sHUn}!%5)k4rHkQx1_4mwjV5+KR= z^^qk>^jyxjFpn9z3g?xFo_rpKPDUyS)O>PaJ*w3-@KCI;W`-O4wV-Ksa7-6ognelb^2uf!i)aDS_+-Iv8RB%v2&@vcBeml<*! z%eKGJ?Sa_#wB=1R%jtWkvU4$Qy09V%3X z3OS$W-2!m7ue2pF5$Kx;rNx2399XLJ+5O?2(aYpnLhq095mAk%Xgll!?(xHmnqVys ze9G#h{a3s>!v(^MA_nfNoMzVBrTVcAMaMqpdHaY5FY`=1_V%C2V3v%-D|r*lN3w7H zV)gBzUaD-*%ERa=R5;rL_t6kr&+z;?XM?qHD-04UR8lLCPuR(Yf7gCu&??H0lM7Iy&+-v zPh8&|`^AKOP%U>&2mm7_@n$$vo$;N11cWJXdn4dY$s=5BI21rZ^Xl|G@OiXjuk1#X zc{EB^1RT@fhzE4IU6QrJyE5;=R@e0LZ4}e@iorBt1P} z3AvVtcvP)-IMu~z>jwlM&$!gr1Zt>*PXe6d7_lkIYE3zZ_KP10M?EoI&@(#+q=wMU z&K0{7&}AeT+1-$zw9r=VmxVHYtHL-vVoC>&`$mNkjh76M#Q&MbU&`UbBz+0;UcN#8 zk8kVsBpxsh^CVf9BO4-8zQI=U&sSrs}@#;ZK~mudNreSFujMgm$bSI?zE zrIM()DCT#t0GflpTM!rR3V!VD{Oq4=JJZcQpYQL``N2{)`u%?J2x8I$(tAWtF`MeG zZ8KAlj%mr-iuoz=B%zwyM2>e`>VLy5?~x}^&o zf7IPFU3d5wgiUeI6_yev$LC$U-1-yWP4mJ*UKo#5xohzkwJhRtM+V`mK5vPCer8^1 zJMmK^P53);>Go}1KsGkpwQ~u_m`@5f2!^{>UvR~h+7Y!d95mYH=IYkeW4}JM+|4<9 z=!1ZbD2}BzFZlkg?x40`B4?#NST^a|>c9H1Gtu4VqM#cjLv zk>dmK{ajkkst89l#|fY5M)^$j!fP0{9(4n%?Me>Rg%dbsf6Ua%Y8f!NRJzy`KGOt$ zk_JMN`H@MW2Keqa_Me2#AQ-{Ev*4Ui^DCAKjVHns${b1$g6UM!HwJQk&MeHS+xdN?xTeW)DAsdN&5>Dr=bKRxV-BN#MuEw zJ4Lrpx2tCWN$o#mVAjZN9KWFt4=}L?N$Fl|hM%_wt8D}=%#QDMH52uu})#4YIf(&#^zuJ-J? z1oKw}=L3-#vI1X#3&*&hboJy?kzn7!(M0V;!OZxfXq0Q_lTajE2n@4-ptqMqS5rWY zxLV*!cR14Xx7TM?>)CjEz;xhqNozNz<}&yNFx`}pgNP3wt*D;Q>2{|gL;&mNKWa>G3;nq zyG@av(%PX5&4VwBL_|k>27WhN0s_N}jidt8cj$h%M2215p-(sO0YpM5{V?7E$z#O( zOA%tvurtI*d-|}%2QLQ#exifaV$T@#*EW`)v7!9e7?nH&m3N)5_ON+o56mnB5~z&= zGpsMMkw7`LmV-O_0Ns9p?%w#fl|ZyPv0s6p+wfd1BsDbzh_tl|Yz4-R zT=yB7k9M>ba7MY;m9^~5?P=fra+&is7YEVaaH6dY>pYeec})jl6B_UKm4MY;e#YFw zf{e`EBDsfyb`jV33LCc`%h_`L1&9{Oi9VcC;`73YLp&4vgJv4bwRQ4fCCv9jU_$c!o$caw4T`4OMgPPbaigtUdpE{J6<>$9tN zqzEWj_v${-m-Jx4cKhZ>Z}#HoP4#cnLW3Z6@02}^%FcpgOtGz6Oq#LbXha@!YX~eU z?$v(RiZvmQ*{MF-Qweed_zg~Y3nB0RC14rJKD~K;0Ua)a1~FTMJ9Q=f&thshQQi1k zU5!%x#!;B9Hov*11h~gavTmw7>ND3Pih% z@P@VTf7HzHAYAj%5$RF^bLXiWyJfY1F|OV7`f2$6F1P4ZB?+~n*X-i#PZ&w#o<+kD zAO4Xo`G?z@i~qRh5sPk>^_FO>#<#51q(QM8Go-bkY%s`_qlY&qk3pHe?U9SBx?Zjc zw1)2Ka&T+Gm)lYrCh1lz{D{u6*#YjFNDsR`Q$5@yPeBf4|5Etn_zO;&hfhLwH_1hn zsv*F^#Gj&QfvYSF^?^cTMv^WIP4g3no9Px5>$JDQaLvCM?^qvb-U-W`<^sl?;)1fp zHmtLiZlXTyc_Ww%x>2`_P*wh_a0@58pdpVxlgiN@NYfn`#}c!SB>Y_6r398zVftjL z4$6xFh-YtU;X`fZo^jQHhLUwbz`jr;wu0?4U@|)YgDIC8Z*+(HVQ7L9 zlK=SandIYQt$Dfqc`fcU_j7gZ&dr}EoD#!STDuY*O)uSHT6v=h>@p`^C?EAM-orB> z!wE9m7eP=VPr0#DjAoFO0KQ8M8{KfD0@H7h-_?4%g#=BH{0>7c6(!?;EqGibcym_A z+1XMR9{DM>r%|O~(|1Lel)9C=&L8CYB=+(8T?{nVID$O>i&HRZD_%64?c!(?h??odg$VGn~4;2)u@b4F(CNuIEDH?!sxE3txoigq?S%) zIhJ4d#p4Nmq5-O>a%5KPQ?nm|H2zzht$%6xxfk4n`f)e_w{~N5m(=(@dHPA_7K=~# zLA=3dRnuIYThGvxK>US4C*3?=&GpSc5&ZzJXMl7McKSVCMjD{Hdj}gc+?4afQ~Sqf zlI>h%S8J9BbG`v4*IYoJRZsRKVeW_Qx~I?M@40{;s~yodrkMc7{>>H}!sBU&sl^E! zL+J+JS-EWgrgL$m>cC-d*nzupy!f!n6O;@oXRq&^ZYWj^z}5gfhhc#9Jk3V>H5x)Y z79`Uylz2>9UO+2O=G~ zk=T?WdX5Egr!q$M)(9r3Q*$b=2tEiy@!l9_QyB&q)IIHYvOfpKQ##pE)J!Hee<4(k zXIWw_mf1S9fn_>X8Da+vnSUEoA9Wf5E#pERQ$ig%0vJDsY~SjJg!Ii=Th^`YE*b&L zYp-tBe;POI`aHz4w`RE_R*&wAflvwitm1?)?#8TfS)-Yp_e!qpY4bg2lCPDxeO%FJ z9)+7SE;~WKb?9oVVG;FNmsgabf;=%D*BC>>Hf=0FfNvH&p#y1hR;R-(#?`Ay!Oi=K zzZ&%05j8$Z6Kzq+$>Jkhv4bPi=V^N$Ib5xg`l|}tQnRtINE4`PT(^~{^?!UYte={H z88k7hTkV%5hF6ce&kG-Z>?%cJCFeC!vZWPiq5M7&qAA9hJZA%z0~hcouzWp}(jP5J?R(BI?WS_<)75+C@W1Qux)!+13SN z%HCvDal|0C*)MMdfpi1`qC{i5ZRc_m`SxRc0zG#^nCHqpg_z})kyBF{y<@CM_ zc!X0oGPbJBzywupP46)3%f@;ri?|0~31XWM`92RWTG5h(79d?p9l1h&^XI6kqJf3j zl_js(V77vK+>|bNsskb}h`!tgagw5jBqKuyfYn^jL-`P^EPII64^bd(eni%fjDE}3 zuOFnP%GMQqS|$)+4*=Nw|I2-EiC+8^>#aV@aa)1QHCd)SfXD~n2C#_#o`Wf zb_Fygi^Ji_3eYz69*sKvqN{No_=vB<&zHR2PtD!pHw1$IkHYgs%WYYSRQAQRBZr3p zlQ{`Hlbb)Yc0ON@xWTUW^x(u=DNR-SNgVG?WcmsHDE64<{oYuOwm^w}4WXD-k3eIj zy!%}>^B3nHgglvRlP&~&L7d(bzs9i$7&0Q)7b&uCl=>l*>;`DOAAwB+fD-NH zG&)MTMh9%wRs#`L z4b$ExKD2km*ZRvUi-O6{50THwIdV^-;-kQuf(Fe!x9a{0O5ytyg?t6|S zf%;=hn>D3;jV{blq@PC3E5Mrlx64QvhQ!)>WzXdd=M&aNfSEi!ST667P?$1(07>jSC zJiRMybUcRx-0oVs@2RLG7zUl}0D9LqOfm(FlWNA=XsS8rO6BbpL|~aq0`9QkvUmBz zFp-ZM)>ScFPT_ki`j`8wKVrDnZ8IA1Q-7^BnC;9K?x^bZs-JLi*|SAjmOeQyL1poDV&alwxj^%=8U z;Zq&+-`~<7pu}pjy6h^l*6iYHZHbj(gh~@wj(C(=mDgOU6d;2ulo_W3C)6dKtgxh? z|6-6S#gx|_36tXouMYBjXX>s;+sW4Zraj3^X>HO&HIutnAgi5dKfcy88L$wBy)^cWLX_2`|RfA6M8MO7QXjx z3T~mk+a39PSYqTkkeqVg{}CqqUvUvW6GyntWLrVkTF(!bW1FD?x3E|jw|RB4moM3g zcy4a8$FJ{y!W4}R&S9QsbFlyY_XL~&nTr4#nnrDuck%-V#{dz`;DX%q@IVGL$c+dt zgy8>c?}DsE`at|L*USE|a-~ZiAxPi#J5a`RK@;QOAy1lF30O$FCnG*qNm=ig6uhjG=@q;EeCG6w_9M4y^j6)q zJ<@$=p=0OcvTMhG`s#~(AOxP=D8S;o7!HS_`0?xnBY9OrW4p*`S9ncEB#H7N0+DKC zq$Gz$arl@fTFC}r3z`p`X{xH~TtyDW{ERRW*dv3VO{1h#1Zv=}&i7r!hQCIqNGG~^ z%gtCIpsf!xRjw?W&&|gjrdT4$TK}A9*IA;=%nZFqHHwgx6PvP3R!jS3#U3(nudL3l zroY&*d_N_Sqv@*{hhtmNOKw;B1PtHrXU|@Sr&aqsCLOuhF9&m1HzL ziFw!9-b_-^lW8Xq2oEVLN2i-ZFfvUo$a7<=gpW!v;q&!IS@}}hBPy? zG>l3Jmx97`7BfLoeW)+us>JwbJQ~BKJpzf`b4xFzRGT@%E$7W9JL-?O*d2k2I~t7= zQF>ACn&zobp};z1bF$8yvy`dEAM~SD^fzMdmWDD{3HB1gl_6umGBA6cQ5o}Zzuuxh zZ@)})T`vthmq}ynZ_Lh+3;32OsoK@Hl3jz&5fQa(p=p|DFk)HjTF3C%1Ps4erPX$} z`wYYUfI_6hd}Ekg6_$7T{G>u}H?Lg&QU3k#xV#W0Ug>Cig;cn{`%ryHEq*q8o)!{afP6j>E_?xp zI3nb&56us~F_5!Pa)vp#38i?Cz7~I{ett{Ke`fK08q0f%I+jYFae%#Ti>jnqpAA#2 zqQ08Kp(s0Ok_3A1>*Pk>4U+VG#lL5by3l`G{h=0a=tsZ-v$sQkOnfn1hu5w4LX2Rp#Wbn9|-;X<xvF(Qu5qylyE~46@uOEj-$=as;* zH^TOH!2V|V5!`C<#{&YvF1!79Z8?;2=A)(4Kglv6Z;&=(RHT+<-A-0udKVoDinLTy z`j%sw$9N<=zPT|Te0t9_@xyb^sr`gXP4ygCX#u!SIQY@x>R%lJb*Szy{b%>>TOzws zhnAuFC+*DKNj8jvhemUwkhm1mpoFM?1p?~@)iL48Xjg@c;tH-6TrWN@dyhLKsuftvl`WrKB1%Q)&lsym~kAX*}U{?=FTZ7$cqm`7Xj4eMGW*i1`17f zuDsnq)kzYNA)=xdHny#0|4udSXJA(#2h`VcnYnke0z70V6cp zb1-@qp(`p-qDj3?y1+OU>eFGPnJMMVo;134*XC)`2K^ zqZM8?L*p|ioz7+<{q3Z)V%(>Y1T%)sCT{6rP7NVq-`yOU)2_G_$cGm0p`d`D>{#4_ zD4_-|U~<~O1#`F@ZNKPMR3@s!?MlnxEFDlY~dL$9F1ot2^jbt6=(% zL}h@;X~IZ=4_H@?j#nt|3>B zbJLO{5xad3{kRm4^s7!@P@66;@=j}e7&5%^UmnjkgqHbXV)Z}a)%t^I!G$2Kj;EcD z1Fs-(SltLXSVddK-7_aWWbm1u6fIR8T{w_<+{;l*YD*G+nBz?FWYZ0J zlI=U=@ZY1Z1kyOlGZqtv)tP15qiv96CT&?*%L>a)b4k@)&H+pnlzQ5TMU*R*KRJO3 zT7(l-JS{INzDSkruBVNIiS1?x8$A;&Lbz8#8kW*Ets226#U>8FyB)k|o?FDy`jYkb z6o2}*EQ*=U@6_WXuK6hF9@!`JI{8n(ja<t`O0Sv={v7VYRKVw$H8{c(RO z6|5;Fg}f0u+>%JX;k-cToecqmkTJy-EMvtG#kNi{l-)OrNe%7{WhO%`8B!i~r817) z^&9n)r>Eg_-AUTyxSx?8Mf4SF2B!zFn{{^pcOzAn#gex48^z)r;Ws2jm3`xI ze6&Bx+*D1B13RbZy_jf$f!~QZLxqVP?&{>q7Nq-VoJ+}r5e;k^^t5bq*7NB%fRg& zCM3E;mSYo9ED!p3F27R2k|=CRfn68Tjw*903d$hb=sb zUnGwb7xj=rKaF%hI@ZFEU=UuDvg)Rf8Vy+E`um*Cd^6G64J=PqJ3VQH5jSKtQ8^KH z*a}T3UD^E9lL#knm0`eZyP@)|qFU3|N4FC9jI+{EYf5&TR<0-55AlRYE!oDr>o&QQvlu4j{?s@Dn#(58rMI3`*UkLHNr`U zfKE6(tnY{mT~yl$0*jIw1*r!#oZdj8Y!=aKhaFYowg|aQDpWZVg+P92vs(>p z7 zA}Xn8Dzh}o{D+p6k9RI3+XVKavPC(d+%6tUK?Is_#w%|x`6_K0d2Lqu363b80(di3 zA zjJjtqS^|X>;&Q8%dudvSAw>HxT&?6s#WP|7A!662MC+hdvE-0e?v((5duT5=NZh#* z3V>?H5aH4iN}(qiZvgD0(gW>^&@tfEL=*4 z0iTOJH2XK5Va50T>^h)tF@L4%|63ITeh9Ekv_>7|P8gcPVV??QY;>Hmjryk7?dB>k z$JIw7|1zNDia1;I3uj=pr568JWhfbJID>18iU$VNG=czBRsD*8w;+>HC7^=S!VeVxbH7%&??^WI`O3Po1uun)V}#to7HZ#o{`&}Y0+Q?e*!~kh#Olp)Ky{8V6G;oyS>?+I^SO2A`-y3Zb%T& zJsUIT&^YCv|SO6j0dS-fQs8^7@J|DgdM@!^YJ{EeCyo$K7sXAt@8vlsQt7w^9 z9N^ww#9jUeJ(YMiUN8}owA}Mh7A_g&{?|xsBXhA+L4B=;Th;$Y-WD(v<%-WYCW@8L zuv)HX9k@REsc2#H{f^4oBjNMuB-bnfignVcIMM|0+JtkwUkPkDZ;q%b3$2-{bM9-N zOQ&!Px77YAnZPGlj{7==>*FDVF=*k*hb*iBV#CdD9i;S2&AzcloX<{`FXT_$XVX2{bI?$2=HTWiVP7%*;eXM1mm!; zrruO^@d^wv9Lv&b(nS+|;5PkUHwxB%5D5`t0UUBz9VGJIG&!k2^hn0d4N0!?IH<%6 zUg_>SHq^@djdwes8-saQ29CMmR1VEB?K(Qp{ySg0%{^b5BLzjLY)UhT=6=lqCvz$F z>)6l;txFCpO|@3gwiWo*Je=%fm(I(Qk9QO4Xw6-oR;|McI%3AAd1Qdi27Xgf3U8G& zLmwm{tBHE15J97DAY7WG_)BIp6J|&S+jRH@KojoZE2dKO8*do31V}q5@cJ}x7H)jM zNWK39Ck9Z(qOJMa(l!64SRq-sBGlffD7^qqQa=e8U#7Mf@DoA^FP~WDSzz3f z=MkT-1+C3eqnxrGgf8n$qA4KR&sSwFDaaJ2NKIyB{#9el>G3xWoI@+uH_I3ZKUqq# zt{J`h;i%HjWsaAlNyeFv8{fP1u(oHL;ZOm!6x)4k4}frsWNj&tBD`1PPB$yiRaD>j zlSmJQaCm5J;pHyit<><{rwDsM`rQ^mKGs(}OoQ1*%b=XU0?Uw*QnnRu`73_xr=~66 zM`rUfvz+%V65y4c{-uGfZw3dhR%VsaBmoTS-@7n{L%?N;qbat%!+{Ac@`V7yfe9}% z_RR$gBqv6TA-ly;lN~@YVVofs>w8Y@cYRu*8D-7xl3~ybI`~Bs0|0+VsIysVxI~+@ z2(_ARy-vNfyGef7RiCT~MkQ!?;VoslOheBsGY@Lw{of zr1IN3+Lq>w&$>X*t?>J@<9gN5XyXS@?`0R$0TKa*_L>dNdT)LsBY7!eWec12Pca{$ zehj^5RvH?A+!+>})3_Ic(}Z?RWUN@^*T`HIxGQ-5Z0uX2trptK&dCDux8Jf7ihOT= z8BLN?pZn#pPtM}~Z)`%n5o|*JA{Cgp#0TiOQoJ~Xg1?H3)P^jZHpc5LZP=HAhe0b%i|2k|2-)X^C)hISK@&ft{ZwW$;->C8f6i%x3f*XYu+ zwzn!fnqrS#Wme|sVi`ixl9ykm`#K_Ly6Yd_oUvlwY#aW?BFZhpAq~IbrrT*CZJo(C zWAmrKKOwX$hD|daRu~MnB|TkNVjV1A1HMy~5!2C&opMU&m)spr6ZD>38R%?%y8EXY zO11MbMYQD*50@e^g8%W3bt@E(CwQ(PTV( za1*5!DQ{WZIJvmXm$P8Mjd20&!b6z+DN2GWhtCqhma6GnT32%OCqz^@s{bBTiF34h zYOOKdHJ$TotBoBX;92S@U7I{%Z4fvyG2R;1o(#RIVsk?Gx*jH42&S+%jEWIDgabc7 zjiN0aUWkejktInImL-LPs0A&lj8;Qtdzw*M4;mgyJl$)=CauTEG$jUB3rQYN5}#V$=t?n_%wNtX@HQ4&`FE(?1G+-Mo?AUh3+fym=*7u>d&)qUXD`-63vY zZ+U~5Sv~ztp53IoMqHM0#=9K}hC1&(ydSx$WAH~t_WeILS%2!zsQe zJzedKy5JD*!?q8-T>|0g1kZn>i`V0A9d0}{ea|-@Gu5pX8GP~}pF8D^)NQ~fkQgFT zuRn58hyUA`jjr9AYa;Y^s!n~(hO&>o&j&Bz_XjUcP@;S9+`zB^B`KG^yV)c2C+(@; z8w+}tX(^}xPC~>eX?G-qnpriJ@!%i6SqBUNr2i0DH4bME2LuvJHCq8x2GRK=wzZ3R zRt)SB31r3UcnDJ{+d|kBVI-9nc&5!7F`IadcJcL59PEiCruKmUt``^M)y%1$rxov( zB-ThxtA*cZu9^@P0@oF(BY2`-_T|P$oM}SR;Jd-Q{IT;>h4@Fdhc3se?R}Mk8HTW zSe8EJ4?}}H|0nuDJ9@dl$!6I-giPk!kR42U6`%mL0%fbM_Gc-#S zCLtV#D}{=DB1h$H2A^u0iUlQQ@=jXJ4pz{ftxpR1cA5!}_tpq=tE+;}&lU?d9HkJJ z-LkO{3$D0(WwEwNc!>*k^$lk4i!@4>zUS*V2IJl-WPfB(OTv`XUKb1v(G&jo#3yDu z_gPISXc*vVSJG-VtmQjX4BRTK?>lj0Vk`;^V?C>sIE)o&)>}Y}0rN88>Mp}-vzVF! z$_{bd4=4aR4x!FyWUC%x&n^?kBns4jQnWJebK~UeajE-L>$4RSIziS>flq-i8{DeJ zYP<9rmCh#I3mqi++_FePm=a8xUrNt@zR~j+j0LP98hXwwOtw9$KlO$C(v-gsF}mbn zLi8S6N_f9#BF8v<0GEL~hYS%kSh~^=dUZcmz6yg$Sm0?{S4Z^iW@S zlx4*S0+;GbbCA&wj2<|=teVvx!s+J#hm~S@wejnAmMriTzI!Ma68 zoJfE~4)NLG1Jm#Z>OzTwbh;V{arnNR@Y4i5QkQtIB!OnuZ|hon&h-)1fv!NSj+w2G``=%VhvHjlK$qaf)UeH1mA_$|b4ZL&mCSJZZ zX;Bhl!pCWHJVYs>$SQ;#(qbVBJPU=R!b}4=M-zwn+mEm=1ya4|HFshZXT0+c&nGbW z<&aagx`a)!s6}i684At>@u}Ms)=N5ND%S|c&}Sggs!3yUz2?Y&C)pK3&0m6dgI$Re z0zG`nk&eaq2eoDw>@0IdV0od^Bzk(U@axIm*UH+98jzI^gSKmdE(v0QKdWl~r<(JX z^!8+<*uGt?rncV35Oq zN*Ke4Fz`O*-EW<6@t$ZsnK0M@fYBIphVt}t9JG*X?cfQ5vC8EL zmDQcp`WQn!$Lt$hGyu%$DP8eQOZh66u=);oA-XdR0=wO;KWqRja)_w6MTe&5-nsma z>yo5XBgJN4JaCE3gSNUZ(J`UT#;Vk6;cypio6ABy`^yy~5tEMb95h>4iq2p#b(q9{ z48__k&kr+UcjzwdGVNX?jj;9V0{LS_T0*|jv42fJ_yA;lX#!ZsQ?EiG_9!=9MRx9m zP&MO))X~eA*K%BBl8#D(@D?6!vQt z$VGOPzV2K<1aX7Zsn>1s0qNDU+#0ET*O?2}p*yB~<_Ao1S)1;|t!ppL&3A_ZA*j=hyl$Y;k zP$xwKa$+V0(lv;oG%5K6C!{{8=bEbI;+d%|$L!3B%qj_bF9=DoxOaC_Y-}VMmL?M=LvsAG<&nT?w*i?msJRSxMLcjlb-AeoBQ4r+U~|#Az5ln(ZA*B<#3l z3WFh=*!BRMy}8UnViWlbA^_Il>5U{>kHxPTl}@~g5BQn3s`5?iE)!nkF5{pzO&5%G2bBwDfDPnW8QQCNx>fCTB+$gpb42nLX2s2#zH!tG-Wzn3td}0%TFIE{Z z&|(7Br)<)c7CG#Q>hc*5-1t7LekR8{g}7xroIT5&dTPgFMc+Hw1w%*2^BnlxhpY(j zs1` z=hy;2r?pN$AB67RU>%0rRLbI`SLmwSB;r?%dV73-wb{zc>flcuR#t$>uS6fBkn%Ol z6lW?gQj0BLw59HNMY2SpgJ=c%LZdAZ=@)E$l}Bz+@s8|Ko_KLHXl=K60b}dC?ku*F zp$xc0%qv9Af-~H4o)*Uuu;TDLG_0#pWuLy8;YR)vdPY}(tAZ~Oe_;_l$I?hI@Dnl{9PUF8;!lFo|v`D!BfB> zS_9KWCW$}vAtS%UHoEsXsl$YO>3ktOMPElV3scrR-j4A3I385M0LxJjQ&m zDX5)#(si;n3>C;n`Z={=jtN$8kT1fI$(REs#Ox|(T11X0<``n$c$vvCDOXTf!Ow;> z_&LLSMj!u=p=Tpp?v;YH9g0Cal01J5y)qRCp!z`$Uj#$2UZ?($s-I#`y3=+RqaeI| zJE{QSRj+%V?r?1=X6#0 zsT0C6T1kj)@qEj;M{KNzO6;ayeL9oX6t7JS(VqSkAv77}CvD7e4F3%DARks=c8Ze% zXz9uMFI0tTIZN72_f|JR(+ksIO>94{GBNLB~D>W5i%|cE*cr%<1j%A3KFc?%V zqe)iXvYOT!V^)kke5K+t)4l2jR1JM8KsPKhXd1C-HM$^Vy{y2)9fo+FTJu?jn&>e5 zj_yRYj6D;A-w0f8(7TkKfVl%cbb7XE0e~p7G)*VR$h+qM^zuba$gp1_FNc z)*n-hBotA#4Y$bS2Ji&mRTHnDv8rj9qT$YK5nCF#lGG|T-6G4i9W)dtLlUW5H4?X!cA^`KokJTq;PDn)egcYheK1D8exdrD&D>^|19JGz_8|G;Fm) z{b!e}EFB%7cA|)`f+Fa#QfH~K0~y(XU`CsN7Z5p{hJz}vjQG-K%WdY=wsOoQ zN!&+$&P?$pf=cM)S)tSETA*PDn6UdP$itJ}!u<5*;pa2;M_vYxe0(*$2rDGqq+niP z=w~A=GfWpe^`zhy#$I1M5Aovse%x`KU3p{9M+%ZP&Xu@=>*^UcSbNE^;}DnrdJZCwyjiwYKO%kHAk zehOI8MuWavUhc|HUO3CHpdBfZu?nd6AIrtNeCB=C3$0q1l=?qO8iJkI*Cs z7ai{b`x^Qse10LuZIW#Z!Pk}+z~~t3 z(@mAS3cUFm-SsOk)q}9v8ip_1P-Xx&6jxgYFyMw*?m4Xd1;Z`$Soo>2l5iz*yd}1v zf1H^7>s{dDM=VK!@NN&qhOx;F^VYuGt8=dcBOi_Y6{g(2x#RR=K5rt_=xL?2JYwy< zh-n^~kqFwE^u!K*Zx=sPyTs(&`T-u%?liu4(`iCrE2*EK*%)QcfG?!Qxj@QzBN@ba z0DwcWholNz72PatR-81%by>1rp2p5_>Mqoj$t^sV8RrGu?I*02k$Ym!loRX;eP}H6 zmvOyg6@O9ZA1K=L$pTk6%|koTesoC@&2;wDeS>h|Kt zLi6(a1z%(WY~>Y*MV5p!VmH{_6#Y9G6O*%9%WEKUGrM~fd z_x9z~u37ZZkW5|zCJ^>9APHP_5H^5;REMq4G*oX7WbF#Pr2f!Xl+E(-sVeZ4wdi zWbv?Y6OWo~!tfDgnL0;uH398@QAVZaZSynpQ`x)1*gkK)LT&WLc-p#~i`JaVwgL!Q zOx{G zA#o@pitI0^-fo#WN$%$N3DYEJ@bJ0RK$1fYKNoytOx~TnKY|K91c9}Vteyl8#O?~4Ppf8 zcv7hLOR8}`jHqTN2MZt)QS8BE9kKjZtC&_|Uym_PtGzCw+Sgxj%jg5n8CxP(&X!B3 zw~<|M1O>Bhj6V{U!85_g&6DU*$>`!zz+-QoNpIogv(skjiF|S;hu(yiBuS0xpMiTp z40-ydc|j2Q0R8C!tE`Uow!AUlTq!TE3^bs)-^iDhNSB@PY+cZ%1Bx@V-{C+tY`XVOu%UZ* zo|N{BHxbpkP-nasRa2BV#M~%VS@zndU1=X9UCrHzpM7yO4`sMVSwGPa zz+RnGw2v|TIlkH1GEW99PtySLy1la5&evbE^hJrI>of%0dh_eg>uqjxW8E)#WDMWo zG@spO7acemk1=cG5<{?z9MW%E+5ixIKXQD6(j04^zQe6VWwdEBtU=$hU|np=8*C1f zLDqKzwBA0{8XlmYquJN))_sp5X?uj|2XJd5E<-u}oMIfa6Gbi?;c=7~DaCT(!uFp0SZJ+EkpT0)J>Z z7zXUcW{8n_&ptGck#mfh!$6dmSSYBt-RzXHOU34^+7oHzB8lXoV_zE#{Dq(_hY0}* z()5Rg1$n56fJK8@rEGyL!yed`Ck;?g10CV^s~@5=s(%NIa)E%Vl1PaHn@bi>R#c>c zp&R17$wl%0=!bdgvTai5Cg6fZ!&lww9LDoEJ)s@f8 z&oA{}zaCHc!Ms*AQF>Ye-^#mA(ov5VvZ!s7R2$;4tLVjS;|1y6w3JA(4xj)4y=!%+ zRS{r858s;HJeIb1>-5djw4L z0bMRw%`vN=_CGa=NL_T3rWoDP)K92Y>`+Uab~PwCB#0;xTi7a2>R+V0g`U@`6>fi# z7`85I_}mtEUtAQ0%?o8;>9PR8cqGBO$^Jw!{SE!G^`yOl*xswyt1q&{ka|Yct^h7m}D?575o^ zCAiV(yyMmLa^?D1pbNB>ozSCHR*lmh_YVyRx%;@lt~tWZR#4OZTgF0+aTJ=Dqw~zg zzD*8A^|h^Jk1Vum6MFkT9}MfgG{S`sVR3ZKX} zAOO!67Qrd(Y>`PB+rnx9Tpci8VQ(o42a8E&q5;99LLx>vQ)3;uaejTYd^2T`INxx; z8AKqYccN*@zQ|&r=O5@~B2Gn6~G+Nrg2W-bx3iJOq^LpHLFjtlmCm1s-Hhsr;xx&LUe@^(CCi3ER~e$J6H;f zb%XA7Q(jdlo}A1#9W?^J08K0XKvo=MHh}!iJy!j?YhXg%#ag4!k#dqZbPA-NGu<(6 zoP5DgZZv8!k`H`hk=RZP!NdBMR?nlLp-&bUjFX|GhfELnM+Dd3_CH7|)>4E92Bt#y zTa?Txha6Zl2Mx&5@iI)BYy6cpda2OiYa0Moz<`Q2Bnw6=stq9yB?A^@sNG9?FAW`< z&clwn9V_?2I&Z!R%ErcubI$rm60Bg6u4-bwW1(YVSHrKPuessO&C9P+mE_FhjGH}O z1{(cQ>h!D28Sj3!%PDWC&uMD&M7txwHwG_8Y<@>s&JuuzOMkD~0VgDtzyYTO#X#>{ zC){nWULdtTt9;rvdjfrHXc|?hoPM@88-r5l2<#jvbWu{od&&*!0)^-l)=-Qz9BLCk zHpOS6YP`;To&oWa%p)M2wRyfZ)#vmfA=*MF=WL~79JV=eR`0oSiBOCT8j)(HaSP@d zb+vpRSq*?%F=P^bD`wf}!tA#jR|20Cam#Sc2j$V{*hbi*d>ob=K5r%b)BEo(27FHsp(D$hl?u{_{0iG1rgYI45%>vTCk`dJR)EWwS#;oN?vrKO(?%QBS zcM7xv*a?;h-LV!R^I7?qagclM#FkrjiY#;x;3dt{*GXwgk^7BTmvt&*nto6&tF{T+ zOJyykk@7>CT#ZU}Su(6I*2Z|Bzz4slG5q{h5UufX3?EIFH`m>R#M zjdpU4od%07|M0khczVH`JY`c4KgYPWM90*3T+?p^bDZ?`a6c60la~P)k^A4Qh;n5e zd;p*z!t6vrRwYhfzqzfGj;g%cS41LgYg(Fei&T#9q?;yytwa_N`r%2!YoqXSU8jy+`iY5!$BQ|6R5kE4H7uzL+x5kalH@T@HFhdC=ZDQ=3p_zCc5S{DAUN=Zqb|4;{?rI# zz8!d&r*nPMFcR;sFNmkEiKH_d|JKLZfDTm_EdZ+=>6TJcWe7}Use5%cP?KG$85zJi zUmxKFldqE9#BUn56k~mw^D8IY6)sx34{dIOn1n0^PA$eg6A{~VoB;B^^%j2GycM8hS4_Lj3))CEHS_PLYAn?0VG%1ur|BLZ?Po zn(9*H%qc~?9y+X>qgotctkm2YUkQMDuy>hEu(E}W&)HV*nslplU$ULG;h`vzD&T2&$*+q zdztoy{n28_hpg#4aD2u&dh526GV#Ioo<~R~8nxmoGdODP%rzznHt`uDm*EoUIYbWb zm0InoOk8%zKFXj1aoXyKOhNVso_)G;gwSr@TmhNLv=Z?xDWYe! z5EKep&J4!`LI@sP_6P>b1wMcaN@L9ptW-#2yVd^qw{lepYgiqD)o}1ILA&5=X%f+r zayQb{+zB=sE?K$WlPk0~d8IAY+~1+)?2|G)>$RQhj~l;ZwI;L7`~pQ*SFkT>qo-%Q z&s-%fC2*a&V$(cvquy$%bDl{?Xij?XoVsd1w07wwcbdSgU8i(|t|SY<8jwZDKqyJo zxs+(tLiHO;eEjZjT&%A(VvT9gD>N@2(K^gHGm-{jWtmcO{Ud)$dNvUzX?h2`Sej)v zzfQ_>A?VUfu7eDQfwz?4BL2Afd--Jzr2+7f@t&`Afcz3^yPYyDso@2NhoqG8zSuV^ zSr_Y0UF1@;AG|thnF|@9&EgX7rn&Jb?scm&Ctxm<>dS9s@kXtO|0G2ml1(s-ru|p> zVB)}T1frUC!PL7FP*?_Xh=Tmd?#}HEhWl9OP2gn`T5zg92f1=s~S8-T3ZJau{F46 z84!Yd5zvn91w!76gd|47FYYxZOp^_aB{4f*X**mp4A}cd40wF z4TtDu6W}N_unjs}pT8`l?5@|)a+a-rDXN%F)Y^s2;D^(i2Z zuj3=z56~+8lJy)S8z9_}Gsmi90@1H*cL?_9*gxdIzJGbgsC@>hCPbWJq`wTYVu{d;cP!}ioEIFD z%3lG9rllRz1oXtNVjip(XHQIdUVhb7PApkIDO^UJsv8#?bbA0B!L8j2~IYO|V#cgv)&%P_Y-pR%IS{&Zp~bhPdju|nhi8Q>l34Z$ZCfv;nhQ}ng~$3P&_ z8za?J!_Tvv&_Zygq~mZ_l=S^p61QpC`yD$z`NNOx(k-NDibR{z3h5jLiii8w8k5fc zO^YK~i%jdIh}rX&g#~_M$|t~_?CT%ofsyrALDX(!WRC%om-90=`u3Xm@P6=hEQ0cQ z@!kP(!vJZ6ePHP2f;0a-0TNYbabtPOuT=&r zE%At(4bFTs&Ycg$!+yHuoe^gFcL#wvKVXN$Eul3trwFWw5!N<+@0*(NY6lO)`4pjb z)jm;ZV?JsQC7fOJd89iHUB*horyu(U!&c(@s0pAn$(G_B6RgC)YHM*fXjJWywr7!` zmy^UzrtsCyHd=t=f$W^&ZOD}@7?)`>h)xjT2HEq&ho${< zMc`QqD$?!%OJX%_^M)T{0VK?KLe61qU*ZCQjX<)$5_dmAe92SxQ=omc;Ku|(oP|Q| z0dCi!p3EDMolFRxuq1!vh>4T_V7%&$m6!;MmTX8MN0)1-#rJM|`KlYfB&b3f^@h-m z^Pj1YbYnh5=}c5J7-O#4#cn=mg&0S-o@%bZoW9v08WK0u&&XueFnt2CKMkl5BCOj1 z!?vBbIK{h@8z#vN4#rZIg#Mr6KbTm`l8dx9*TGO?sDCn;k8zgJTaSUCQ!2f<_ggC! z_~_foY{jI}wPx+@9aO^COrq6V@dIUVn(jr_9dkY{oop;vnw`?wyT`6|dd?>wA?kBf zVR?r3ndpU~H!YbO@lteX6+1g!8*BYGz>1KRE<0NSYNZipLOd-}a@LqobKAM;2=yEE zaWCe9IJ$keWN{Bw|C+f#B%>#h);7#yB=HX2Q)CjcA^uCW`BlC|Xjh8UA`|))U5F)6 z^*A1(Rt*m|QDKpiA0@%PVFal~_f%4yNK8Ka`&rK-BXho*{y^g;lYNrG4%NLNz&=@c z+x;HlG@iYMo;vLCvvDW>{p?6o^$uzivLRbXfEa;bB&zp#u6atm><0qApf-gr?hD2= zv>xMWB2lKsxpy1oq0Xjf=AM^C?oW8{PFOzg(XHyvXMFuRxN0^vXDqkj*~Te~$;cNU0KReQcdl#~oOA31f+OirI9|}Z(An(g?EyU{ECcU6 zwvMfK6!U@$X0*HJza^;6o={+AndAG;IDB@+A;AY1y^+8DPZZLaUl*Z zuRxZSMhNaCyW$SRRN1D3p$BUtx?;G~m5t)B`#2jFenp8gghT5+|HCd`_q%OhD zbo}GFw1T;;vihVtWy>2;iW$djl zkOMON{WKUqgOR7Y8w)+t-vhqDO`uYMg0>cvnLz7U# zuQAN2P#X`ruAxL*lODiK(1SNt#Vy=S2B>_b)SLgViVzdnZ(mU$@{=y{Mo&=_by z_lW1)_wTAaC4RmqaycUJKLWVyQIP*c)JH|HM)Ok|ly0Z0$JGSLorf~?WK2gkn{uzG zJbOy%_r!fyXs16zAq7f5kfiL`g+#TaFIE}zr}_xTb{Y}juRaU42NAQy3X7upi+|BO zih-}2QX~9+w}rGMdP)w$Psl<(aFyrd>R%@=JSEkMa6uz%lP zi#e}nCJvKl%vzS3i`_S^-{bOSL}pawz^<%%CZhWc(Z?GAMej4&JGrcVrmy}2-F{E6 zl}y-Ed=-h<7ZF*}&wfGTOpoK9!L45U-aiVj70d1VCECf|jF?JMi$s+8@xToDLa5jo zoxeeMP>Xw|!MQ$ww!S0x*@;kSwzf8WS-ocb3HFzw(j%|ps(S_M3&gL_P7ahWQPh>n z_O$*n8pPIhnSHhrk*gad>yZprZQVuhxP(~&U2sHEyfgZk+vr;vKcVE?I_K-lU z6q>|LmCF(1Bu36H>7Ue6J1A+rV-&pQpVt9=bAMv4+Fxe8Lw~-t$nPHLEmKN6q*l9~ z!)Z8t92Qert2iqsh;^8}O^zyYtn8>>{Z3fyGC9WmPUS}jU-p;kSnAVqKLNkw&Y_hd z9*CR{K%!z^B&py%MX^8PgcZiYodt>#;bFlifcz}9#_=3@d4Z$g(peT8btT>T=C9dt z(-(9`{K3qbUkSjh`<)8@5ft}g2O$~3CF^T=$8pQ!gMP(z)>EdI4--b#{s%Pff-r4o z7klcwE@_N&%TiZQHI&^$z)#`cBL{ z!!omutKBkMVxO-wi3eVwZ@B9^BaV8OHb(uKAK$O?wj@8>6?SGl?@Kr9w%@&XIQ{j1 zhkyTG?X71xMR*2z#hU;%nwbGd1_WBT{^rc1b@H&vOaWAfRJ(Nya+4IxV6@4Ty}@80 zVSb9agWxO%AQQi-2bP$P!uv|yGY^Hzg)+~GPqUD+7dHw3|J(PS*E!07d#D^2bN5BF zFzC+F_WjXEp?$xcTW9xQUS}trU_9Z%5>tUtm^sm8A49AuoHY83F^m8f7nz;{)!S++ z>iqo7vzs$5nnBnXiJt8h+w62g_!5Lz`HK-+*5|x>Y8Ur-zTnl8=b#Yjk<-T`DjeJN zbRBpS_*i6jpF9DAfezuf+#&tV!Mn{i=Y)FbBWLg*&pzMzLoO5?e6!)a1J}pq6UK-m zju<$i^RF;0;dIE*j~eet9}l zu}Gh)KNNy7bQ~QndYtAJOlEyBXEWbG_hxb^l&omw70ke6ukXk8+TKJ-u-SK!TpwH> z*X(#Ym+2`U?%CxRf|1S5WVK+#p+zp8Z?WFL+S2>NBIRp8o4g5#PWJ*|>#(vxI4&zN zpKl1mXy}#0P+XE!>`Y^YZkwQmjnSqR^PD)Rf_K4=SykK42n=Plk(|h!aVger%b0t+ zFCa>zp81)?u!-K1;X;wf{5`M#%pPTqVlMplN>PTBcGcL+7?=Mtsn7iABy*d+9m!A4oMHcjJ6yiX}BMU~|swZcv$Dp=CC zd=WY3!oHKhN((;44$(rbAcgjF(*WN;MZ*hQ$1S*BTA?eOc@bWh9x)bkRDKvGU2E(K zjy<&`#aW+Xomn8~&EDdsxQe6e3RZ`{*2Ir{<0;NQdd@F^L$$SvQ>;Cm5<(gDvC#S` ziII6v8I3sY^k`Oc9qf62c1`b5Z5B3tbnZubdg(j=#e9Iz^TZYM^AlBmiE1G%b!@I+Z#Z|J8gx++Ns1t|jfwfQ|d9w4spI<*cNOUvb9jtf4y~H_kT3ljH z9F^io3msemIQgtFo!R^0)Z_#9;O`*1t&pV7+0nj;Ov?xgkHjuaMnPZ_d-7X)3ct1H zi4KA%TD;@S965{%G(;l)2E$9u(H@ZFqHTEx<^S0Tu2U7IEoWnN@kV><+~!HL&tVBA zqgBgEtmo|92P$keofqY3klJLoIk{fTzbN7>+c$P>0CRmjr$9&v=My-$m#`fxzh@qW z37-9yLToG(k_GE$Bgo=V;q9hKemo)Y#IMnqG@g%)#Er!$ZZ5U#vcgp ztF-6qOlWt|3X}*@a7W5K<$CDsZ9MU5k!)H%$k;=<948DPs;@G%L104eIE!bjOSp9%arW)~eO1LN4B~|Jq3y6=OlV8Z54-*<7 zmoe$I@@C;Y3)wx6`UKgoGCw@ECn(qJO8lymRJt(L`XJfg# zKH%Pm=)4!Yv7CsYBv2{}WyghAJTxH&3^$#p0^DJd@^|uboEQK8 zARLI-5to~D36?8Q3Y*MdO{JMjF^`g_FLM4?6r%^D{Pf)#3OPnOt>js5u?6O*z>Dv6 z?ldj2GZ@yGPe}aK^U6|L4*|+&YgB8#C-L{Mc%^fy<-Ou$4+lk zK-8&3NwEgg>Ak}y>Iu7(GEEHc;gM`t*^b+E3VngiPBMT9d_yn*_fD3WDl$+liL)as zOGUPXwwb&^u_rf@6?2D<>Z8wrx|3ud0A24HKkFA|w%9GOVJmtIXu4zdKtAb>_o@6P zI_1zX5V4egk&~AY%{T_5M>HDtg0W#ei6!vuK!aH50ngkH9|20x#F$0=8Qp2t2QmEm z^G#37!J*lA(&r2=j@r`pZ@Yc;#WKvq=$VhZT#qoD-;h$U1i5tGG0y1SY_n2-=KtT@m zat?GPZDmoEjBlVEdrWxfFoR3t05Ullgb)**-dJNOTNrx2$cy2^$23Mt8!+zBwA3-} zQ4ool_JJqNR`pHB8Jvm~i~7h0Qar9ER?lmjNX-0W-?fr-@mtrGs+NaLv2`${s7q%g z!+Hbq&rI|$JbX@Fd%hLbZDaFWr8*&XwJ8tXoW_vl>~gQ?F_AN)a?@0Z13YG)Z@|_n z@s(Z(+vX=(*Cufji#Ev1e4su66e7FfWf$;GM`>@WMDEenvXkux2)?0V6ZTv4!ih`4 z6xyKDcue(CeNP?noHwgeVnYa)0N9-t%gb8QguW-K3mO>@2L$$kjyodS~|7BhlzE_*x@ru!Ni6YQr zT%Y$6gEmGw$3}%34y}pz`&#{meOSATG+6ID_nkXk8wwlp8Bi*NX&6JgA~zBK znkE2I=W*+;I^iKe!dNz-k1^d}KpI)~(!JBarLyq*6^(xH9K-&#a}3%({yX7-J{h}N z*_tvso0*tdIl3}AIy*R;IlEe!xzNio(v6Q!{G=aKkY%jnoMPvgWQPRp!ok2!mDM1E zbV%j69s{_K(F7q{5Jsy z`w8Ft&JQ$>R*3A&4 zH|P#RLHWOKvUw5^2_JYqgiHPViT-74R9gd}_^cqmLXe?>HfsNoGRQ%A22iw)Kjs58 z8U9`6Ri-~BXANWgGZX$5Y5@Rf^2?0^21fReEWp6}ng8ICnUMcg)1PM7woIUkn9_rR zk-Q>>aQ{Vu`JZM}8Tnm@3%Ng+31t!gmFM5_%zpd4zREL8@efI86bU#lkMl3$s|Z@J zh(*eO5pn)`$FHK6yuyQN|G_hB5(2A7q5jGFue|^NJ{qPEe>mr(#Q((qt*H)bB$`)c zz#IOBhXJaNp@TXj&|>U&%K)r^A1x^V^T>Yp!?7`VqTkeYtN-hwfAvEXTdV){Md0-x zDv~wuttI=v+U1qZ=&L%U*@6hTzg0*7Qy%c^`0q-*cLF+j5ddGt34T|MVB*gsa|Vh` zu>AApUTFZn%2>tq4`0t25%_K5RRjL5{sh9mZjd~05QiLyJc;{ z0w!PO|4K>b6`mUa!qWiX`5^%}UvpjwcD&*+1pcWL^AyQH>-6_&eNanDzQV_Z{=uhD z(ft$u-_zD$V1lpkViAAvNYkYM#Q%M)3^Z^Eyuvrd|6l&G`2Tv!|2*aMx{wCoA5vNZ z!M{ka4pO`-sWasd38X9W&zimRM}LK%OaFtfOvV0JA+Ow?Uy)`q{*cPj(f&pHuT$y& zw%JVfUnE43nZ1g%|q*)NQ8uyR)Ir|L$19-Jl=IK>C?~I?L}4XY=Rx z(mr7PyvXnGGk?DO4gd{$`Tr*4_v3>8RQ{8chJXeO(R.id.resource_limit) as Button + resourceLimitBtn.setOnClickListener(this) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item.itemId + + return if (id == R.id.action_settings) { + true + } else super.onOptionsItemSelected(item) + } + + override fun onClick(v: View) { + if (v === resourceLimitBtn) { + val intent = Intent(this, ResourceLimitActivity::class.java) + startActivity(intent) + } + } + + companion object { + private const val TAG = "MainActivity" + } +} diff --git a/jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.java b/jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.java deleted file mode 100644 index 86a3fed..0000000 --- a/jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.java +++ /dev/null @@ -1,80 +0,0 @@ -package me.ycdev.android.lib.commonjni.demo; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import me.ycdev.android.lib.commonjni.SysResourceLimitHelper; - -public class ResourceLimitActivity extends AppCompatActivity implements View.OnClickListener { - private TextView mOflimitStatusView; - private Button mIncreaseOflimitBtn; - private Button mDecreaseOflimitBtn; - - private SysResourceLimitHelper.LimitInfo mCurOflimit; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_resource_limit); - - mOflimitStatusView = (TextView) findViewById(R.id.oflimit_status); - mIncreaseOflimitBtn = (Button) findViewById(R.id.oflimit_increase); - mIncreaseOflimitBtn.setOnClickListener(this); - mDecreaseOflimitBtn = (Button) findViewById(R.id.oflimit_decrease); - mDecreaseOflimitBtn.setOnClickListener(this); - - refreshOflimitStatus(); - } - - private void refreshOflimitStatus() { - mCurOflimit = SysResourceLimitHelper.getOpenFilesLimit(); - String status = getString(R.string.oflimit_status, mCurOflimit.curLimit, mCurOflimit.maxLimit); - mOflimitStatusView.setText(status); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_resource_limit, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - - return super.onOptionsItemSelected(item); - } - - @Override - public void onClick(View v) { - if (v == mIncreaseOflimitBtn) { - if (mCurOflimit.curLimit == 0) { - mCurOflimit.curLimit = 1; - } - if (!SysResourceLimitHelper.setOpenFilesLimit(mCurOflimit.curLimit * 2)) { - Toast.makeText(this, "failed to set limit", Toast.LENGTH_SHORT).show(); - } - refreshOflimitStatus(); - } else if (v == mDecreaseOflimitBtn) { - if (!SysResourceLimitHelper.setOpenFilesLimit(mCurOflimit.curLimit / 2)) { - Toast.makeText(this, "failed to set limit", Toast.LENGTH_SHORT).show(); - } - refreshOflimitStatus(); - } - } -} diff --git a/jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.kt b/jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.kt new file mode 100644 index 0000000..f3a9c5d --- /dev/null +++ b/jniLibDemo/src/main/java/me/ycdev/android/lib/commonjni/demo/ResourceLimitActivity.kt @@ -0,0 +1,74 @@ +package me.ycdev.android.lib.commonjni.demo + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.Button +import android.widget.TextView +import android.widget.Toast + +import androidx.appcompat.app.AppCompatActivity +import me.ycdev.android.lib.commonjni.SysResourceLimitHelper + +class ResourceLimitActivity : AppCompatActivity(), View.OnClickListener { + private lateinit var ofLimitStatusView: TextView + private lateinit var increaseOflimitBtn: Button + private lateinit var decreaseOflimitBtn: Button + + private lateinit var curOflimit: SysResourceLimitHelper.LimitInfo + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_resource_limit) + + ofLimitStatusView = findViewById(R.id.oflimit_status) as TextView + increaseOflimitBtn = findViewById(R.id.oflimit_increase) as Button + increaseOflimitBtn.setOnClickListener(this) + decreaseOflimitBtn = findViewById(R.id.oflimit_decrease) as Button + decreaseOflimitBtn.setOnClickListener(this) + + refreshOflimitStatus() + } + + private fun refreshOflimitStatus() { + curOflimit = SysResourceLimitHelper.getOpenFilesLimit() + val status = + getString(R.string.oflimit_status, curOflimit.curLimit, curOflimit.maxLimit) + ofLimitStatusView.text = status + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_resource_limit, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item.itemId + + return if (id == R.id.action_settings) { + true + } else super.onOptionsItemSelected(item) + } + + override fun onClick(v: View) { + if (v === increaseOflimitBtn) { + if (curOflimit.curLimit == 0) { + curOflimit.curLimit = 1 + } + if (!SysResourceLimitHelper.setOpenFilesLimit(curOflimit.curLimit * 2)) { + Toast.makeText(this, "failed to set limit", Toast.LENGTH_SHORT).show() + } + refreshOflimitStatus() + } else if (v === decreaseOflimitBtn) { + if (!SysResourceLimitHelper.setOpenFilesLimit(curOflimit.curLimit / 2)) { + Toast.makeText(this, "failed to set limit", Toast.LENGTH_SHORT).show() + } + refreshOflimitStatus() + } + } +} diff --git a/testLib/src/androidTest/java/me/ycdev/android/lib/test/ObjectLeakCheckerTest.kt b/testLib/src/androidTest/java/me/ycdev/android/lib/test/ObjectLeakCheckerTest.kt index 7b294c8..66aeb99 100644 --- a/testLib/src/androidTest/java/me/ycdev/android/lib/test/ObjectLeakCheckerTest.kt +++ b/testLib/src/androidTest/java/me/ycdev/android/lib/test/ObjectLeakCheckerTest.kt @@ -1,8 +1,7 @@ package me.ycdev.android.lib.test -import org.junit.Test - import com.google.common.truth.Truth.assertThat +import org.junit.Test class ObjectLeakCheckerTest { private class Dummy @@ -32,7 +31,7 @@ class ObjectLeakCheckerTest { } override fun operate(obj: Dummy) { - sHolder = obj + holder = obj } } val checker = ObjectLeakChecker(operator) @@ -43,6 +42,6 @@ class ObjectLeakCheckerTest { companion object { - private var sHolder: Dummy? = null + private var holder: Dummy? = null } } diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.java b/testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.java deleted file mode 100644 index 53d0172..0000000 --- a/testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.java +++ /dev/null @@ -1,66 +0,0 @@ -package me.ycdev.android.lib.test; - -import android.os.SystemClock; - -import java.lang.ref.WeakReference; -import java.util.concurrent.CountDownLatch; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ObjectLeakChecker { - public interface ObjectOperator { - T createObject(); - void operate(T obj); - } - - private CountDownLatch mLatch = new CountDownLatch(2); - private ObjectOperator mTargetOperator; - private WeakReference mTargetObjHolder; - - private boolean mIsPrepared = false; - private boolean mIsGcDone = false; - - public ObjectLeakChecker(ObjectOperator operator) { - mTargetOperator = operator; - } - - public void prepareForGc() { - Object objPartner = new Object() { - @Override - protected void finalize() throws Throwable { - super.finalize(); - mLatch.countDown(); - } - }; - - T targetObj = mTargetOperator.createObject(); - mTargetObjHolder = new WeakReference(targetObj); - mTargetOperator.operate(targetObj); - mIsPrepared = true; - } - - public void waitGcDone() { - if (!mIsPrepared) { - throw new RuntimeException("please call #prepareForGc() first"); - } - - // create a lot of objects to force GC - while (true) { - byte[] gcObj = new byte[1024 * 1024]; // 1MB - SystemClock.sleep(50); // wait for GC - if (mLatch.getCount() < 2) { - break; // GC happened - } - } - if (mTargetObjHolder.get() == null) { - mLatch.countDown(); - } - mIsGcDone = true; - } - - public long getLeakedObjectCount() { - if (!mIsGcDone) { - throw new RuntimeException("please call #waitGcDone() first"); - } - return mLatch.getCount(); - } -} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.kt b/testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.kt new file mode 100644 index 0000000..36d40db --- /dev/null +++ b/testLib/src/main/java/me/ycdev/android/lib/test/ObjectLeakChecker.kt @@ -0,0 +1,62 @@ +package me.ycdev.android.lib.test + +import android.os.SystemClock + +import java.lang.ref.WeakReference +import java.util.concurrent.CountDownLatch + +class ObjectLeakChecker(private val targetOperator: ObjectOperator) { + + private val latch = CountDownLatch(2) + private var targetObjHolder: WeakReference? = null + + private var isPrepared = false + private var isGcDone = false + + val leakedObjectCount: Long + get() { + if (!isGcDone) { + throw RuntimeException("please call #waitGcDone() first") + } + return latch.count + } + + interface ObjectOperator { + fun createObject(): T + fun operate(obj: T) + } + + fun prepareForGc() { + // partner object + object : Any() { + @Throws(Throwable::class) + protected fun finalize() { + latch.countDown() + } + } + + val targetObj = targetOperator.createObject() + targetObjHolder = WeakReference(targetObj) + targetOperator.operate(targetObj) + isPrepared = true + } + + fun waitGcDone() { + if (!isPrepared) { + throw RuntimeException("please call #prepareForGc() first") + } + + // create a lot of objects to force GC + while (true) { + ByteArray(1024 * 1024) // 1MB + SystemClock.sleep(50) // wait for GC + if (latch.count < 2) { + break // GC happened + } + } + if (targetObjHolder!!.get() == null) { + latch.countDown() + } + isGcDone = true + } +} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.java b/testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.java deleted file mode 100644 index 5ff03f2..0000000 --- a/testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.java +++ /dev/null @@ -1,13 +0,0 @@ -package me.ycdev.android.lib.test.base; - -import org.junit.BeforeClass; - -import me.ycdev.android.lib.test.log.TimberJvmTree; -import timber.log.Timber; - -public class NormalJUnitBase { - @BeforeClass - public static void setupClass() { - Timber.plant(new TimberJvmTree()); - } -} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.kt b/testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.kt new file mode 100644 index 0000000..d25f3c5 --- /dev/null +++ b/testLib/src/main/java/me/ycdev/android/lib/test/base/NormalJUnitBase.kt @@ -0,0 +1,14 @@ +package me.ycdev.android.lib.test.base + +import me.ycdev.android.lib.test.log.TimberJvmTree +import org.junit.BeforeClass +import timber.log.Timber + +open class NormalJUnitBase { + companion object { + @BeforeClass @JvmStatic + fun setupClass() { + Timber.plant(TimberJvmTree()) + } + } +} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.java b/testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.java deleted file mode 100644 index 99d6277..0000000 --- a/testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.java +++ /dev/null @@ -1,11 +0,0 @@ -package me.ycdev.android.lib.test.base; - -import org.junit.BeforeClass; -import org.robolectric.shadows.ShadowLog; - -public class RobolectricBase { - @BeforeClass - public static void setupClass() { - ShadowLog.stream = System.out; - } -} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.kt b/testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.kt new file mode 100644 index 0000000..e097ef6 --- /dev/null +++ b/testLib/src/main/java/me/ycdev/android/lib/test/base/RobolectricBase.kt @@ -0,0 +1,14 @@ +package me.ycdev.android.lib.test.base + +import org.junit.BeforeClass +import org.robolectric.shadows.ShadowLog + +@Suppress("unused") +open class RobolectricBase { + companion object { + @BeforeClass @JvmStatic + fun setupClass() { + ShadowLog.stream = System.out + } + } +} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.java b/testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.java deleted file mode 100644 index ba1fca6..0000000 --- a/testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.ycdev.android.lib.test.log; - -public class AndroidLogHelper { - // Copy the priority constants from android.util.Log - public static final int VERBOSE = 2; - public static final int DEBUG = 3; - public static final int INFO = 4; - public static final int WARN = 5; - public static final int ERROR = 6; - public static final int ASSERT = 7; - - public static String getPriorityName(int priority) { - switch (priority) { - case VERBOSE: - return "V"; - case DEBUG: - return "D"; - case INFO: - return "I"; - case WARN: - return "W"; - case ERROR: - return "E"; - case ASSERT: - return "A"; - default: - return "U"; - } - } -} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.kt b/testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.kt new file mode 100644 index 0000000..470f549 --- /dev/null +++ b/testLib/src/main/java/me/ycdev/android/lib/test/log/AndroidLogHelper.kt @@ -0,0 +1,24 @@ +package me.ycdev.android.lib.test.log + +@Suppress("MemberVisibilityCanBePrivate") +object AndroidLogHelper { + // Copy the priority constants from android.util.Log + const val VERBOSE = 2 + const val DEBUG = 3 + const val INFO = 4 + const val WARN = 5 + const val ERROR = 6 + const val ASSERT = 7 + + fun getPriorityName(priority: Int): String { + return when (priority) { + VERBOSE -> "V" + DEBUG -> "D" + INFO -> "I" + WARN -> "W" + ERROR -> "E" + ASSERT -> "A" + else -> "U" + } + } +} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.java b/testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.java deleted file mode 100644 index 9cbf81a..0000000 --- a/testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.java +++ /dev/null @@ -1,29 +0,0 @@ -package me.ycdev.android.lib.test.log; - -import java.util.ArrayList; - -import androidx.annotation.NonNull; - -import timber.log.Timber; - -public class TimberJvmTree extends Timber.Tree { - private ArrayList mLogs = new ArrayList<>(); - - public void clear() { - mLogs.clear(); - } - - public boolean hasLogs() { - return !mLogs.isEmpty(); - } - - @Override - protected void log(int priority, String tag, @NonNull String message, Throwable t) { - String log = AndroidLogHelper.getPriorityName(priority) + "/" + tag + ": " + message; - mLogs.add(log); - System.out.println(log); - if (t != null) { - t.printStackTrace(System.out); - } - } -} diff --git a/testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.kt b/testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.kt new file mode 100644 index 0000000..624465b --- /dev/null +++ b/testLib/src/main/java/me/ycdev/android/lib/test/log/TimberJvmTree.kt @@ -0,0 +1,28 @@ +package me.ycdev.android.lib.test.log + +import timber.log.Timber +import java.util.ArrayList + +@Suppress("unused") +class TimberJvmTree : Timber.Tree() { + private var logs: ArrayList? = null + + fun clear() { + logs?.clear() + } + + fun hasLogs(): Boolean { + return logs?.isNotEmpty() ?: false + } + + fun keepLogs() { + logs = ArrayList() + } + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + val log = AndroidLogHelper.getPriorityName(priority) + "/" + tag + ": " + message + logs?.add(log) + println(log) + t?.printStackTrace(System.out) + } +} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.java b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.java deleted file mode 100644 index 606de9f..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.java +++ /dev/null @@ -1,154 +0,0 @@ -package me.ycdev.android.lib.commonui.activity; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.view.View; -import android.widget.AdapterView; -import android.widget.GridView; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.List; - -import me.ycdev.android.arch.activity.AppCompatBaseActivity; -import me.ycdev.android.arch.wrapper.ToastHelper; -import me.ycdev.android.lib.common.utils.IntentUtils; -import me.ycdev.android.lib.commonui.R; -import me.ycdev.android.lib.commonui.base.ListAdapterBase; -import me.ycdev.android.lib.commonui.base.ViewHolderBase; - -import static me.ycdev.android.arch.ArchConstants.IntentType; -import static me.ycdev.android.arch.ArchConstants.IntentType.INTENT_TYPE_ACTIVITY; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class GridEntriesActivity extends AppCompatBaseActivity - implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { - public static class IntentEntry { - public @NonNull Intent intent; - public @NonNull String title; - public @NonNull String desc; - public @IntentType int type = INTENT_TYPE_ACTIVITY; - public @Nullable String perm; - - public IntentEntry(@NonNull Intent intent, @NonNull String title, @NonNull String desc) { - this.intent = intent; - this.title = title; - this.desc = desc; - } - - public IntentEntry(@IntentType int type, Intent intent, String title, String desc) { - this(intent, title, desc); - this.type = type; - } - } - - protected SystemEntriesAdapter mAdapter; - protected GridView mGridView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(getContentViewLayout()); - - mAdapter = new SystemEntriesAdapter(this); - - mGridView = findViewById(R.id.grid); - mGridView.setAdapter(mAdapter); - mGridView.setOnItemClickListener(this); - mGridView.setOnItemLongClickListener(this); - - loadItems(); - } - - @SuppressLint("StaticFieldLeak") - private void loadItems() { - if (needLoadIntentsAsync()) { - new AsyncTask>() { - @Override - protected List doInBackground(Void... params) { - return getIntents(); - } - - @Override - protected void onPostExecute(List result) { - mAdapter.setData(getIntents()); - } - }.execute(); - } else { - mAdapter.setData(getIntents()); - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - IntentEntry item = mAdapter.getItem(position); - onItemClicked(item); - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - IntentEntry item = mAdapter.getItem(position); - ToastHelper.show(this, item.desc, Toast.LENGTH_LONG); - return true; - } - - protected @LayoutRes int getContentViewLayout() { - return R.layout.commonui_grid_entries; - } - - /** - * Decide if we need to invoke {@link #getIntent()} async. - * @return true for async and false for sync. false by default - */ - protected boolean needLoadIntentsAsync() { - return false; - } - - protected abstract List getIntents(); - - protected void onItemClicked(IntentEntry item) { - if (IntentUtils.canStartActivity(this, item.intent)) { - startActivity(item.intent); - } else { - ToastHelper.show(this, item.desc, Toast.LENGTH_LONG); - } - } - - protected static class SystemEntriesAdapter extends ListAdapterBase { - public SystemEntriesAdapter(Context cxt) { - super(cxt); - } - - @Override - protected int getItemLayoutResId() { - return R.layout.commonui_grid_entries_item; - } - - @NonNull - @Override - protected ViewHolder createViewHolder(@NonNull View itemView, int position) { - return new ViewHolder(itemView, position); - } - - @Override - protected void bindView(@NonNull IntentEntry item, @NonNull ViewHolder vh) { - vh.titleView.setText(item.title); - } - - protected static class ViewHolder extends ViewHolderBase { - public TextView titleView; - - public ViewHolder(@NonNull View itemView, int position) { - super(itemView, position); - titleView = itemView.findViewById(R.id.title); - } - } - - } -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt new file mode 100644 index 0000000..517b265 --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt @@ -0,0 +1,133 @@ +package me.ycdev.android.lib.commonui.activity + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.GridView +import android.widget.TextView +import android.widget.Toast + +import androidx.annotation.LayoutRes +import me.ycdev.android.arch.ArchConstants +import me.ycdev.android.arch.activity.AppCompatBaseActivity +import me.ycdev.android.arch.wrapper.ToastHelper +import me.ycdev.android.lib.common.utils.IntentUtils +import me.ycdev.android.lib.commonui.R +import me.ycdev.android.lib.commonui.base.ListAdapterBase +import me.ycdev.android.lib.commonui.base.ViewHolderBase + +import me.ycdev.android.arch.ArchConstants.IntentType + +@Suppress("MemberVisibilityCanBePrivate", "unused") +abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItemClickListener, + AdapterView.OnItemLongClickListener { + + protected lateinit var mAdapter: SystemEntriesAdapter + protected lateinit var mGridView: GridView + + protected val contentViewLayout: Int + @LayoutRes get() = R.layout.commonui_grid_entries + + protected abstract val intents: List + + class IntentEntry(var intent: Intent, var title: String, var desc: String) { + @IntentType + var type = ArchConstants.INTENT_TYPE_ACTIVITY + var perm: String? = null + + constructor(@IntentType type: Int, intent: Intent, title: String, desc: String) : this( + intent, + title, + desc + ) { + this.type = type + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(contentViewLayout) + + mAdapter = SystemEntriesAdapter(this) + + mGridView = findViewById(R.id.grid) + mGridView.adapter = mAdapter + mGridView.onItemClickListener = this + mGridView.onItemLongClickListener = this + + loadItems() + } + + @SuppressLint("StaticFieldLeak") + private fun loadItems() { + if (needLoadIntentsAsync()) { + object : AsyncTask>() { + override fun doInBackground(vararg params: Void): List { + return intents + } + + override fun onPostExecute(result: List) { + mAdapter.setData(intents) + } + }.execute() + } else { + mAdapter.setData(intents) + } + } + + override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val item = mAdapter.getItem(position) + onItemClicked(item) + } + + override fun onItemLongClick( + parent: AdapterView<*>, + view: View, + position: Int, + id: Long + ): Boolean { + val item = mAdapter.getItem(position) + ToastHelper.show(this, item.desc, Toast.LENGTH_LONG) + return true + } + + /** + * Decide if we need to invoke [.getIntent] async. + * @return true for async and false for sync. false by default + */ + protected fun needLoadIntentsAsync(): Boolean { + return false + } + + protected fun onItemClicked(item: IntentEntry) { + if (IntentUtils.canStartActivity(this, item.intent)) { + startActivity(item.intent) + } else { + ToastHelper.show(this, item.desc, Toast.LENGTH_LONG) + } + } + + protected open class SystemEntriesAdapter(cxt: Context) : + ListAdapterBase(cxt) { + + override val itemLayoutResId: Int + get() = R.layout.commonui_grid_entries_item + + override fun createViewHolder(itemView: View, position: Int): ViewHolder { + return ViewHolder(itemView, position) + } + + override fun bindView(item: IntentEntry, holder: ViewHolder) { + holder.titleView.text = item.title + } + + class ViewHolder(itemView: View, position: Int) : + ViewHolderBase(itemView, position) { + var titleView: TextView = itemView.findViewById(R.id.title) + } + } +} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.java b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.java deleted file mode 100644 index 2e9a203..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.java +++ /dev/null @@ -1,84 +0,0 @@ -package me.ycdev.android.lib.commonui.base; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import android.app.Activity; -import android.content.Context; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class ListAdapterBase extends BaseAdapter { - protected Context mContext; - protected LayoutInflater mInflater; - protected List mList; - - public ListAdapterBase(@NonNull Context cxt) { - mContext = cxt; - if (cxt instanceof Activity) { - mInflater = ((Activity) cxt).getLayoutInflater(); - } else { - mInflater = LayoutInflater.from(cxt); - } - } - - public void setData(@Nullable List data) { - mList = data; - notifyDataSetChanged(); - } - - public void sort(@NonNull Comparator comparator) { - Collections.sort(mList, comparator); - notifyDataSetChanged(); - } - - /** - * @return null will be returned if no data set. - */ - @Nullable - public List getData() { - return mList; - } - - @Override - public int getCount() { - return mList != null ? mList.size(): 0; - } - - @Override - public ItemType getItem(int position) { - return mList.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - VH holder; - if (convertView == null) { - convertView = mInflater.inflate(getItemLayoutResId(), parent, false); - holder = createViewHolder(convertView, position); - convertView.setTag(holder); - } else { - @SuppressWarnings("unchecked") - VH tmp = (VH) convertView.getTag(); - holder = tmp; - } - bindView(getItem(position), holder); - return convertView; - } - - protected abstract @LayoutRes int getItemLayoutResId(); - protected abstract @NonNull VH createViewHolder(@NonNull View itemView, int position); - protected abstract void bindView(@NonNull ItemType item, @NonNull VH holder); -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt new file mode 100644 index 0000000..6845aa0 --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt @@ -0,0 +1,74 @@ +package me.ycdev.android.lib.commonui.base + +import java.util.Collections +import java.util.Comparator + +import android.app.Activity +import android.content.Context +import androidx.annotation.LayoutRes +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter + +@Suppress("unused", "MemberVisibilityCanBePrivate") +abstract class ListAdapterBase(context: Context) : + BaseAdapter() { + protected var inflater: LayoutInflater = if (context is Activity) { + context.layoutInflater + } else { + LayoutInflater.from(context) + } + protected var list: List? = null + + @get:LayoutRes + protected abstract val itemLayoutResId: Int + + /** + * @return null will be returned if no data set. + */ + fun getData(): List? { + return list + } + + fun setData(data: List?) { + list = data + notifyDataSetChanged() + } + + fun sort(comparator: Comparator) { + Collections.sort(list!!, comparator) + notifyDataSetChanged() + } + + override fun getCount(): Int { + return if (list != null) list!!.size else 0 + } + + override fun getItem(position: Int): ItemType { + return list!![position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + var itemView = convertView + val holder: VH + if (itemView == null) { + itemView = inflater.inflate(itemLayoutResId, parent, false) + holder = createViewHolder(itemView!!, position) + itemView.tag = holder + } else { + @Suppress("UNCHECKED_CAST") + val tmp = itemView.tag as VH + holder = tmp + } + bindView(getItem(position), holder) + return itemView + } + + protected abstract fun createViewHolder(itemView: View, position: Int): VH + protected abstract fun bindView(item: ItemType, holder: VH) +} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.java b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.java deleted file mode 100644 index 294e605..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.java +++ /dev/null @@ -1,30 +0,0 @@ -package me.ycdev.android.lib.commonui.base; - -import android.app.Activity; - -import me.ycdev.android.lib.commonui.R; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class LoadingAsyncTaskBase extends - WaitingAsyncTaskBase { - public LoadingAsyncTaskBase(Activity activity) { - super(activity); - } - - public LoadingAsyncTaskBase(Activity activity, boolean cancelable, boolean autoFinishWhenCanceled) { - super(activity, cancelable, autoFinishWhenCanceled); - } - - @Override - protected String getInitMessage() { - return mActivity.getString(R.string.commonui_tips_loading_percent, 0); - } - - @Override - protected void onProgressUpdate(Integer... values) { - int percent = values[0]; - if (mDialog.isShowing()) { - mDialog.setMessage(mActivity.getString(R.string.commonui_tips_loading_percent, percent)); - } - } -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.kt new file mode 100644 index 0000000..4b215e9 --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/LoadingAsyncTaskBase.kt @@ -0,0 +1,27 @@ +package me.ycdev.android.lib.commonui.base + +import android.app.Activity + +import me.ycdev.android.lib.commonui.R + +@Suppress("unused") +abstract class LoadingAsyncTaskBase : WaitingAsyncTaskBase { + + override val initMessage: String + get() = activity.getString(R.string.commonui_tips_loading_percent, 0) + + constructor(activity: Activity) : super(activity) + + constructor(activity: Activity, cancelable: Boolean, autoFinishWhenCanceled: Boolean) : super( + activity, + cancelable, + autoFinishWhenCanceled + ) + + override fun onProgressUpdate(vararg values: Int?) { + val percent = values[0] + if (dialog.isShowing) { + dialog.setMessage(activity.getString(R.string.commonui_tips_loading_percent, percent)) + } + } +} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.java b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.java deleted file mode 100644 index c818851..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.ycdev.android.lib.commonui.base; - -import androidx.annotation.NonNull; -import android.view.View; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ViewHolderBase { - public @NonNull View itemView; - public int position; - - public ViewHolderBase(@NonNull View itemView, int position) { - this.itemView = itemView; - this.position = position; - } -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt new file mode 100644 index 0000000..bde125c --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt @@ -0,0 +1,5 @@ +package me.ycdev.android.lib.commonui.base + +import android.view.View + +open class ViewHolderBase(var itemView: View, var position: Int) diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.java b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.java deleted file mode 100644 index d28800d..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.java +++ /dev/null @@ -1,73 +0,0 @@ -package me.ycdev.android.lib.commonui.base; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ProgressDialog; -import android.os.AsyncTask; - -import me.ycdev.android.lib.common.utils.LibLogger; -import me.ycdev.android.lib.commonui.R; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class WaitingAsyncTaskBase extends - AsyncTask { - private static final String TAG = "WaitingAsyncTaskBase"; - - @SuppressLint("StaticFieldLeak") - protected Activity mActivity; - protected boolean mCancelable; - protected boolean mAutoFinishWhenCanceled; - - protected ProgressDialog mDialog; - - public WaitingAsyncTaskBase(Activity activity) { - this(activity, false, false); - } - - public WaitingAsyncTaskBase(Activity activity, boolean cancelable, - boolean autoFinishWhenCanceled) { - mActivity = activity; - mCancelable = cancelable; - mAutoFinishWhenCanceled = autoFinishWhenCanceled; - } - - public void setCancelable(boolean cancelable) { - mCancelable = cancelable; - } - - public void setAutoFinishWhenCanceled(boolean autoFinishWhenCanceled) { - mAutoFinishWhenCanceled = autoFinishWhenCanceled; - } - - protected String getInitMessage() { - return mActivity.getString(R.string.commonui_tips_loading); - } - - @Override - protected void onPreExecute() { - mDialog = new ProgressDialog(mActivity); - mDialog.setMessage(getInitMessage()); - mDialog.setCancelable(mCancelable); - mDialog.setOnCancelListener(dialog -> { - LibLogger.d(TAG, "to cancel, finish activity? " + mAutoFinishWhenCanceled); - cancel(true); - if (mAutoFinishWhenCanceled) { - mActivity.finish(); - } - }); - mDialog.show(); - } - - @Override - protected void onCancelled() { - LibLogger.d(TAG, "cancelled"); - if (mDialog.isShowing()) { - mDialog.dismiss(); - } - } - - @Override - protected void onPostExecute(Result result) { - mDialog.dismiss(); - } -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.kt new file mode 100644 index 0000000..90dc71a --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/WaitingAsyncTaskBase.kt @@ -0,0 +1,56 @@ +package me.ycdev.android.lib.commonui.base + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.ProgressDialog +import android.os.AsyncTask + +import me.ycdev.android.lib.common.utils.LibLogger +import me.ycdev.android.lib.commonui.R + +@Suppress("MemberVisibilityCanBePrivate") +abstract class WaitingAsyncTaskBase constructor( + @field:SuppressLint("StaticFieldLeak") + protected var activity: Activity, + var cancelable: Boolean = false, + protected var mAutoFinishWhenCanceled: Boolean = false +) : AsyncTask() { + + protected lateinit var dialog: ProgressDialog + + protected open val initMessage: String + get() = activity.getString(R.string.commonui_tips_loading) + + fun setAutoFinishWhenCanceled(autoFinishWhenCanceled: Boolean) { + mAutoFinishWhenCanceled = autoFinishWhenCanceled + } + + override fun onPreExecute() { + dialog = ProgressDialog(activity) + dialog.setMessage(initMessage) + dialog.setCancelable(cancelable) + dialog.setOnCancelListener { _ -> + LibLogger.d(TAG, "to cancel, finish activity? $mAutoFinishWhenCanceled") + cancel(true) + if (mAutoFinishWhenCanceled) { + activity.finish() + } + } + dialog.show() + } + + override fun onCancelled() { + LibLogger.d(TAG, "cancelled") + if (dialog.isShowing) { + dialog.dismiss() + } + } + + override fun onPostExecute(result: Result) { + dialog.dismiss() + } + + companion object { + private const val TAG = "WaitingAsyncTaskBase" + } +} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.java b/uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.java deleted file mode 100644 index 2ef814f..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.java +++ /dev/null @@ -1,36 +0,0 @@ -package me.ycdev.android.lib.commonui.utils; - -import android.app.Activity; -import android.os.SystemClock; - -import me.ycdev.android.lib.commonui.base.WaitingAsyncTaskBase; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class WaitingAsyncTask extends WaitingAsyncTaskBase { - private static final long WAITING_TIME_MIN = 500; // ms - - private Runnable mTask; - private String mMsg; - - public WaitingAsyncTask(Activity activity, String msg, Runnable task) { - super(activity); - mMsg = msg; - mTask = task; - } - - @Override - protected String getInitMessage() { - return mMsg; - } - - @Override - protected Void doInBackground(Void... params) { - long timeStart = SystemClock.elapsedRealtime(); - mTask.run(); - long timeUsed = SystemClock.elapsedRealtime() - timeStart; - if (timeUsed < WAITING_TIME_MIN) { - SystemClock.sleep(WAITING_TIME_MIN - timeUsed); - } - return null; - } -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.kt new file mode 100644 index 0000000..87c0c93 --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/utils/WaitingAsyncTask.kt @@ -0,0 +1,27 @@ +package me.ycdev.android.lib.commonui.utils + +import android.app.Activity +import android.os.SystemClock + +import me.ycdev.android.lib.commonui.base.WaitingAsyncTaskBase + +class WaitingAsyncTask( + activity: Activity, + protected override val initMessage: String, + private val mTask: Runnable +) : WaitingAsyncTaskBase(activity) { + + override fun doInBackground(vararg params: Void): Void? { + val timeStart = SystemClock.elapsedRealtime() + mTask.run() + val timeUsed = SystemClock.elapsedRealtime() - timeStart + if (timeUsed < WAITING_TIME_MIN) { + SystemClock.sleep(WAITING_TIME_MIN - timeUsed) + } + return null + } + + companion object { + private val WAITING_TIME_MIN: Long = 500 // ms + } +} From 31afbb4674a809d7e188d2a71b0999c211f2b1c1 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 21 Jul 2019 08:20:16 +0800 Subject: [PATCH 002/100] Use property instead of function for ServiceConnector --- android_project_common.gradle | 137 +++++++++++++----- archLintRulesTestDemo/build.gradle | 8 +- .../lib/common/ipc/ServiceClientBaseTest.kt | 4 +- .../lib/common/ipc/ServiceConnectorTest.kt | 54 +++---- .../lib/common/ipc/ServiceClientBase.kt | 6 +- .../lib/common/ipc/ServiceConnector.kt | 32 ++-- build.gradle | 2 +- 7 files changed, 153 insertions(+), 90 deletions(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 0c34562..bf6e101 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -36,24 +36,33 @@ ext { // Android official support 'kotlin' : "1.3.31", - 'supportLib' : "28.0.0", 'multidexLib' : "2.0.1", + 'androidxCore' : "1.0.2", + 'fragment' : "1.0.0", + 'preference' : "1.0.0", + 'palette' : "1.0.0", + 'recyclerView' : "1.0.0", 'constraintLayout' : "1.1.3", + 'vectorDrawable' : "1.0.0", 'lintLib' : "26.4.0", 'archCore' : "2.0.0", 'lifecycle' : "2.0.0", 'room' : "2.0.0", + 'sqlite' : "2.0.1", + 'navigation' : "2.0.0", + 'paging' : "2.1.0", + 'work' : "2.0.1", // test - 'runner' : "1.1.0", - 'rules' : "1.1.0", - 'truth' : "0.42", + 'testCore' : "1.2.0", 'espresso' : "3.1.0", 'uiautomator' : "2.2.0", + 'truth' : "0.42", 'hamcrest' : "1.3", 'mockito' : "1.10.19", 'powermock' : "1.6.4", - 'robolectric' : "3.8", + 'robolectric' : "4.2", + 'mockk' : "1.9.3", // google 'gms' : "16.0.0", @@ -81,7 +90,7 @@ ext { // rx 'rxjava' : "2.1.6", - 'rxandroid' : "2.0.2", + 'rxandroid' : "2.1.1", // ycdev 'androidLib' : "1.4.0", @@ -91,65 +100,115 @@ ext { ] deps = [ // Android official support + 'kotlin': [ + 'stdlib' : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}", + ], 'androidx': [ - 'annotation' : "androidx.annotation:annotation:1.0.2", - 'core' : "androidx.core:core:1.0.0", - 'media' : "androidx.media:media:1.0.0", - 'fragment' : "androidx.fragment:fragment:1.0.0", - 'appcompat' : "androidx.appcompat:appcompat:1.0.0", + // core + 'annotation' : "androidx.annotation:annotation:1.1.0", + 'core' : "androidx.core:core:${versions.androidxCore}", + 'coreKtx' : "androidx.core:core-ktx:${versions.androidxCore}", + 'fragment' : "androidx.fragment:fragment:${versions.fragment}", + 'fragmentKtx' : "androidx.fragment:fragment-ktx:${versions.fragment}", + 'localBroadcast' : "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0", + 'collection' : "androidx.collection:collection:1.0.0", + 'collectionKtx' : "androidx.collection:collection-ktx:1.0.0", + // UI + 'appcompat' : "androidx.appcompat:appcompat:1.0.2", + 'design' : "com.google.android.material:material:1.0.0", + 'preference' : "androidx.preference:preference:${versions.preference}", + 'preferenceKtx' : "androidx.preference:preference-ktx:${versions.preference}", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:1.1.3", 'cardview' : "androidx.cardview:cardview:1.0.0", 'gridlayout' : "androidx.gridlayout:gridlayout:1.0.0", + 'palette' : "androidx.palette:palette:${versions.palette}", + 'paletteKtx' : "androidx.palette:palette-ktx:${versions.palette}", + 'recyclerview' : "androidx.recyclerview:recyclerview:${versions.recyclerView}", + 'recyclerviewSelection' : "androidx.recyclerview:recyclerview:${versions.recyclerView}", + 'percent' : "androidx.percentlayout:percentlayout:1.0.0", + 'coordinatorLayout' : "androidx.coordinatorlayout:coordinatorlayout:1.0.0", + 'drawerLayout' : "androidx.drawerlayout:drawerlayout:1.0.0", + 'swipeRefreshLayout' : "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0", + 'viewPager' : "androidx.viewpager:viewpager:1.0.0", + 'vectorDrawable' : "androidx.vectordrawable:vectordrawable:${versions.vectorDrawable}", + 'animatedVectorDrawable' : "androidx.vectordrawable:vectordrawable-animated:${versions.vectorDrawable}", + 'customtabs' : "androidx.browser:browser:1.0.0", + 'transition' : "androidx.transition:transition:1.0.1", + // others + 'multidex' : "androidx.multidex:multidex:${versions.multidexLib}", + 'mediaCompat' : "androidx.media:media:1.0.1", 'mediarouter' : "androidx.mediarouter:mediarouter:1.0.0", - 'palette' : "androidx.palette:palette:1.0.0", - 'design' : "com.google.android.material:material:1.0.0", - 'recyclerview' : "androidx.recyclerview:recyclerview:1.0.0", - 'vectorDrawable' : "androidx.versionedparcelable:versionedparcelable:1.0.0", - 'animatedVectorDrawable' : "androidx.vectordrawable:vectordrawable-animated:1.0.0", - 'browser' : "androidx.browser:browser:1.0.0", 'exifinterface' : "androidx.exifinterface:exifinterface:1.0.0", 'wear' : "androidx.wear:wear:1.0.0", - - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:${versions.constraintLayout}", - 'multidex' : "androidx.multidex:multidex:${versions.multidexLib}", - ], - 'kotlin': [ - 'stdlib' : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}", + // legacy + 'coreUtils' : "androidx.legacy:legacy-support-core-utils:1.0.0", + 'coreUi' : "androidx.legacy:legacy-support-core-ui:1.0.0", + 'supportV13' : "androidx.legacy:legacy-support-v13:1.0.0", ], 'archCore': [ 'common' : "androidx.arch.core:core-common:${versions.archCore}", - 'core' : "androidx.arch.core:core:${versions.archCore}", - 'testing' : "androidx.arch.core:core-testing:${versions.archCore}", 'runtime' : "androidx.arch.core:core-runtime:${versions.archCore}", + 'test' : "androidx.arch.core:core-testing:${versions.archCore}", ], 'lifecycle': [ - 'common' : "androidx.lifecycle:lifecycle-common:${versions.lifecycle}", - 'commonJava8' : "androidx.lifecycle:lifecycle-common-java8:${versions.lifecycle}", + 'runtime' : "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}", 'compiler' : "androidx.lifecycle:lifecycle-compiler:${versions.lifecycle}", + 'commonJava8' : "androidx.lifecycle:lifecycle-common-java8:${versions.lifecycle}", 'extensions' : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}", 'reactiveStreams' : "androidx.lifecycle:lifecycle-reactivestreams:${versions.lifecycle}", - "livedata" : "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}", - "livedataCore" : "androidx.lifecycle:lifecycle-livedata-core:${versions.lifecycle}", - "viewmodel" : "androidx.lifecycle:lifecycle-viewmodel:${versions.lifecycle}", - 'runtime' : "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}", - ], 'room': [ - 'common' : "androidx.room:room-common:${versions.room}", 'runtime' : "androidx.room:room-runtime:${versions.room}", 'compiler' : "androidx.room:room-compiler:${versions.room}", 'rxjava' : "androidx.room:room-rxjava2:${versions.room}", 'testing' : "androidx.room:room-testing:${versions.room}", ], + 'sqlite': [ + 'sqlite' : "androidx.sqlite:sqlite:${versions.sqlite}", + 'sqliteKtx' : "androidx.sqlite:sqlite-ktx:${versions.sqlite}", + 'framework' : "androidx.sqlite:sqlite-framework:${versions.sqlite}", + ], + 'navigation': [ + 'common' : "androidx.navigation:navigation-common:${versions.navigation}", + 'commonKtx' : "androidx.navigation:navigation-common-ktx:${versions.navigation}", + 'fragment' : "androidx.navigation:navigation-fragment:${versions.navigation}", + 'fragmentKtx' : "androidx.navigation:navigation-fragment-ktx:${versions.navigation}", + 'runtime' : "androidx.navigation:navigation-runtime:${versions.navigation}", + 'runtimeKtx' : "androidx.navigation:navigation-runtime-ktx:${versions.navigation}", + 'ui' : "androidx.navigation:navigation-ui:${versions.navigation}", + 'uiKtx' : "androidx.navigation:navigation-ui-ktx:${versions.navigation}", + ], + 'paging': [ + 'common' : "androidx.paging:paging-common:${versions.paging}", + 'commonKtx' : "androidx.paging:paging-common-ktx:${versions.paging}", + 'runtime' : "androidx.paging:paging-runtime:${versions.paging}", + 'runtimeKtx' : "androidx.paging:paging-runtime-ktx:${versions.paging}", + 'rxjava2' : "androidx.paging:paging-rxjava2:${versions.paging}", + 'rxjava2Ktx' : "androidx.paging:paging-rxjava2-ktx:${versions.paging}", + ], + 'work': [ + "runtime" : "androidx.work:work-runtime:${versions.work}", + "runtimeKtx" : "androidx.work:work-runtime-ktx:${versions.work}", + "rxjava2" : "androidx.work:work-rxjava2:${versions.work}", + "test" : "androidx.work:work-testing:${versions.work}", + ], // test 'test': [ - 'core' : "androidx.test:core:1.0.0", - 'junit' : "androidx.test.ext:junit:1.0.0", - 'runner' : "androidx.test:runner:${versions.runner}", - 'rules' : "androidx.test:rules:${versions.rules}", + // core + 'core' : "androidx.test:core:${versions.testCore}", + 'coreKtx' : "androidx.test:core-ktx:${versions.testCore}", + 'runner' : "androidx.test:runner:${versions.testCore}", + 'rules' : "androidx.test:rules:${versions.testCore}", + 'monitor' : "androidx.test:monitor:${versions.testCore}", + 'orchestrator' : "androidx.test:orchestrator:${versions.testCore}", + 'truthAndroidX' : 'androidx.test.ext:truth:${versions.testCore}', + // ext + 'junit' : "androidx.test.ext:junit:1.1.1", + 'junitKtx' : "androidx.test.ext:junit-ktx:1.1.1", 'truth' : "com.google.truth:truth:${versions.truth}", 'truthJava8' : "com.google.truth.extensions:truth-java8-extension:${versions.truth}", - 'truthAndroidX' : 'androidx.test.ext:truth:1.0.0', + // espresso 'espressoCore' : "androidx.test.espresso:espresso-core:${versions.espresso}", 'espressoContrib' : "androidx.test.espresso:espresso-contrib:${versions.espresso}", 'espressoIntents' : "androidx.test.espresso:espresso-intents:${versions.espresso}", @@ -160,6 +219,8 @@ ext { 'mockitoCore' : "org.mockito:mockito-core:${versions.mockito}", 'powermockMockito' : "org.powermock:powermock-api-mockito:${versions.powermock}", 'powermockJunit' : "org.powermock:powermock-module-junit4:${versions.powermock}", + 'mockk' : "io.mockk:mockk:${versions.mockk}", + 'mockkAndroid' : "io.mockk:mockk-android:${versions.mockk}", 'robolectric' : "org.robolectric:robolectric:${versions.robolectric}", ], diff --git a/archLintRulesTestDemo/build.gradle b/archLintRulesTestDemo/build.gradle index b6383c3..766e3a3 100644 --- a/archLintRulesTestDemo/build.gradle +++ b/archLintRulesTestDemo/build.gradle @@ -39,7 +39,7 @@ dependencies { // The following dependencies are just for checking new versions of library implementation "androidx.multidex:multidex:${versions.multidexLib}" - implementation "androidx.annotation:annotation:1.0.2" + implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.constraintlayout:constraintlayout:${versions.constraintLayout}" implementation "androidx.arch.core:core-common:${versions.archCore}" implementation "androidx.lifecycle:lifecycle-common:${versions.lifecycle}" @@ -70,9 +70,9 @@ dependencies { implementation "com.google.zxing:core:${versions.zxing}" - testImplementation "junit:junit:$versions.junit" - testImplementation "androidx.test:runner:${versions.runner}" - testImplementation "androidx.test:rules:${versions.rules}" + testImplementation "androidx.test.ext:junit:1.1.1" + testImplementation "androidx.test:runner:${versions.testCore}" + testImplementation "androidx.test:rules:${versions.testCore}" testImplementation "org.hamcrest:hamcrest-core:${versions.hamcrest}" testImplementation "org.mockito:mockito-core:${versions.mockito}" testImplementation "org.powermock:powermock-api-mockito:${versions.powermock}" diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt index f660efd..6a8dbc0 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceClientBaseTest.kt @@ -96,9 +96,9 @@ class ServiceClientBaseTest { val latch = CountDownLatch(1) client.addOperation(HelloOperation("Hello, world").setNotifier(latch)) - assertThat(client.serviceConnector.getService()).isNull() + assertThat(client.serviceConnector.service).isNull() latch.await() - assertThat(client.serviceConnector.getService()).isNotNull() + assertThat(client.serviceConnector.service).isNotNull() timeStart = SystemClock.elapsedRealtime() } diff --git a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt index e306dfc..33be9a8 100644 --- a/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt +++ b/baseLib/src/androidTest/java/me/ycdev/android/lib/common/ipc/ServiceConnectorTest.kt @@ -39,7 +39,7 @@ class ServiceConnectorTest { // BinderProxy - assertThat(connector.getService()!!.asBinder().javaClass.name).isEqualTo("android.os.BinderProxy") + assertThat(connector.service!!.asBinder().javaClass.name).isEqualTo("android.os.BinderProxy") disconnectSync(connector) } @@ -52,7 +52,7 @@ class ServiceConnectorTest { connectSync(connector) // Local object - assertThat(connector.getService()!!.asBinder().javaClass.name) + assertThat(connector.service!!.asBinder().javaClass.name) .isEqualTo("me.ycdev.android.lib.common.demo.service.LocalService\$BinderServer") disconnectSync(connector) } @@ -63,12 +63,12 @@ class ServiceConnectorTest { val context = ApplicationProvider.getApplicationContext() run { val connector = RemoteServiceConnector(context) - assertThat(connector.getConnectLooper()).isEqualTo(Looper.getMainLooper()) + assertThat(connector.connectLooper).isEqualTo(Looper.getMainLooper()) } run { val connector = LocalServiceConnector(context) - assertThat(connector.getConnectLooper()).isEqualTo(Looper.getMainLooper()) + assertThat(connector.connectLooper).isEqualTo(Looper.getMainLooper()) } } @@ -192,8 +192,8 @@ class ServiceConnectorTest { } connector.addListener(listener) connector.disconnect() - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) - assertThat(connector.getService()).isNull() + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.service).isNull() assertThat(stateChangeCount.value).isEqualTo(0) } @@ -223,12 +223,12 @@ class ServiceConnectorTest { connector.addListener(listener) connector.waitForConnected() - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) - assertThat(connector.getService()).isNotNull() + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.service).isNotNull() assertThat(stateChangeCount.value).isEqualTo(2) // connecting & connected connector.waitForConnected() - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) assertThat(stateChangeCount.value).isEqualTo(2) // already connected, no change anymore disconnectSync(connector) @@ -242,12 +242,12 @@ class ServiceConnectorTest { run { val connector = FakeServiceConnector(context) connector.waitForConnected() // should fail immediately - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) } run { val connector = NoPermServiceConnector(context) connector.waitForConnected() // should fail immediately - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) } } @@ -266,11 +266,11 @@ class ServiceConnectorTest { connector.addListener(listener) connector.waitForConnected(100) // - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTING) + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTING) assertThat(stateChangeCount.value).isEqualTo(1) // connecting connector.waitForConnected() - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) assertThat(stateChangeCount.value).isEqualTo(2) // connected disconnectSync(connector) @@ -282,24 +282,24 @@ class ServiceConnectorTest { fun getService() { val context = ApplicationProvider.getApplicationContext() val connector = RemoteServiceConnector(context) - assertThat(connector.getService()).isNull() + assertThat(connector.service).isNull() val listener = object : ConnectStateListener { override fun onStateChanged(newState: Int) { if (newState == ServiceConnector.STATE_CONNECTED) { - assertThat(connector.getService()).isNotNull() + assertThat(connector.service).isNotNull() } else { - assertThat(connector.getService()).isNull() + assertThat(connector.service).isNull() } } } connector.addListener(listener) connector.waitForConnected() - assertThat(connector.getService()).isNotNull() + assertThat(connector.service).isNotNull() connector.disconnect() - assertThat(connector.getService()).isNull() + assertThat(connector.service).isNull() } private class FakeServiceConnector internal constructor(cxt: Context) : @@ -365,11 +365,11 @@ class ServiceConnectorTest { } connector.addListener(listener) - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) - assertThat(connector.getService()).isNull() + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.service).isNull() connector.connect() - assertThat(connector.getConnectState()) + assertThat(connector.connectState) .isAnyOf(ServiceConnector.STATE_CONNECTING, ServiceConnector.STATE_CONNECTED) try { @@ -378,13 +378,13 @@ class ServiceConnectorTest { fail("Should not happen: $e") } - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) - assertThat(connector.getService()).isNotNull() + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.service).isNotNull() } private fun disconnectSync(connector: ServiceConnector<*>) { - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_CONNECTED) - assertThat(connector.getService()).isNotNull() + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_CONNECTED) + assertThat(connector.service).isNotNull() val latch = CountDownLatch(1) val listener = object : ConnectStateListener { @@ -396,8 +396,8 @@ class ServiceConnectorTest { } connector.addListener(listener) connector.disconnect() - assertThat(connector.getConnectState()).isEqualTo(ServiceConnector.STATE_DISCONNECTED) - assertThat(connector.getService()).isNull() + assertThat(connector.connectState).isEqualTo(ServiceConnector.STATE_DISCONNECTED) + assertThat(connector.service).isNull() try { latch.await() } catch (e: InterruptedException) { diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt index 3f586de..90a153f 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt @@ -55,7 +55,7 @@ open class ServiceClientBase protected constructor( } fun addOperation(operation: IpcOperation) { - if (serviceConnector.getConnectState() == ServiceConnector.STATE_DISCONNECTED) { + if (serviceConnector.connectState == ServiceConnector.STATE_DISCONNECTED) { // try to connect if not connected or connecting // (such as the Service APK was installed after the previous connecting) // (such as autoDisconnect enabled) @@ -75,7 +75,7 @@ open class ServiceClientBase protected constructor( @WorkerThread private fun handleOperation(operation: IpcOperation) { - val service = serviceConnector.getService() + val service = serviceConnector.service if (service != null) { try { operation.execute(service) @@ -107,7 +107,7 @@ open class ServiceClientBase protected constructor( @WorkerThread private fun handlePendingOperations() { Timber.tag(TAG).d("[%s] handlePendingOperations: %d", serviceName, pendingOperations.size) - while (serviceConnector.getService() != null) { + while (serviceConnector.service != null) { val operation = pendingOperations.poll() ?: break handleOperation(operation) } diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt index 20120ad..542749b 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceConnector.kt @@ -19,9 +19,16 @@ import me.ycdev.android.lib.common.utils.WeakListenerManager import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger +/** + * @constructor + * @param serviceName Used to print logs only. + * @param connectLooper The looper used to connect/reconnect target Service. + * By default, it's the main looper. + */ abstract class ServiceConnector protected constructor( cxt: Context, - protected var serviceName: String + protected var serviceName: String, + val connectLooper: Looper = Looper.getMainLooper() ) { protected var appContext: Context = cxt.applicationContext @@ -30,13 +37,13 @@ abstract class ServiceConnector protected constructor( private val state = AtomicInteger(STATE_DISCONNECTED) private var connectStartTime: Long = 0 private var serviceConnection: ServiceConnection? = null - private var service: IServiceInterface? = null - /** - * Get the looper used to connect/reconnect target Service. - * By default, it's the main looper. - */ - fun getConnectLooper(): Looper = Looper.getMainLooper() + var service: IServiceInterface? = null + private set + + @ConnectState + val connectState: Int + get() = state.get() /** * Get Intent to bind the target service. @@ -86,11 +93,6 @@ abstract class ServiceConnector protected constructor( } else null } - @ConnectState - fun getConnectState(): Int = state.get() - - fun getService(): IServiceInterface? = service - /** * Add a connect state listener, using [WeakListenerManager] to manager listeners. * Callbacks will be invoked in [.getConnectLooper] thread. @@ -111,6 +113,7 @@ abstract class ServiceConnector protected constructor( Timber.tag(TAG).i("[%s] disconnect service...", serviceName) connectHandler.removeMessages(MSG_CONNECT_TIMEOUT_CHECK) connectHandler.removeMessages(MSG_RECONNECT) + connectHandler.removeMessages(MSG_NOTIFY_LISTENERS) service = null if (serviceConnection != null) { appContext.unbindService(serviceConnection!!) @@ -251,8 +254,7 @@ abstract class ServiceConnector protected constructor( } } - private val connectHandler = object : Handler(getConnectLooper()) { - + private val connectHandler = object : Handler(connectLooper) { override fun handleMessage(msg: Message) { when (msg.what) { MSG_RECONNECT -> { @@ -307,7 +309,7 @@ abstract class ServiceConnector protected constructor( private const val MSG_CONNECT_TIMEOUT_CHECK = 3 private const val CONNECT_TIMEOUT_CHECK_INTERVAL: Long = 5000 // 5s - private const val FORCE_REBIND_TIME = (30 * 1000).toLong() // 30 seconds + private const val FORCE_REBIND_TIME: Long = 30 * 1000 // 30 seconds fun strConnectState(state: Int): String { return when (state) { diff --git a/build.gradle b/build.gradle index 49a48e0..44c80d2 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' From e7125823f9f71de5ed83c0c50ee57aed98ed7109 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 28 Jul 2019 22:34:42 +0800 Subject: [PATCH 003/100] Fix issue#3: Custom lint rules failed to work caused by Android X migrating --- android_project_common.gradle | 2 +- archLintRules/build.gradle | 13 +- .../arch/lint/MyBaseActivityDetector.java | 46 -- .../arch/lint/MyBaseActivityDetector.kt | 38 + .../arch/lint/MyBroadcastHelperDetector.java | 50 -- .../arch/lint/MyBroadcastHelperDetector.kt | 36 + .../arch/lint/MyIntentHelperDetector.java | 84 --- .../arch/lint/MyIntentHelperDetector.kt | 70 ++ .../android/arch/lint/MyIssueRegistry.java | 25 - .../android/arch/lint/MyIssueRegistry.kt | 21 + .../arch/lint/MyToastHelperDetector.java | 48 -- .../arch/lint/MyToastHelperDetector.kt | 35 + .../arch/lint/base/InheritDetectorBase.java | 47 -- .../arch/lint/base/InheritDetectorBase.kt | 35 + .../arch/lint/base/WrapperDetectorBase.java | 56 -- .../arch/lint/base/WrapperDetectorBase.kt | 41 ++ .../arch/lint/MyBaseActivityDetectorTest.java | 189 ----- .../arch/lint/MyBaseActivityDetectorTest.kt | 378 ++++++++++ .../lint/MyBroadcastHelperDetectorTest.java | 158 ----- .../lint/MyBroadcastHelperDetectorTest.kt | 323 +++++++++ .../arch/lint/MyIntentHelperDetectorTest.java | 82 --- .../arch/lint/MyIntentHelperDetectorTest.kt | 163 +++++ .../arch/lint/MyToastHelperDetectorTest.java | 71 -- .../arch/lint/MyToastHelperDetectorTest.kt | 141 ++++ .../arch/lint/utils/TestFileStubs.java | 627 ----------------- .../android/arch/lint/utils/TestFileStubs.kt | 649 ++++++++++++++++++ 26 files changed, 1940 insertions(+), 1488 deletions(-) delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.kt delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.kt delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.kt delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.kt delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt delete mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.java create mode 100644 archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.kt delete mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.java create mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt delete mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.java create mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.kt delete mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.java create mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.kt delete mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.java create mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.kt delete mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.java create mode 100644 archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.kt diff --git a/android_project_common.gradle b/android_project_common.gradle index bf6e101..3f92c15 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -74,7 +74,7 @@ ext { 'guava' : "23.5-android", // debug - 'leakcanary' : "1.5.4", + 'leakcanary' : "1.6.3", 'stetho' : "1.5.0", 'ktlint' : "0.29.0", diff --git a/archLintRules/build.gradle b/archLintRules/build.gradle index 9fdd98a..0215a7a 100644 --- a/archLintRules/build.gradle +++ b/archLintRules/build.gradle @@ -1,7 +1,4 @@ -apply plugin: 'java-library' - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +apply plugin: 'kotlin' dependencies { compileOnly "com.android.tools.lint:lint-api:${versions.lintLib}" @@ -18,3 +15,11 @@ jar { attributes("Lint-Registry-v2": "me.ycdev.android.arch.lint.MyIssueRegistry") } } + +test { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen { false } + showStandardStreams = true + } +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.java deleted file mode 100644 index acf860a..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.java +++ /dev/null @@ -1,46 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.jetbrains.uast.UElement; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; - -import me.ycdev.android.arch.lint.base.InheritDetectorBase; - -public class MyBaseActivityDetector extends InheritDetectorBase { - static final Issue ISSUE = Issue.create( - "MyBaseActivity", - "Base classes for Activity should be used.", - "Please use the base classes for Activity." - + " So that we can do some unified behaviors.", - Category.CORRECTNESS, 5, Severity.ERROR, - new Implementation(MyBaseActivityDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - protected HashSet getWrapperClasses() { - HashSet sets = new HashSet<>(); - sets.add("me.ycdev.android.arch.activity.BaseActivity"); - sets.add("me.ycdev.android.arch.activity.PreferenceBaseActivity"); - sets.add("me.ycdev.android.arch.activity.AppCompatBaseActivity"); - return sets; - } - - @Override - public List applicableSuperClasses() { - return Collections.singletonList("android.app.Activity"); - } - - @Override - protected void reportViolation(JavaContext context, UElement element) { - context.report(ISSUE, element, context.getNameLocation(element), - "Please use the base classes for Activity."); - } -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.kt new file mode 100644 index 0000000..2cc65a2 --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBaseActivityDetector.kt @@ -0,0 +1,38 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import me.ycdev.android.arch.lint.base.InheritDetectorBase +import org.jetbrains.uast.UElement +import java.util.HashSet + +class MyBaseActivityDetector : InheritDetectorBase() { + override val applicableClasses: List = arrayListOf("android.app.Activity") + + override val wrapperClasses: HashSet = hashSetOf( + "me.ycdev.android.arch.activity.BaseActivity", + "me.ycdev.android.arch.activity.PreferenceBaseActivity", + "me.ycdev.android.arch.activity.AppCompatBaseActivity" + ) + + override fun reportViolation(context: JavaContext, element: UElement) { + context.report( + ISSUE, element, context.getNameLocation(element), + "Please use the base classes for Activity." + ) + } + + companion object { + internal val ISSUE = Issue.create( + "MyBaseActivity", + "Base classes for Activity should be used.", + "Please use the base classes for Activity." + " So that we can do some unified behaviors.", + Category.CORRECTNESS, 5, Severity.ERROR, + Implementation(MyBaseActivityDetector::class.java, Scope.JAVA_FILE_SCOPE) + ) + } +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.java deleted file mode 100644 index c9a05e1..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.jetbrains.uast.UElement; - -import java.util.Arrays; -import java.util.List; - -import me.ycdev.android.arch.lint.base.WrapperDetectorBase; - -public class MyBroadcastHelperDetector extends WrapperDetectorBase { - static final Issue ISSUE = Issue.create( - "MyBroadcastHelper", - "BroadcastHelper should be used.", - "Please use the wrapper class 'BroadcastHelper' to register broadcast receivers" - + " and send broadcasts to avoid security issues.", - Category.CORRECTNESS, 5, Severity.ERROR, - new Implementation(MyBroadcastHelperDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - protected String getWrapperClassName() { - return "me.ycdev.android.lib.common.wrapper.BroadcastHelper"; - } - - @Override - protected String[] getTargetClassNames() { - return new String[] { - "android.content.Context" - }; - } - - @Override - public List getApplicableMethodNames() { - return Arrays.asList( - "registerReceiver", - "sendBroadcast"); - } - - @Override - protected void reportViolation(JavaContext context, UElement element) { - context.report(ISSUE, element, context.getLocation(element), - "Please use the wrapper class 'BroadcastHelper'."); - } -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt new file mode 100644 index 0000000..959321c --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt @@ -0,0 +1,36 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import me.ycdev.android.arch.lint.base.WrapperDetectorBase +import org.jetbrains.uast.UElement + +class MyBroadcastHelperDetector : WrapperDetectorBase() { + override val applicableMethods = arrayListOf("registerReceiver", "sendBroadcast") + + override val wrapperClassName = "me.ycdev.android.lib.common.wrapper.BroadcastHelper" + + override val targetClassNames = arrayOf("android.content.Context") + + override fun reportViolation(context: JavaContext, element: UElement) { + context.report( + ISSUE, element, context.getLocation(element), + "Please use the wrapper class 'BroadcastHelper'." + ) + } + + companion object { + internal val ISSUE = Issue.create( + "MyBroadcastHelper", + "BroadcastHelper should be used.", + "Please use the wrapper class 'BroadcastHelper' to register broadcast receivers" + + " and send broadcasts to avoid security issues.", + Category.CORRECTNESS, 5, Severity.ERROR, + Implementation(MyBroadcastHelperDetector::class.java, Scope.JAVA_FILE_SCOPE) + ) + } +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.java deleted file mode 100644 index cacb0fe..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.java +++ /dev/null @@ -1,84 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.jetbrains.uast.UElement; - -import java.util.Arrays; -import java.util.List; - -import me.ycdev.android.arch.lint.base.WrapperDetectorBase; - -public class MyIntentHelperDetector extends WrapperDetectorBase { - static final Issue ISSUE = Issue.create( - "MyIntentHelper", - "IntentHelper should be used.", - "Please use the wrapper class 'IntentHelper' to get Intent extras" - + " to avoid security issues.", - Category.CORRECTNESS, 5, Severity.ERROR, - new Implementation(MyIntentHelperDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - protected String getWrapperClassName() { - return "me.ycdev.android.lib.common.wrapper.IntentHelper"; - } - - @Override - protected String[] getTargetClassNames() { - return new String[] { - "android.content.Intent" - }; - } - - @Override - public List getApplicableMethodNames() { - return Arrays.asList( - "hasExtra", - "getBooleanArrayExtra", - "getBooleanExtra", - "getBundleExtra", - "getByteArrayExtra", - "getByteExtra", - "getCharArrayExtra", - "getCharExtra", - "getCharSequenceArrayExtra", - "getCharSequenceArrayListExtra", - "getCharSequenceExtra", - "getDoubleArrayExtra", - "getDoubleExtra", - "getExtra", - "getExtras", - "getFloatArrayExtra", - "getFloatExtra", - "getIBinderExtra", - "getIntArrayExtra", - "getIBinderExtra", - "getIntArrayExtra", - "getIntegerArrayListExtra", - "getIntExtra", - "getLongArrayExtra", - "getLongExtra", - "getParcelableArrayExtra", - "getParcelableArrayListExtra", - "getParcelableExtra", - "getSerializableExtra", - "getShortArrayExtra", - "getShortExtra", - "getStringArrayExtra", - "getStringArrayListExtra", - "getStringExtra" - ); - } - - @Override - protected void reportViolation(JavaContext context, UElement element) { - context.report(ISSUE, element, context.getLocation(element), - "Please use the wrapper class 'IntentHelper'."); - } - -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.kt new file mode 100644 index 0000000..8d86747 --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIntentHelperDetector.kt @@ -0,0 +1,70 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import me.ycdev.android.arch.lint.base.WrapperDetectorBase +import org.jetbrains.uast.UElement + +class MyIntentHelperDetector : WrapperDetectorBase() { + override val applicableMethods = arrayListOf( + "hasExtra", + "getBooleanArrayExtra", + "getBooleanExtra", + "getBundleExtra", + "getByteArrayExtra", + "getByteExtra", + "getCharArrayExtra", + "getCharExtra", + "getCharSequenceArrayExtra", + "getCharSequenceArrayListExtra", + "getCharSequenceExtra", + "getDoubleArrayExtra", + "getDoubleExtra", + "getExtra", + "getExtras", + "getFloatArrayExtra", + "getFloatExtra", + "getIBinderExtra", + "getIntArrayExtra", + "getIBinderExtra", + "getIntArrayExtra", + "getIntegerArrayListExtra", + "getIntExtra", + "getLongArrayExtra", + "getLongExtra", + "getParcelableArrayExtra", + "getParcelableArrayListExtra", + "getParcelableExtra", + "getSerializableExtra", + "getShortArrayExtra", + "getShortExtra", + "getStringArrayExtra", + "getStringArrayListExtra", + "getStringExtra" + ) + + override val wrapperClassName = "me.ycdev.android.lib.common.wrapper.IntentHelper" + + override val targetClassNames = arrayOf("android.content.Intent") + + override fun reportViolation(context: JavaContext, element: UElement) { + context.report( + ISSUE, element, context.getLocation(element), + "Please use the wrapper class 'IntentHelper'." + ) + } + + companion object { + internal val ISSUE = Issue.create( + "MyIntentHelper", + "IntentHelper should be used.", + "Please use the wrapper class 'IntentHelper' to get Intent extras" + " to avoid security issues.", + Category.CORRECTNESS, 5, Severity.ERROR, + Implementation(MyIntentHelperDetector::class.java, Scope.JAVA_FILE_SCOPE) + ) + } +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.java deleted file mode 100644 index d0bee7e..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.detector.api.Issue; - -import java.util.Arrays; -import java.util.List; - -public class MyIssueRegistry extends IssueRegistry { - @Override - public List getIssues() { - System.out.println("!!!!!!!!!!!!! ArchLib lint rules works"); - return Arrays.asList( - MyToastHelperDetector.ISSUE, - MyBroadcastHelperDetector.ISSUE, - MyBaseActivityDetector.ISSUE, - MyIntentHelperDetector.ISSUE - ); - } - - @Override - public int getApi() { - return com.android.tools.lint.detector.api.ApiKt.CURRENT_API; - } -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.kt new file mode 100644 index 0000000..fe84903 --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyIssueRegistry.kt @@ -0,0 +1,21 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.detector.api.Issue + +import java.util.Arrays + +class MyIssueRegistry : IssueRegistry() { + override val issues: List + get() { + println("!!!!!!!!!!!!! ArchLib lint rules works") + return Arrays.asList( + MyToastHelperDetector.ISSUE, + MyBroadcastHelperDetector.ISSUE, + MyBaseActivityDetector.ISSUE, + MyIntentHelperDetector.ISSUE + ) + } + + override val api: Int = com.android.tools.lint.detector.api.CURRENT_API +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.java deleted file mode 100644 index 1d03f4c..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.java +++ /dev/null @@ -1,48 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Implementation; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.jetbrains.uast.UElement; - -import java.util.Collections; -import java.util.List; - -import me.ycdev.android.arch.lint.base.WrapperDetectorBase; - -public class MyToastHelperDetector extends WrapperDetectorBase { - static final Issue ISSUE = Issue.create( - "MyToastHelper", - "ToastHelper should be used.", - "Please use the wrapper class 'ToastHelper' to show toast." - + " So that we can customize and unify the UI in future.", - Category.CORRECTNESS, 5, Severity.ERROR, - new Implementation(MyToastHelperDetector.class, Scope.JAVA_FILE_SCOPE)); - - @Override - protected String getWrapperClassName() { - return "me.ycdev.android.arch.wrapper.ToastHelper"; - } - - @Override - protected String[] getTargetClassNames() { - return new String[] { - "android.widget.Toast" - }; - } - - @Override - public List getApplicableMethodNames() { - return Collections.singletonList("makeText"); - } - - @Override - protected void reportViolation(JavaContext context, UElement element) { - context.report(ISSUE, element, context.getLocation(element), - "Please use the wrapper class 'ToastHelper'."); - } -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.kt new file mode 100644 index 0000000..6abbb2f --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyToastHelperDetector.kt @@ -0,0 +1,35 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import me.ycdev.android.arch.lint.base.WrapperDetectorBase +import org.jetbrains.uast.UElement + +class MyToastHelperDetector : WrapperDetectorBase() { + override val applicableMethods = arrayListOf("makeText") + + override val wrapperClassName = "me.ycdev.android.arch.wrapper.ToastHelper" + + override val targetClassNames = arrayOf("android.widget.Toast") + + override fun reportViolation(context: JavaContext, element: UElement) { + context.report( + ISSUE, element, context.getLocation(element), + "Please use the wrapper class 'ToastHelper'." + ) + } + + companion object { + internal val ISSUE = Issue.create( + "MyToastHelper", + "ToastHelper should be used.", + "Please use the wrapper class 'ToastHelper' to show toast." + " So that we can customize and unify the UI in future.", + Category.CORRECTNESS, 5, Severity.ERROR, + Implementation(MyToastHelperDetector::class.java, Scope.JAVA_FILE_SCOPE) + ) + } +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.java deleted file mode 100644 index a80fe0e..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.java +++ /dev/null @@ -1,47 +0,0 @@ -package me.ycdev.android.arch.lint.base; - -import com.android.tools.lint.client.api.JavaEvaluator; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.JavaContext; - -import org.jetbrains.uast.UClass; -import org.jetbrains.uast.UElement; - -import java.util.HashSet; -import java.util.List; - -public abstract class InheritDetectorBase extends Detector implements Detector.UastScanner { - private HashSet mWrapperClasses; - - /** Constructs a new {@link InheritDetectorBase} check */ - public InheritDetectorBase() { - mWrapperClasses = getWrapperClasses(); - } - - protected abstract HashSet getWrapperClasses(); - - @Override - public abstract List applicableSuperClasses(); - - protected abstract void reportViolation(JavaContext context, UElement element); - - @Override - public void visitClass(JavaContext context, UClass cls) { - String className = cls.getQualifiedName(); - if (mWrapperClasses.contains(className)) { - return; // ignore the wrapper classes - } - - JavaEvaluator evaluator = context.getEvaluator(); - boolean found = false; - for (String wrapperClass : mWrapperClasses) { - if (evaluator.inheritsFrom(cls, wrapperClass, false)) { - found = true; - break; - } - } - if (!found) { - reportViolation(context, cls); - } - } -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt new file mode 100644 index 0000000..a980b4f --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt @@ -0,0 +1,35 @@ +package me.ycdev.android.arch.lint.base + +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UElement +import java.util.HashSet + +abstract class InheritDetectorBase : Detector(), Detector.UastScanner { + protected abstract val applicableClasses: List + protected abstract val wrapperClasses: HashSet + protected abstract fun reportViolation(context: JavaContext, element: UElement) + + override fun applicableSuperClasses(): List? = applicableClasses + + override fun visitClass(context: JavaContext, declaration: UClass) { + val wrappers = wrapperClasses + val className = declaration.qualifiedName + if (wrappers.contains(className)) { + return // ignore the wrapper classes + } + + val evaluator = context.evaluator + var found = false + for (wrapperClass in wrappers) { + if (evaluator.inheritsFrom(declaration, wrapperClass, false)) { + found = true + break + } + } + if (!found) { + reportViolation(context, declaration) + } + } +} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.java b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.java deleted file mode 100644 index 9065d4b..0000000 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.java +++ /dev/null @@ -1,56 +0,0 @@ -package me.ycdev.android.arch.lint.base; - -import com.android.tools.lint.client.api.JavaEvaluator; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.JavaContext; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiMethod; - -import org.jetbrains.uast.UCallExpression; -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.UastUtils; - -import java.util.List; - -public abstract class WrapperDetectorBase extends Detector implements Detector.UastScanner { - private String mWrapperClassName; - private String[] mTargetClassNames; - - /** Constructs a new {@link WrapperDetectorBase} check */ - public WrapperDetectorBase() { - mWrapperClassName = getWrapperClassName(); - mTargetClassNames = getTargetClassNames(); - } - - protected abstract String getWrapperClassName(); - - protected abstract String[] getTargetClassNames(); - - protected abstract void reportViolation(JavaContext context, UElement element); - - @Override - public abstract List getApplicableMethodNames(); - - @Override - public void visitMethod(JavaContext context, UCallExpression call, PsiMethod method) { - JavaEvaluator evaluator = context.getEvaluator(); - PsiClass surroundingClass = UastUtils.getContainingClass(call); - if (surroundingClass == null) { - System.out.println("Fatal error in WrapperDetectorBase! Failed to get surrounding" + - " class \'" + call.getUastParent() + "\'"); - return; - } - - String containingClassName = surroundingClass.getQualifiedName(); - if (mWrapperClassName.equals(containingClassName)) { - return; - } - - for (String targetClassName : mTargetClassNames) { - if (evaluator.isMemberInSubClassOf(method, targetClassName, false)) { - reportViolation(context, call); - return; - } - } - } -} diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.kt new file mode 100644 index 0000000..3b04eba --- /dev/null +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/WrapperDetectorBase.kt @@ -0,0 +1,41 @@ +package me.ycdev.android.arch.lint.base + +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.getContainingClass + +abstract class WrapperDetectorBase : Detector(), Detector.UastScanner { + protected abstract val applicableMethods: List + protected abstract val wrapperClassName: String + protected abstract val targetClassNames: Array + protected abstract fun reportViolation(context: JavaContext, element: UElement) + + override fun getApplicableMethodNames(): List = applicableMethods + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + val surroundingClass = node.getContainingClass() + if (surroundingClass == null) { + println( + "Fatal error in WrapperDetectorBase! Failed to get surrounding" + + " class \'" + node.uastParent + "\'" + ) + return + } + + val containingClassName = surroundingClass.qualifiedName + if (wrapperClassName == containingClassName) { + return + } + + for (targetClassName in targetClassNames) { + if (evaluator.isMemberInSubClassOf(method, targetClassName, false)) { + reportViolation(context, node) + return + } + } + } +} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.java b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.java deleted file mode 100644 index 89c7b50..0000000 --- a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.java +++ /dev/null @@ -1,189 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; -import com.android.tools.lint.checks.infrastructure.TestFiles; - -import org.junit.Test; - -import me.ycdev.android.arch.lint.utils.TestFileStubs; - -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; - -public class MyBaseActivityDetectorTest { - @Test - public void testLintGoodActivity() { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "import android.os.Bundle;\n" + - "import android.view.Menu;\n" + - "import android.view.MenuItem;\n" + - "\n" + - "import me.ycdev.android.arch.activity.AppCompatBaseActivity;\n" + - "\n" + - "\n" + - "public class LintGoodActivity extends AppCompatBaseActivity { // lint good\n" + - "\n" + - " @Override\n" + - " protected void onCreate(Bundle savedInstanceState) {\n" + - " super.onCreate(savedInstanceState);\n" + - " }\n" + - "\n" + - " @Override\n" + - " public boolean onCreateOptionsMenu(Menu menu) {\n" + - " // Inflate the menu; this adds items to the action bar if it is present.\n" + - " return true;\n" + - " }\n" + - "\n" + - " @Override\n" + - " public boolean onOptionsItemSelected(MenuItem item) {\n" + - " // Handle action bar item clicks here. The action bar will\n" + - " // automatically handle clicks on the Home/Up button, so long\n" + - " // as you specify a parent activity in AndroidManifest.xml.\n" + - " int id = item.getItemId();\n" + - "\n" + - " return super.onOptionsItemSelected(item);\n" + - " }\n" + - "}\n"); - lint().files(TestFileStubs.getAppCompatBaseActivity(), testFile) - .issues(MyBaseActivityDetector.ISSUE) - .run() - .expectClean(); - } - - @Test - public void testLintGood2Activity() { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "import android.os.Bundle;\n" + - "\n" + - "import me.ycdev.android.arch.activity.BaseActivity;\n" + - "\n" + - "public class LintGood2Activity extends BaseActivity { // lint good\n" + - " @Override\n" + - " protected void onCreate(Bundle savedInstanceState) {\n" + - " super.onCreate(savedInstanceState);\n" + - " }\n" + - "}\n"); - lint().files(TestFileStubs.getBaseActivity(), testFile) - .issues(MyBaseActivityDetector.ISSUE) - .run() - .expectClean(); - } - - @Test - public void testLintGood3Activity() { - TestFile good2File = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "import android.os.Bundle;\n" + - "\n" + - "import me.ycdev.android.arch.activity.BaseActivity;\n" + - "\n" + - "public class LintGood2Activity extends BaseActivity { // lint good\n" + - " @Override\n" + - " protected void onCreate(Bundle savedInstanceState) {\n" + - " super.onCreate(savedInstanceState);\n" + - " }\n" + - "}\n"); - TestFile good3File = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "public class LintGood3Activity extends LintGood2Activity { // lint good\n" + - " // nothing to do\n" + - "}\n"); - lint().files(TestFileStubs.getBaseActivity(), good2File, good3File) - .issues(MyBaseActivityDetector.ISSUE) - .run() - .expectClean(); - } - - @Test - public void testLintViolationActivity() { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "import android.content.BroadcastReceiver;\n" + - "import android.content.Context;\n" + - "import android.content.Intent;\n" + - "import android.content.IntentFilter;\n" + - "import android.os.Bundle;\n" + - "import android.support.v7.app.AppCompatActivity;\n" + - "import android.view.MenuItem;\n" + - "\n" + - "\n" + - "/**\n" + - " * Class doc for test\n" + - " */\n" + - "public class LintViolationActivity extends AppCompatActivity { // lint violation\n" + - " private static final String TEST_ACTION = \"action.test\";\n" + - "\n" + - " private BroadcastReceiver mReceiver = new BroadcastReceiver() {\n" + - " @Override\n" + - " public void onReceive(Context context, Intent intent) {\n" + - " // nothing to do\n" + - " }\n" + - " };\n" + - "\n" + - " @Override\n" + - " protected void onCreate(Bundle savedInstanceState) {\n" + - " super.onCreate(savedInstanceState);\n" + - "\n" + - "\n" + - " IntentFilter filter = new IntentFilter();\n" + - " filter.addAction(TEST_ACTION);\n" + - " registerReceiver(mReceiver, filter); // lint violation\n" + - " }\n" + - "\n" + - " @Override\n" + - " public boolean onOptionsItemSelected(MenuItem item) {\n" + - " // Handle action bar item clicks here. The action bar will\n" + - " // automatically handle clicks on the Home/Up button, so long\n" + - " // as you specify a parent activity in AndroidManifest.xml.\n" + - " int id = item.getItemId();\n" + - "\n" + - " sendBroadcast(new Intent(TEST_ACTION)); // lint violation\n" + - "\n" + - " return super.onOptionsItemSelected(item);\n" + - " }\n" + - "\n" + - " @Override\n" + - " protected void onDestroy() {\n" + - " super.onDestroy();\n" + - " unregisterReceiver(mReceiver);\n" + - " }\n" + - "}\n"); - lint().files(TestFileStubs.getAppCompatActivity(), testFile) - .issues(MyBaseActivityDetector.ISSUE) - .run() - .expect("src/me/ycdev/android/arch/demo/activity/LintViolationActivity.java:15: Error: Please use the base classes for Activity. [MyBaseActivity]\n" + - "public class LintViolationActivity extends AppCompatActivity { // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings\n"); - } - - @Test - public void testLintViolation2Activity() { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "import android.app.Activity;\n" + - "import android.os.Bundle;\n" + - "\n" + - "// class comment for test\n" + - "public class LintViolation2Activity extends Activity { // lint violation\n" + - " @Override\n" + - " protected void onCreate(Bundle savedInstanceState) {\n" + - " super.onCreate(savedInstanceState);\n" + - " }\n" + - "}\n"); - lint().files(testFile) - .issues(MyBaseActivityDetector.ISSUE) - .run() - .expect("src/me/ycdev/android/arch/demo/activity/LintViolation2Activity.java:7: Error: Please use the base classes for Activity. [MyBaseActivity]\n" + - "public class LintViolation2Activity extends Activity { // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~\n" + - "1 errors, 0 warnings\n"); - } -} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt new file mode 100644 index 0000000..e7e177a --- /dev/null +++ b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt @@ -0,0 +1,378 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import me.ycdev.android.arch.lint.utils.TestFileStubs +import org.junit.Test + +class MyBaseActivityDetectorTest { + @Test + fun testLintGoodActivity_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "import android.os.Bundle;\n" + + "import android.view.Menu;\n" + + "import android.view.MenuItem;\n" + + "\n" + + "import me.ycdev.android.arch.activity.AppCompatBaseActivity;\n" + + "\n" + + "\n" + + "public class LintGoodActivity extends AppCompatBaseActivity { // lint good\n" + + "\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean onCreateOptionsMenu(Menu menu) {\n" + + " // Inflate the menu; this adds items to the action bar if it is present.\n" + + " return true;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean onOptionsItemSelected(MenuItem item) {\n" + + " // Handle action bar item clicks here. The action bar will\n" + + " // automatically handle clicks on the Home/Up button, so long\n" + + " // as you specify a parent activity in AndroidManifest.xml.\n" + + " int id = item.getItemId();\n" + + "\n" + + " return super.onOptionsItemSelected(item);\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.appCompatBaseActivity, testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testLintGoodActivity_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "import android.os.Bundle\n" + + "import android.view.Menu\n" + + "import android.view.MenuItem\n" + + "\n" + + "import me.ycdev.android.arch.activity.AppCompatBaseActivity\n" + + "\n" + + "class LintGoodActivity : AppCompatBaseActivity() { // lint good\n" + + "\n" + + " override fun onCreate(savedInstanceState: Bundle?) {\n" + + " super.onCreate(savedInstanceState)\n" + + " }\n" + + "\n" + + " override fun onCreateOptionsMenu(menu: Menu): Boolean {\n" + + " // Inflate the menu; this adds items to the action bar if it is present.\n" + + " return true\n" + + " }\n" + + "\n" + + " override fun onOptionsItemSelected(item: MenuItem): Boolean {\n" + + " // Handle action bar item clicks here. The action bar will\n" + + " // automatically handle clicks on the Home/Up button, so long\n" + + " // as you specify a parent activity in AndroidManifest.xml.\n" + + " val id = item.itemId\n" + + "\n" + + " return super.onOptionsItemSelected(item)\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.appCompatBaseActivity, testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testLintGood2Activity_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "import android.os.Bundle;\n" + + "\n" + + "import me.ycdev.android.arch.activity.BaseActivity;\n" + + "\n" + + "public class LintGood2Activity extends BaseActivity { // lint good\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.baseActivity, testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testLintGood2Activity_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "import android.os.Bundle\n" + + "\n" + + "import me.ycdev.android.arch.activity.BaseActivity\n" + + "\n" + + "open class LintGood2Activity : BaseActivity() { // lint good\n" + + " override fun onCreate(savedInstanceState: Bundle?) {\n" + + " super.onCreate(savedInstanceState)\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.baseActivity, testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testLintGood3Activity_java() { + val good2File = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "import android.os.Bundle;\n" + + "\n" + + "import me.ycdev.android.arch.activity.BaseActivity;\n" + + "\n" + + "public class LintGood2Activity extends BaseActivity { // lint good\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + " }\n" + + "}\n" + ) + val good3File = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "public class LintGood3Activity extends LintGood2Activity { // lint good\n" + + " // nothing to do\n" + + "}\n" + ) + lint().files(TestFileStubs.baseActivity, good2File, good3File) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testLintGood3Activity_kotlin() { + val good2File = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "import android.os.Bundle\n" + + "\n" + + "import me.ycdev.android.arch.activity.BaseActivity\n" + + "\n" + + "open class LintGood2Activity : BaseActivity() { // lint good\n" + + " override fun onCreate(savedInstanceState: Bundle?) {\n" + + " super.onCreate(savedInstanceState)\n" + + " }\n" + + "}\n" + ) + val good3File = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "class LintGood3Activity : LintGood2Activity() // lint good\n" + + "// nothing to do\n" + ) + lint().files(TestFileStubs.baseActivity, good2File, good3File) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testLintViolationActivity_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "import android.content.BroadcastReceiver;\n" + + "import android.content.Context;\n" + + "import android.content.Intent;\n" + + "import android.content.IntentFilter;\n" + + "import android.os.Bundle;\n" + + "import android.support.v7.app.AppCompatActivity;\n" + + "import android.view.MenuItem;\n" + + "\n" + + "\n" + + "/**\n" + + " * Class doc for test\n" + + " */\n" + + "public class LintViolationActivity extends AppCompatActivity { // lint violation\n" + + " private static final String TEST_ACTION = \"action.test\";\n" + + "\n" + + " private BroadcastReceiver mReceiver = new BroadcastReceiver() {\n" + + " @Override\n" + + " public void onReceive(Context context, Intent intent) {\n" + + " // nothing to do\n" + + " }\n" + + " };\n" + + "\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + "\n" + + "\n" + + " IntentFilter filter = new IntentFilter();\n" + + " filter.addAction(TEST_ACTION);\n" + + " registerReceiver(mReceiver, filter); // lint violation\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean onOptionsItemSelected(MenuItem item) {\n" + + " // Handle action bar item clicks here. The action bar will\n" + + " // automatically handle clicks on the Home/Up button, so long\n" + + " // as you specify a parent activity in AndroidManifest.xml.\n" + + " int id = item.getItemId();\n" + + "\n" + + " sendBroadcast(new Intent(TEST_ACTION)); // lint violation\n" + + "\n" + + " return super.onOptionsItemSelected(item);\n" + + " }\n" + + "\n" + + " @Override\n" + + " protected void onDestroy() {\n" + + " super.onDestroy();\n" + + " unregisterReceiver(mReceiver);\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.appCompatActivity, testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.java:15: Error: Please use the base classes for Activity. [MyBaseActivity]\n" + + "public class LintViolationActivity extends AppCompatActivity { // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~\n" + + "1 errors, 0 warnings\n" + ) + } + + + @Test + fun testLintViolationActivity_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "import android.content.BroadcastReceiver\n" + + "import android.content.Context\n" + + "import android.content.Intent\n" + + "import android.content.IntentFilter\n" + + "import android.os.Bundle\n" + + "import androidx.appcompat.app.AppCompatActivity\n" + + "import android.view.MenuItem\n" + + "\n" + + "/**\n" + + " * Class doc for test\n" + + " */\n" + + "class LintViolationActivity : AppCompatActivity() { // lint violation\n" + + "\n" + + " private val receiver = object : BroadcastReceiver() {\n" + + " override fun onReceive(context: Context, intent: Intent) {\n" + + " // nothing to do\n" + + " }\n" + + " }\n" + + "\n" + + " override fun onCreate(savedInstanceState: Bundle?) {\n" + + " super.onCreate(savedInstanceState)\n" + + "\n" + + " val filter = IntentFilter()\n" + + " filter.addAction(TEST_ACTION)\n" + + " registerReceiver(receiver, filter) // lint violation\n" + + " }\n" + + "\n" + + " override fun onOptionsItemSelected(item: MenuItem): Boolean {\n" + + " // Handle action bar item clicks here. The action bar will\n" + + " // automatically handle clicks on the Home/Up button, so long\n" + + " // as you specify a parent activity in AndroidManifest.xml.\n" + + " val id = item.itemId\n" + + "\n" + + " sendBroadcast(Intent(TEST_ACTION)) // lint violation\n" + + "\n" + + " return super.onOptionsItemSelected(item)\n" + + " }\n" + + "\n" + + " override fun onDestroy() {\n" + + " super.onDestroy()\n" + + " unregisterReceiver(receiver)\n" + + " }\n" + + "\n" + + " companion object {\n" + + " private val TEST_ACTION = \"action.test\"\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.appCompatActivityAndroidX, testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt:14: Error: Please use the base classes for Activity. [MyBaseActivity]\n" + + "class LintViolationActivity : AppCompatActivity() { // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~\n" + + "1 errors, 0 warnings" + ) + } + + @Test + fun testLintViolation2Activity_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "import android.app.Activity;\n" + + "import android.os.Bundle;\n" + + "\n" + + "// class comment for test\n" + + "public class LintViolation2Activity extends Activity { // lint violation\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + " }\n" + + "}\n" + ) + lint().files(testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/activity/LintViolation2Activity.java:7: Error: Please use the base classes for Activity. [MyBaseActivity]\n" + + "public class LintViolation2Activity extends Activity { // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~\n" + + "1 errors, 0 warnings\n" + ) + } + + @Test + fun testLintViolation2Activity_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "import android.app.Activity\n" + + "import android.os.Bundle\n" + + "\n" + + "// class comment for test\n" + + "class LintViolation2Activity : Activity() { // lint violation\n" + + " override fun onCreate(savedInstanceState: Bundle?) {\n" + + " super.onCreate(savedInstanceState)\n" + + " }\n" + + "}\n" + ) + lint().files(testFile) + .issues(MyBaseActivityDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/activity/LintViolation2Activity.kt:7: Error: Please use the base classes for Activity. [MyBaseActivity]\n" + + "class LintViolation2Activity : Activity() { // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~\n" + + "1 errors, 0 warnings" + ) + } +} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.java b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.java deleted file mode 100644 index b35dbce..0000000 --- a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; -import com.android.tools.lint.checks.infrastructure.TestFiles; - -import org.junit.Test; - -import me.ycdev.android.arch.lint.utils.TestFileStubs; - -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; - -public class MyBroadcastHelperDetectorTest { - @Test - public void testBroadcastHelperLintCase() throws Exception { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.wrapper;\n" + - "\n" + - "import android.content.BroadcastReceiver;\n" + - "import android.content.Context;\n" + - "import android.content.Intent;\n" + - "import android.content.IntentFilter;\n" + - "\n" + - "import me.ycdev.android.lib.common.wrapper.BroadcastHelper;\n" + - "\n" + - "public class BroadcastHelperLintCase {\n" + - " private static class Foo {\n" + - " public void registerReceiver() { // lint good\n" + - " }\n" + - "\n" + - " public void sendBroadcast() { // lint good\n" + - " }\n" + - " }\n" + - "\n" + - " public static void registerReceiver() { // lint good\n" + - " new Foo().registerReceiver();\n" + - " }\n" + - "\n" + - " public static void sendBroadcast() { // lint good\n" + - " new Foo().sendBroadcast();\n" + - " }\n" + - "\n" + - " public static Intent registerGood(Context cxt, BroadcastReceiver receiver, IntentFilter filter) {\n" + - " return BroadcastHelper.registerForInternal(cxt, receiver, filter); // lint good\n" + - " }\n" + - "\n" + - " public static void sendToInternalGood(Context cxt, Intent intent) {\n" + - " BroadcastHelper.sendToInternal(cxt, intent); // lint good\n" + - " }\n" + - "\n" + - " public static void sendToExternalGood(Context cxt, Intent intent, String perm) {\n" + - " BroadcastHelper.sendToExternal(cxt, intent, perm); // lint good\n" + - " }\n" + - "\n" + - " public static void sendToExternal(Context cxt, Intent intent) {\n" + - " BroadcastHelper.sendToExternal(cxt, intent); // lint good\n" + - " }\n" + - "\n" + - " public static Intent registerViolation(Context cxt, BroadcastReceiver receiver, IntentFilter filter) {\n" + - " return cxt.registerReceiver(receiver, filter); // lint violation\n" + - " }\n" + - "\n" + - " public static Intent registerViolation2(Context cxt, BroadcastReceiver receiver, IntentFilter filter) {\n" + - " return cxt.registerReceiver(receiver, filter, null, null); // lint violation\n" + - " }\n" + - "\n" + - " public static void sendViolation(Context cxt, Intent intent, String perm) {\n" + - " cxt.sendBroadcast(intent, perm); // lint violation\n" + - " }\n" + - "\n" + - " public static void sendViolation2(Context cxt, Intent intent) {\n" + - " cxt.sendBroadcast(intent); // lint violation\n" + - " }\n" + - "}\n"); - lint().files(TestFileStubs.getNonNull(), TestFileStubs.getBroadcastHelper(), testFile) - .issues(MyBroadcastHelperDetector.ISSUE) - .run() - .expect("src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:44: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + - " return cxt.registerReceiver(receiver, filter); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:48: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + - " return cxt.registerReceiver(receiver, filter, null, null); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:52: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + - " cxt.sendBroadcast(intent, perm); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:56: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + - " cxt.sendBroadcast(intent); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "4 errors, 0 warnings\n"); - } - - @Test - public void testLintViolationActivity() throws Exception { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.activity;\n" + - "\n" + - "import android.content.BroadcastReceiver;\n" + - "import android.content.Context;\n" + - "import android.content.Intent;\n" + - "import android.content.IntentFilter;\n" + - "import android.os.Bundle;\n" + - "import android.support.v7.app.AppCompatActivity;\n" + - "import android.view.MenuItem;\n" + - "\n" + - "\n" + - "/**\n" + - " * Class doc for test\n" + - " */\n" + - "public class LintViolationActivity extends AppCompatActivity { // lint violation\n" + - " private static final String TEST_ACTION = \"action.test\";\n" + - "\n" + - " private BroadcastReceiver mReceiver = new BroadcastReceiver() {\n" + - " @Override\n" + - " public void onReceive(Context context, Intent intent) {\n" + - " // nothing to do\n" + - " }\n" + - " };\n" + - "\n" + - " @Override\n" + - " protected void onCreate(Bundle savedInstanceState) {\n" + - " super.onCreate(savedInstanceState);\n" + - "\n" + - "\n" + - " IntentFilter filter = new IntentFilter();\n" + - " filter.addAction(TEST_ACTION);\n" + - " registerReceiver(mReceiver, filter); // lint violation\n" + - " }\n" + - "\n" + - " @Override\n" + - " public boolean onOptionsItemSelected(MenuItem item) {\n" + - " // Handle action bar item clicks here. The action bar will\n" + - " // automatically handle clicks on the Home/Up button, so long\n" + - " // as you specify a parent activity in AndroidManifest.xml.\n" + - " int id = item.getItemId();\n" + - "\n" + - " sendBroadcast(new Intent(TEST_ACTION)); // lint violation\n" + - "\n" + - " return super.onOptionsItemSelected(item);\n" + - " }\n" + - "\n" + - " @Override\n" + - " protected void onDestroy() {\n" + - " super.onDestroy();\n" + - " unregisterReceiver(mReceiver);\n" + - " }\n" + - "}\n"); - lint().files(TestFileStubs.getAppCompatActivity(), testFile) - .issues(MyBroadcastHelperDetector.ISSUE) - .run() - .expect("src/me/ycdev/android/arch/demo/activity/LintViolationActivity.java:32: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + - " registerReceiver(mReceiver, filter); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.java:42: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + - " sendBroadcast(new Intent(TEST_ACTION)); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "2 errors, 0 warnings\n"); - } -} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.kt b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.kt new file mode 100644 index 0000000..7595e05 --- /dev/null +++ b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetectorTest.kt @@ -0,0 +1,323 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import me.ycdev.android.arch.lint.utils.TestFileStubs +import org.junit.Test + +class MyBroadcastHelperDetectorTest { + @Test + @Throws(Exception::class) + fun testBroadcastHelperLintCase_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.wrapper;\n" + + "\n" + + "import android.content.BroadcastReceiver;\n" + + "import android.content.Context;\n" + + "import android.content.Intent;\n" + + "import android.content.IntentFilter;\n" + + "\n" + + "import me.ycdev.android.lib.common.wrapper.BroadcastHelper;\n" + + "\n" + + "public class BroadcastHelperLintCase {\n" + + " private static class Foo {\n" + + " public void registerReceiver() { // lint good\n" + + " }\n" + + "\n" + + " public void sendBroadcast() { // lint good\n" + + " }\n" + + " }\n" + + "\n" + + " public static void registerReceiver() { // lint good\n" + + " new Foo().registerReceiver();\n" + + " }\n" + + "\n" + + " public static void sendBroadcast() { // lint good\n" + + " new Foo().sendBroadcast();\n" + + " }\n" + + "\n" + + " public static Intent registerGood(Context cxt, BroadcastReceiver receiver, IntentFilter filter) {\n" + + " return BroadcastHelper.registerForInternal(cxt, receiver, filter); // lint good\n" + + " }\n" + + "\n" + + " public static void sendToInternalGood(Context cxt, Intent intent) {\n" + + " BroadcastHelper.sendToInternal(cxt, intent); // lint good\n" + + " }\n" + + "\n" + + " public static void sendToExternalGood(Context cxt, Intent intent, String perm) {\n" + + " BroadcastHelper.sendToExternal(cxt, intent, perm); // lint good\n" + + " }\n" + + "\n" + + " public static void sendToExternal(Context cxt, Intent intent) {\n" + + " BroadcastHelper.sendToExternal(cxt, intent); // lint good\n" + + " }\n" + + "\n" + + " public static Intent registerViolation(Context cxt, BroadcastReceiver receiver, IntentFilter filter) {\n" + + " return cxt.registerReceiver(receiver, filter); // lint violation\n" + + " }\n" + + "\n" + + " public static Intent registerViolation2(Context cxt, BroadcastReceiver receiver, IntentFilter filter) {\n" + + " return cxt.registerReceiver(receiver, filter, null, null); // lint violation\n" + + " }\n" + + "\n" + + " public static void sendViolation(Context cxt, Intent intent, String perm) {\n" + + " cxt.sendBroadcast(intent, perm); // lint violation\n" + + " }\n" + + "\n" + + " public static void sendViolation2(Context cxt, Intent intent) {\n" + + " cxt.sendBroadcast(intent); // lint violation\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.nonNull, TestFileStubs.broadcastHelper, testFile) + .issues(MyBroadcastHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:44: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " return cxt.registerReceiver(receiver, filter); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:48: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " return cxt.registerReceiver(receiver, filter, null, null); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:52: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " cxt.sendBroadcast(intent, perm); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.java:56: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " cxt.sendBroadcast(intent); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "4 errors, 0 warnings\n" + ) + } + + @Test + @Throws(Exception::class) + fun testBroadcastHelperLintCase_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.wrapper\n" + + "\n" + + "import android.content.BroadcastReceiver\n" + + "import android.content.Context\n" + + "import android.content.Intent\n" + + "import android.content.IntentFilter\n" + + "\n" + + "import me.ycdev.android.lib.common.wrapper.BroadcastHelper\n" + + "\n" + + "object BroadcastHelperLintCase {\n" + + " private class Foo {\n" + + " fun registerReceiver() { // lint good\n" + + " }\n" + + "\n" + + " fun sendBroadcast() { // lint good\n" + + " }\n" + + " }\n" + + "\n" + + " fun registerReceiver() { // lint good\n" + + " Foo().registerReceiver()\n" + + " }\n" + + "\n" + + " fun sendBroadcast() { // lint good\n" + + " Foo().sendBroadcast()\n" + + " }\n" + + "\n" + + " fun registerGood(cxt: Context, receiver: BroadcastReceiver, filter: IntentFilter): Intent? {\n" + + " return BroadcastHelper.registerForInternal(cxt, receiver, filter) // lint good\n" + + " }\n" + + "\n" + + " fun sendToInternalGood(cxt: Context, intent: Intent) {\n" + + " BroadcastHelper.sendToInternal(cxt, intent) // lint good\n" + + " }\n" + + "\n" + + " fun sendToExternalGood(cxt: Context, intent: Intent, perm: String) {\n" + + " BroadcastHelper.sendToExternal(cxt, intent, perm) // lint good\n" + + " }\n" + + "\n" + + " fun sendToExternal(cxt: Context, intent: Intent) {\n" + + " BroadcastHelper.sendToExternal(cxt, intent) // lint good\n" + + " }\n" + + "\n" + + " fun registerViolation(\n" + + " cxt: Context,\n" + + " receiver: BroadcastReceiver,\n" + + " filter: IntentFilter\n" + + " ): Intent? {\n" + + " return cxt.registerReceiver(receiver, filter) // lint violation\n" + + " }\n" + + "\n" + + " fun registerViolation2(\n" + + " cxt: Context,\n" + + " receiver: BroadcastReceiver,\n" + + " filter: IntentFilter\n" + + " ): Intent? {\n" + + " return cxt.registerReceiver(receiver, filter, null, null) // lint violation\n" + + " }\n" + + "\n" + + " fun sendViolation(cxt: Context, intent: Intent, perm: String) {\n" + + " cxt.sendBroadcast(intent, perm) // lint violation\n" + + " }\n" + + "\n" + + " fun sendViolation2(cxt: Context, intent: Intent) {\n" + + " cxt.sendBroadcast(intent) // lint violation\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.broadcastHelper, testFile) + .issues(MyBroadcastHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt:48: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " return cxt.registerReceiver(receiver, filter) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt:56: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " return cxt.registerReceiver(receiver, filter, null, null) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt:60: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " cxt.sendBroadcast(intent, perm) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/BroadcastHelperLintCase.kt:64: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " cxt.sendBroadcast(intent) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "4 errors, 0 warnings" + ) + } + + @Test + @Throws(Exception::class) + fun testLintViolationActivity_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.activity;\n" + + "\n" + + "import android.content.BroadcastReceiver;\n" + + "import android.content.Context;\n" + + "import android.content.Intent;\n" + + "import android.content.IntentFilter;\n" + + "import android.os.Bundle;\n" + + "import android.support.v7.app.AppCompatActivity;\n" + + "import android.view.MenuItem;\n" + + "\n" + + "\n" + + "/**\n" + + " * Class doc for test\n" + + " */\n" + + "public class LintViolationActivity extends AppCompatActivity { // lint violation\n" + + " private static final String TEST_ACTION = \"action.test\";\n" + + "\n" + + " private BroadcastReceiver mReceiver = new BroadcastReceiver() {\n" + + " @Override\n" + + " public void onReceive(Context context, Intent intent) {\n" + + " // nothing to do\n" + + " }\n" + + " };\n" + + "\n" + + " @Override\n" + + " protected void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + "\n" + + "\n" + + " IntentFilter filter = new IntentFilter();\n" + + " filter.addAction(TEST_ACTION);\n" + + " registerReceiver(mReceiver, filter); // lint violation\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean onOptionsItemSelected(MenuItem item) {\n" + + " // Handle action bar item clicks here. The action bar will\n" + + " // automatically handle clicks on the Home/Up button, so long\n" + + " // as you specify a parent activity in AndroidManifest.xml.\n" + + " int id = item.getItemId();\n" + + "\n" + + " sendBroadcast(new Intent(TEST_ACTION)); // lint violation\n" + + "\n" + + " return super.onOptionsItemSelected(item);\n" + + " }\n" + + "\n" + + " @Override\n" + + " protected void onDestroy() {\n" + + " super.onDestroy();\n" + + " unregisterReceiver(mReceiver);\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.appCompatActivity, testFile) + .issues(MyBroadcastHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.java:32: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " registerReceiver(mReceiver, filter); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.java:42: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " sendBroadcast(new Intent(TEST_ACTION)); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "2 errors, 0 warnings\n" + ) + } + + @Test + @Throws(Exception::class) + fun testLintViolationActivity_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.activity\n" + + "\n" + + "import android.content.BroadcastReceiver\n" + + "import android.content.Context\n" + + "import android.content.Intent\n" + + "import android.content.IntentFilter\n" + + "import android.os.Bundle\n" + + "import androidx.appcompat.app.AppCompatActivity\n" + + "import android.view.MenuItem\n" + + "\n" + + "/**\n" + + " * Class doc for test\n" + + " */\n" + + "class LintViolationActivity : AppCompatActivity() { // lint violation\n" + + "\n" + + " private val receiver = object : BroadcastReceiver() {\n" + + " override fun onReceive(context: Context, intent: Intent) {\n" + + " // nothing to do\n" + + " }\n" + + " }\n" + + "\n" + + " override fun onCreate(savedInstanceState: Bundle?) {\n" + + " super.onCreate(savedInstanceState)\n" + + "\n" + + " val filter = IntentFilter()\n" + + " filter.addAction(TEST_ACTION)\n" + + " registerReceiver(receiver, filter) // lint violation\n" + + " }\n" + + "\n" + + " override fun onOptionsItemSelected(item: MenuItem): Boolean {\n" + + " // Handle action bar item clicks here. The action bar will\n" + + " // automatically handle clicks on the Home/Up button, so long\n" + + " // as you specify a parent activity in AndroidManifest.xml.\n" + + " val id = item.itemId\n" + + "\n" + + " sendBroadcast(Intent(TEST_ACTION)) // lint violation\n" + + "\n" + + " return super.onOptionsItemSelected(item)\n" + + " }\n" + + "\n" + + " override fun onDestroy() {\n" + + " super.onDestroy()\n" + + " unregisterReceiver(receiver)\n" + + " }\n" + + "\n" + + " companion object {\n" + + " private val TEST_ACTION = \"action.test\"\n" + + " }\n" + + "}\n" + ) + lint().files(TestFileStubs.appCompatActivityAndroidX, testFile) + .issues(MyBroadcastHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt:27: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " registerReceiver(receiver, filter) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/activity/LintViolationActivity.kt:36: Error: Please use the wrapper class 'BroadcastHelper'. [MyBroadcastHelper]\n" + + " sendBroadcast(Intent(TEST_ACTION)) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "2 errors, 0 warnings" + ) + } +} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.java b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.java deleted file mode 100644 index 137038f..0000000 --- a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; -import com.android.tools.lint.checks.infrastructure.TestFiles; - -import org.junit.Test; - -import me.ycdev.android.arch.lint.utils.TestFileStubs; - -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; - -public class MyIntentHelperDetectorTest { - @Test - public void testIntentHelperLintCase() { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.wrapper;\n" + - "\n" + - "import android.content.Intent;\n" + - "import android.os.Bundle;\n" + - "\n" + - "import me.ycdev.android.lib.common.wrapper.IntentHelper;\n" + - "\n" + - "public class IntentHelperLintCase {\n" + - " private static class Foo {\n" + - " public void hasExtra() { // lint good\n" + - " }\n" + - "\n" + - " public void getBundleExtra() { // lint good\n" + - " }\n" + - " }\n" + - "\n" + - " public static void hasExtra() { // lint good\n" + - " new Foo().hasExtra();\n" + - " }\n" + - "\n" + - " public static void getBundleExtra() { // lint good\n" + - " new Foo().getBundleExtra();\n" + - " }\n" + - "\n" + - " public static boolean hasExtraGood(Intent intent, String key) {\n" + - " return IntentHelper.hasExtra(intent, key); // lint good\n" + - " }\n" + - "\n" + - " public static boolean getBooleanExtraGood(Intent intent, String key, boolean defValue) {\n" + - " return IntentHelper.getBooleanExtra(intent, key, defValue); // lint good\n" + - " }\n" + - "\n" + - " public static Bundle getBundleExtraGood(Intent intent, String key) {\n" + - " return IntentHelper.getBundleExtra(intent, key); // lint good\n" + - " }\n" + - "\n" + - " public static boolean hasExtraBad(Intent intent, String key) {\n" + - " return intent.hasExtra(key); // lint violation\n" + - " }\n" + - "\n" + - " public static boolean getBooleanExtraBad(Intent intent, String key, boolean defValue) {\n" + - " return intent.getBooleanExtra(key, defValue); // lint violation\n" + - " }\n" + - "\n" + - " public static Bundle getBundleExtraBad(Intent intent, String key) {\n" + - " return intent.getBundleExtra(key); // lint violation\n" + - " }\n" + - "}\n"); - TestFile[] testFiles = new TestFile[] { - TestFileStubs.getNonNull(), TestFileStubs.getNullable(), - TestFileStubs.getLibLogger(), TestFileStubs.getIntentHelper(), testFile - }; - lint().files(testFiles) - .issues(MyIntentHelperDetector.ISSUE) - .run() - .expect("src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java:38: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + - " return intent.hasExtra(key); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java:42: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + - " return intent.getBooleanExtra(key, defValue); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java:46: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + - " return intent.getBundleExtra(key); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "3 errors, 0 warnings\n"); - } -} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.kt b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.kt new file mode 100644 index 0000000..c6b0faa --- /dev/null +++ b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyIntentHelperDetectorTest.kt @@ -0,0 +1,163 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles + +import org.junit.Test + +import me.ycdev.android.arch.lint.utils.TestFileStubs + +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint + +class MyIntentHelperDetectorTest { + @Test + fun testIntentHelperLintCase_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.wrapper;\n" + + "\n" + + "import android.content.Intent;\n" + + "import android.os.Bundle;\n" + + "\n" + + "import me.ycdev.android.lib.common.wrapper.IntentHelper;\n" + + "\n" + + "public class IntentHelperLintCase {\n" + + " private static class Foo {\n" + + " public void hasExtra() { // lint good\n" + + " }\n" + + "\n" + + " public void getBundleExtra() { // lint good\n" + + " }\n" + + " }\n" + + "\n" + + " public static void hasExtra() { // lint good\n" + + " new Foo().hasExtra();\n" + + " }\n" + + "\n" + + " public static void getBundleExtra() { // lint good\n" + + " new Foo().getBundleExtra();\n" + + " }\n" + + "\n" + + " public static boolean hasExtraGood(Intent intent, String key) {\n" + + " return IntentHelper.hasExtra(intent, key); // lint good\n" + + " }\n" + + "\n" + + " public static boolean getBooleanExtraGood(Intent intent, String key, boolean defValue) {\n" + + " return IntentHelper.getBooleanExtra(intent, key, defValue); // lint good\n" + + " }\n" + + "\n" + + " public static Bundle getBundleExtraGood(Intent intent, String key) {\n" + + " return IntentHelper.getBundleExtra(intent, key); // lint good\n" + + " }\n" + + "\n" + + " public static boolean hasExtraBad(Intent intent, String key) {\n" + + " return intent.hasExtra(key); // lint violation\n" + + " }\n" + + "\n" + + " public static boolean getBooleanExtraBad(Intent intent, String key, boolean defValue) {\n" + + " return intent.getBooleanExtra(key, defValue); // lint violation\n" + + " }\n" + + "\n" + + " public static Bundle getBundleExtraBad(Intent intent, String key) {\n" + + " return intent.getBundleExtra(key); // lint violation\n" + + " }\n" + + "}\n" + ) + val testFiles = arrayOf( + TestFileStubs.nonNull, + TestFileStubs.nullable, + TestFileStubs.libLogger, + TestFileStubs.intentHelper, + testFile + ) + lint().files(*testFiles) + .issues(MyIntentHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java:38: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + + " return intent.hasExtra(key); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java:42: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + + " return intent.getBooleanExtra(key, defValue); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.java:46: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + + " return intent.getBundleExtra(key); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "3 errors, 0 warnings\n" + ) + } + + @Test + fun testIntentHelperLintCase_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.wrapper\n" + + "\n" + + "import android.content.Intent\n" + + "import android.os.Bundle\n" + + "\n" + + "import me.ycdev.android.lib.common.wrapper.IntentHelper\n" + + "\n" + + "object IntentHelperLintCase {\n" + + " private class Foo {\n" + + " fun hasExtra() { // lint good\n" + + " }\n" + + "\n" + + " fun getBundleExtra() { // lint good\n" + + " }\n" + + " }\n" + + "\n" + + " fun hasExtra() { // lint good\n" + + " Foo().hasExtra()\n" + + " }\n" + + "\n" + + " fun getBundleExtra() { // lint good\n" + + " Foo().getBundleExtra()\n" + + " }\n" + + "\n" + + " fun hasExtraGood(intent: Intent, key: String): Boolean {\n" + + " return IntentHelper.hasExtra(intent, key) // lint good\n" + + " }\n" + + "\n" + + " fun getBooleanExtraGood(intent: Intent, key: String, defValue: Boolean): Boolean {\n" + + " return IntentHelper.getBooleanExtra(intent, key, defValue) // lint good\n" + + " }\n" + + "\n" + + " fun getBundleExtraGood(intent: Intent, key: String): Bundle? {\n" + + " return IntentHelper.getBundleExtra(intent, key) // lint good\n" + + " }\n" + + "\n" + + " fun hasExtraBad(intent: Intent, key: String): Boolean {\n" + + " return intent.hasExtra(key) // lint violation\n" + + " }\n" + + "\n" + + " fun getBooleanExtraBad(intent: Intent, key: String, defValue: Boolean): Boolean {\n" + + " return intent.getBooleanExtra(key, defValue) // lint violation\n" + + " }\n" + + "\n" + + " fun getBundleExtraBad(intent: Intent, key: String): Bundle {\n" + + " return intent.getBundleExtra(key) // lint violation\n" + + " }\n" + + "}\n" + ) + val testFiles = arrayOf( + TestFileStubs.libLogger, + TestFileStubs.intentHelper, + testFile + ) + lint().files(*testFiles) + .issues(MyIntentHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt:38: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + + " return intent.hasExtra(key) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt:42: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + + " return intent.getBooleanExtra(key, defValue) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/IntentHelperLintCase.kt:46: Error: Please use the wrapper class 'IntentHelper'. [MyIntentHelper]\n" + + " return intent.getBundleExtra(key) // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "3 errors, 0 warnings" + ) + } +} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.java b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.java deleted file mode 100644 index 72f36d7..0000000 --- a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package me.ycdev.android.arch.lint; - -import com.android.tools.lint.checks.infrastructure.TestFile; -import com.android.tools.lint.checks.infrastructure.TestFiles; - -import org.junit.Test; - -import me.ycdev.android.arch.lint.utils.TestFileStubs; - -import static com.android.tools.lint.checks.infrastructure.TestLintTask.lint; - -public class MyToastHelperDetectorTest { - @Test - public void testToastHelperLintCase() { - TestFile testFile = TestFiles.java("" + - "package me.ycdev.android.arch.demo.wrapper;\n" + - "\n" + - "import android.content.Context;\n" + - "import android.widget.Toast;\n" + - "\n" + - "import me.ycdev.android.arch.wrapper.ToastHelper;\n" + - "\n" + - "public class ToastHelperLintCase {\n" + - " private static class Foo {\n" + - " public void show() { // lint good\n" + - " }\n" + - "\n" + - " public void makeText() { // lint good\n" + - " }\n" + - " }\n" + - "\n" + - " public static void show() { // lint good\n" + - " new Foo().show();\n" + - " }\n" + - "\n" + - " public static void makeText() { // lint good\n" + - " new Foo().makeText();\n" + - " }\n" + - "\n" + - " public static void showGood(Context cxt, int msgResId, int duration) {\n" + - " ToastHelper.show(cxt, msgResId, duration); // lint good\n" + - " }\n" + - "\n" + - " public static void showGood(Context cxt, CharSequence msg, int duration) {\n" + - " ToastHelper.show(cxt, msg, duration); // lint good\n" + - " }\n" + - "\n" + - " public static void showViolation(Context cxt, int msgResId, int duration) {\n" + - " Toast.makeText(cxt, msgResId, duration).show(); // lint violation\n" + - " }\n" + - "\n" + - " public static void showViolation(Context cxt, CharSequence msg, int duration) {\n" + - " Toast.makeText(cxt, msg, duration).show(); // lint violation\n" + - " }\n" + - "}\n"); - TestFile[] testFiles = new TestFile[] { - TestFileStubs.getNonNull(), TestFileStubs.getNullable(), - TestFileStubs.getStringRes(), TestFileStubs.getToastHelper(), testFile - }; - lint().files(testFiles) - .issues(MyToastHelperDetector.ISSUE) - .run() - .expect("src/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java:34: Error: Please use the wrapper class 'ToastHelper'. [MyToastHelper]\n" + - " Toast.makeText(cxt, msgResId, duration).show(); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "src/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java:38: Error: Please use the wrapper class 'ToastHelper'. [MyToastHelper]\n" + - " Toast.makeText(cxt, msg, duration).show(); // lint violation\n" + - " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + - "2 errors, 0 warnings\n"); - } -} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.kt b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.kt new file mode 100644 index 0000000..08ad3dd --- /dev/null +++ b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyToastHelperDetectorTest.kt @@ -0,0 +1,141 @@ +package me.ycdev.android.arch.lint + +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles + +import org.junit.Test + +import me.ycdev.android.arch.lint.utils.TestFileStubs + +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint + +class MyToastHelperDetectorTest { + @Test + fun testToastHelperLintCase_java() { + val testFile = TestFiles.java( + "" + + "package me.ycdev.android.arch.demo.wrapper;\n" + + "\n" + + "import android.content.Context;\n" + + "import android.widget.Toast;\n" + + "\n" + + "import me.ycdev.android.arch.wrapper.ToastHelper;\n" + + "\n" + + "public class ToastHelperLintCase {\n" + + " private static class Foo {\n" + + " public void show() { // lint good\n" + + " }\n" + + "\n" + + " public void makeText() { // lint good\n" + + " }\n" + + " }\n" + + "\n" + + " public static void show() { // lint good\n" + + " new Foo().show();\n" + + " }\n" + + "\n" + + " public static void makeText() { // lint good\n" + + " new Foo().makeText();\n" + + " }\n" + + "\n" + + " public static void showGood(Context cxt, int msgResId, int duration) {\n" + + " ToastHelper.show(cxt, msgResId, duration); // lint good\n" + + " }\n" + + "\n" + + " public static void showGood(Context cxt, CharSequence msg, int duration) {\n" + + " ToastHelper.show(cxt, msg, duration); // lint good\n" + + " }\n" + + "\n" + + " public static void showViolation(Context cxt, int msgResId, int duration) {\n" + + " Toast.makeText(cxt, msgResId, duration).show(); // lint violation\n" + + " }\n" + + "\n" + + " public static void showViolation(Context cxt, CharSequence msg, int duration) {\n" + + " Toast.makeText(cxt, msg, duration).show(); // lint violation\n" + + " }\n" + + "}\n" + ) + val testFiles = arrayOf( + TestFileStubs.nonNull, + TestFileStubs.nullable, + TestFileStubs.stringRes, + TestFileStubs.toastHelper, + testFile + ) + lint().files(*testFiles) + .issues(MyToastHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java:34: Error: Please use the wrapper class 'ToastHelper'. [MyToastHelper]\n" + + " Toast.makeText(cxt, msgResId, duration).show(); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.java:38: Error: Please use the wrapper class 'ToastHelper'. [MyToastHelper]\n" + + " Toast.makeText(cxt, msg, duration).show(); // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "2 errors, 0 warnings\n" + ) + } + + @Test + fun testToastHelperLintCase_kotlin() { + val testFile = TestFiles.kotlin( + "package me.ycdev.android.arch.demo.wrapper\n" + + "\n" + + "import android.content.Context\n" + + "import android.widget.Toast\n" + + "\n" + + "import me.ycdev.android.arch.wrapper.ToastHelper\n" + + "\n" + + "object ToastHelperLintCase {\n" + + " private class Foo {\n" + + " fun show() { // lint good\n" + + " }\n" + + "\n" + + " fun makeText() { // lint good\n" + + " }\n" + + " }\n" + + "\n" + + " fun show() { // lint good\n" + + " Foo().show()\n" + + " }\n" + + "\n" + + " fun makeText() { // lint good\n" + + " Foo().makeText()\n" + + " }\n" + + "\n" + + " fun showGood(cxt: Context, msgResId: Int, duration: Int) {\n" + + " ToastHelper.show(cxt, msgResId, duration) // lint good\n" + + " }\n" + + "\n" + + " fun showGood(cxt: Context, msg: CharSequence, duration: Int) {\n" + + " ToastHelper.show(cxt, msg, duration) // lint good\n" + + " }\n" + + "\n" + + " fun showViolation(cxt: Context, msgResId: Int, duration: Int) {\n" + + " Toast.makeText(cxt, msgResId, duration).show() // lint violation\n" + + " }\n" + + "\n" + + " fun showViolation(cxt: Context, msg: CharSequence, duration: Int) {\n" + + " Toast.makeText(cxt, msg, duration).show() // lint violation\n" + + " }\n" + + "}\n" + ) + val testFiles = arrayOf( + TestFileStubs.stringRes, + TestFileStubs.toastHelper, + testFile + ) + lint().files(*testFiles) + .issues(MyToastHelperDetector.ISSUE) + .run() + .expect( + "src/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.kt:34: Error: Please use the wrapper class 'ToastHelper'. [MyToastHelper]\n" + + " Toast.makeText(cxt, msgResId, duration).show() // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "src/me/ycdev/android/arch/demo/wrapper/ToastHelperLintCase.kt:38: Error: Please use the wrapper class 'ToastHelper'. [MyToastHelper]\n" + + " Toast.makeText(cxt, msg, duration).show() // lint violation\n" + + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + + "2 errors, 0 warnings" + ) + } +} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.java b/archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.java deleted file mode 100644 index ec49321..0000000 --- a/archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.java +++ /dev/null @@ -1,627 +0,0 @@ -package me.ycdev.android.arch.lint.utils; - -import com.android.tools.lint.checks.infrastructure.TestFile; -import com.android.tools.lint.checks.infrastructure.TestFiles; - -public class TestFileStubs { - public static TestFile getNonNull() { - return TestFiles.java("" + - "package android.support.annotation;\n" + - "\n" + - "import static java.lang.annotation.ElementType.ANNOTATION_TYPE;\n" + - "import static java.lang.annotation.ElementType.FIELD;\n" + - "import static java.lang.annotation.ElementType.LOCAL_VARIABLE;\n" + - "import static java.lang.annotation.ElementType.METHOD;\n" + - "import static java.lang.annotation.ElementType.PACKAGE;\n" + - "import static java.lang.annotation.ElementType.PARAMETER;\n" + - "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + - "\n" + - "import java.lang.annotation.Documented;\n" + - "import java.lang.annotation.Retention;\n" + - "import java.lang.annotation.Target;\n" + - "\n" + - "/**\n" + - " * Denotes that a parameter, field or method return value can never be null.\n" + - " *

    \n" + - " * This is a marker annotation and it has no specific attributes.\n" + - " */\n" + - "@Documented\n" + - "@Retention(CLASS)\n" + - "@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})\n" + - "public @interface NonNull {\n" + - "}\n"); - } - - public static TestFile getNullable() { - return TestFiles.java("" + - "package android.support.annotation;\n" + - "\n" + - "import static java.lang.annotation.ElementType.ANNOTATION_TYPE;\n" + - "import static java.lang.annotation.ElementType.FIELD;\n" + - "import static java.lang.annotation.ElementType.LOCAL_VARIABLE;\n" + - "import static java.lang.annotation.ElementType.METHOD;\n" + - "import static java.lang.annotation.ElementType.PACKAGE;\n" + - "import static java.lang.annotation.ElementType.PARAMETER;\n" + - "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + - "\n" + - "import java.lang.annotation.Documented;\n" + - "import java.lang.annotation.Retention;\n" + - "import java.lang.annotation.Target;\n" + - "\n" + - "/**\n" + - " * Denotes that a parameter, field or method return value can never be null.\n" + - " *

    \n" + - " * This is a marker annotation and it has no specific attributes.\n" + - " */\n" + - "@Documented\n" + - "@Retention(CLASS)\n" + - "@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})\n" + - "public @interface Nullable {\n" + - "}\n\n"); - } - - public static TestFile getStringRes() { - return TestFiles.java("" + - "package android.support.annotation;\n" + - "\n" + - "import static java.lang.annotation.ElementType.FIELD;\n" + - "import static java.lang.annotation.ElementType.LOCAL_VARIABLE;\n" + - "import static java.lang.annotation.ElementType.METHOD;\n" + - "import static java.lang.annotation.ElementType.PARAMETER;\n" + - "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + - "\n" + - "import java.lang.annotation.Documented;\n" + - "import java.lang.annotation.Retention;\n" + - "import java.lang.annotation.Target;\n" + - "\n" + - "/**\n" + - " * Denotes that an integer parameter, field or method return value is expected\n" + - " * to be a String resource reference (e.g. {@code android.R.string.ok}).\n" + - " */\n" + - "@Documented\n" + - "@Retention(CLASS)\n" + - "@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})\n" + - "public @interface StringRes {\n" + - "}\n"); - } - - public static TestFile getAppCompatActivity() { - return TestFiles.java("" + - "package android.support.v7.app;\n" + - "\n" + - "import android.annotation.SuppressLint;\n" + - "import android.app.Activity;\n" + - "\n" + - "@SuppressLint(\"MyBaseActivity\")" + - "public class AppCompatActivity extends Activity {" + - "}\n"); - } - - public static TestFile getLibLogger() { - return TestFiles.java("" + - "package me.ycdev.android.lib.common.utils;\n" + - "\n" + - "import android.support.annotation.NonNull;\n" + - "import android.support.annotation.Nullable;\n" + - "import android.util.Log;\n" + - "\n" + - "import java.util.Locale;\n" + - "\n" + - "public class LibLogger {\n" + - " private static final String TAG = \"AndroidLib\";\n" + - " private static boolean sJvmLogger = true;\n" + - "\n" + - " protected LibLogger() {\n" + - " // nothing to do\n" + - " }\n" + - "\n" + - " public static void enableJvmLogger() {\n" + - " sJvmLogger = true;\n" + - " }\n" + - "\n" + - " /**\n" + - " * Log enabled by default\n" + - " */\n" + - " public static void setLogEnabled(boolean enabled) {\n" + - " }\n" + - "\n" + - " public static boolean isLogEnabled() {\n" + - " return true;\n" + - " }\n" + - "\n" + - " public static void v(@NonNull String tag, @NonNull String msg, Object... args) {\n" + - " log(Log.VERBOSE, tag, msg, null, args);\n" + - " }\n" + - "\n" + - " public static void d(@NonNull String tag, @NonNull String msg, Object... args) {\n" + - " log(Log.DEBUG, tag, msg, null, args);\n" + - " }\n" + - "\n" + - " public static void i(@NonNull String tag, @NonNull String msg, Object... args) {\n" + - " log(Log.INFO, tag, msg, null, args);\n" + - " }\n" + - "\n" + - " public static void w(@NonNull String tag, @NonNull String msg, Object... args) {\n" + - " log(Log.WARN, tag, msg, null, args);\n" + - " }\n" + - "\n" + - " public static void w(@NonNull String tag, @NonNull String msg, @NonNull Throwable e,\n" + - " Object... args) {\n" + - " log(Log.WARN, tag, msg, e, args);\n" + - " }\n" + - "\n" + - " public static void w(@NonNull String tag, @NonNull Throwable e, Object... args) {\n" + - " log(Log.WARN, tag, null, e, args);\n" + - " }\n" + - "\n" + - " public static void e(@NonNull String tag, @NonNull String msg, Object... args) {\n" + - " log(Log.ERROR, tag, msg, null, args);\n" + - " }\n" + - "\n" + - " public static void e(@NonNull String tag, @NonNull String msg, @NonNull Throwable e,\n" + - " Object... args) {\n" + - " log(Log.ERROR, tag, msg, e, args);\n" + - " }\n" + - "\n" + - " public static void log(int level, @NonNull String tag, @Nullable String msg,\n" + - " @Nullable Throwable tr, Object... args) {\n" + - " }\n" + - "\n" + - "}\n"); - } - - public static TestFile getBaseActivity() { - return TestFiles.java("" + - "package me.ycdev.android.arch.activity;\n" + - "\n" + - "import android.app.Activity;\n" + - "\n" + - "/**\n" + - " * Base class for Activity which wants to inherit {@link android.app.Activity}.\n" + - " */\n" + - "public abstract class BaseActivity extends Activity {\n" + - " // nothing to do right now\n" + - "}\n"); - - } - - public static TestFile getAppCompatBaseActivity() { - return TestFiles.java("package me.ycdev.android.arch.activity;\n" + - "\n" + - "import android.app.Activity;\n" + - "\n" + - "public abstract class AppCompatBaseActivity extends Activity {\n" + - "}\n"); - } - - public static TestFile getBroadcastHelper() { - return TestFiles.java("package me.ycdev.android.lib.common.wrapper;\n" + - "\n" + - "import android.content.BroadcastReceiver;\n" + - "import android.content.Context;\n" + - "import android.content.Intent;\n" + - "import android.content.IntentFilter;\n" + - "import android.support.annotation.NonNull;\n" + - "\n" + - "/**\n" + - " * A wrapper class to avoid security issues when sending/receiving broadcast.\n" + - " */\n" + - "@SuppressWarnings(\"unused\")\n" + - "public class BroadcastHelper {\n" + - " private static final String PERM_INTERNAL_BROADCAST_SUFFIX = \".permission.INTERNAL\";\n" + - "\n" + - " private BroadcastHelper() {\n" + - " // nothing to do\n" + - " }\n" + - "\n" + - " private static String getInternalBroadcastPerm(Context cxt) {\n" + - " return cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX;\n" + - " }\n" + - "\n" + - " /**\n" + - " * Register a receiver for internal broadcast.\n" + - " */\n" + - " public static Intent registerForInternal(@NonNull Context cxt,\n" + - " @NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) {\n" + - " String perm = cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX;\n" + - " return cxt.registerReceiver(receiver, filter, perm, null);\n" + - " }\n" + - "\n" + - " /**\n" + - " * Register a receiver for external broadcast (includes system broadcast).\n" + - " */\n" + - " public static Intent registerForExternal(@NonNull Context cxt,\n" + - " @NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) {\n" + - " return cxt.registerReceiver(receiver, filter);\n" + - " }\n" + - "\n" + - " /**\n" + - " * Send a broadcast to internal receivers.\n" + - " */\n" + - " public static void sendToInternal(@NonNull Context cxt, @NonNull Intent intent) {\n" + - " String perm = cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX;\n" + - " intent.setPackage(cxt.getPackageName()); // only works on Android 4.0 and higher versions\n" + - " cxt.sendBroadcast(intent, perm);\n" + - " }\n" + - "\n" + - " /**\n" + - " * Send a broadcast to external receivers.\n" + - " */\n" + - " public static void sendToExternal(@NonNull Context cxt, @NonNull Intent intent,\n" + - " @NonNull String perm) {\n" + - " cxt.sendBroadcast(intent, perm);\n" + - " }\n" + - "\n" + - " /**\n" + - " * Send a broadcast to external receivers.\n" + - " */\n" + - " public static void sendToExternal(@NonNull Context cxt, @NonNull Intent intent) {\n" + - " cxt.sendBroadcast(intent);\n" + - " }\n" + - "\n" + - "}\n"); - } - - public static TestFile getIntentHelper() { - return TestFiles.java("package me.ycdev.android.lib.common.wrapper;\n" + - "\n" + - "import android.content.Intent;\n" + - "import android.os.Bundle;\n" + - "import android.os.Parcelable;\n" + - "import android.support.annotation.NonNull;\n" + - "import android.support.annotation.Nullable;\n" + - "\n" + - "import java.io.Serializable;\n" + - "import java.util.ArrayList;\n" + - "\n" + - "import me.ycdev.android.lib.common.utils.LibLogger;\n" + - "\n" + - "/**\n" + - " * A wrapper class to avoid security issues when parsing Intent extras.\n" + - " *

    See details of the issue: http://code.google.com/p/android/issues/detail?id=177223.

    \n" + - " */\n" + - "@SuppressWarnings(\"unused\")\n" + - "public class IntentHelper {\n" + - " private static final String TAG = \"IntentUtils\";\n" + - "\n" + - " private IntentHelper() {\n" + - " // nothing to do\n" + - " }\n" + - "\n" + - " private static void onIntentAttacked(@NonNull Intent intent, Throwable e) {\n" + - " // prevent OOM for Android 5.0~?\n" + - " intent.replaceExtras((Bundle) null);\n" + - " LibLogger.w(TAG, \"attacked?\", e);\n" + - " }\n" + - "\n" + - " public static boolean hasExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return false;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.hasExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return false;\n" + - " }\n" + - "\n" + - " public static boolean getBooleanExtra(@Nullable Intent intent, @NonNull String key,\n" + - " boolean defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getBooleanExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static byte getByteExtra(@Nullable Intent intent, @NonNull String key,\n" + - " byte defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getByteExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static short getShortExtra(@Nullable Intent intent, @NonNull String key,\n" + - " short defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getShortExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static int getIntExtra(@Nullable Intent intent, @NonNull String key,\n" + - " int defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getIntExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static long getLongExtra(@Nullable Intent intent, @NonNull String key,\n" + - " long defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getLongExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static float getFloatExtra(@Nullable Intent intent, @NonNull String key,\n" + - " float defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getFloatExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static double getDoubleExtra(@Nullable Intent intent, @NonNull String key,\n" + - " double defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getDoubleExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " public static char getCharExtra(@Nullable Intent intent, @NonNull String key,\n" + - " char defValue) {\n" + - " if (intent == null) {\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getCharExtra(key, defValue);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return defValue;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static String getStringExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getStringExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static CharSequence getCharSequenceExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getCharSequenceExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static Serializable getSerializableExtra(@Nullable Intent intent,\n" + - " @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getSerializableExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static T getParcelableExtra(@Nullable Intent intent,\n" + - " @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getParcelableExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static boolean[] getBooleanArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getBooleanArrayExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static int[] getIntArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getIntArrayExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static long[] getLongArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getLongArrayExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static String[] getStringArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getStringArrayExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static Parcelable[] getParcelableArrayExtra(@Nullable Intent intent,\n" + - " @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getParcelableArrayExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static ArrayList getStringArrayListExtra(@Nullable Intent intent,\n" + - " @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getStringArrayListExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static ArrayList getParcelableArrayListExtra(\n" + - " @Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getParcelableArrayListExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - " @Nullable\n" + - " public static Bundle getBundleExtra(@Nullable Intent intent, @NonNull String key) {\n" + - " if (intent == null) {\n" + - " return null;\n" + - " }\n" + - "\n" + - " try {\n" + - " return intent.getBundleExtra(key);\n" + - " } catch (Exception e) {\n" + - " onIntentAttacked(intent, e);\n" + - " }\n" + - " return null;\n" + - " }\n" + - "\n" + - "}\n"); - } - - public static TestFile getToastHelper() { - return TestFiles.java("package me.ycdev.android.arch.wrapper;\n" + - "\n" + - "import android.content.Context;\n" + - "import android.support.annotation.NonNull;\n" + - "import android.support.annotation.StringRes;\n" + - "import android.widget.Toast;\n" + - "\n" + - "/**\n" + - " * A wrapper class for Toast so that we can customize and unify the UI in future.\n" + - " */\n" + - "@SuppressWarnings(\"unused\")\n" + - "public class ToastHelper {\n" + - " private ToastHelper() {\n" + - " // nothing to do\n" + - " }\n" + - "\n" + - " public static void show(@NonNull Context cxt, @StringRes int msgResId,\n" + - " int duration) {\n" + - " Toast.makeText(cxt, msgResId, duration).show();\n" + - " }\n" + - "\n" + - " public static void show(@NonNull Context cxt, @NonNull CharSequence msg,\n" + - " int duration) {\n" + - " Toast.makeText(cxt, msg, duration).show();\n" + - " }\n" + - "\n" + - "}\n"); - } -} diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.kt b/archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.kt new file mode 100644 index 0000000..79b529c --- /dev/null +++ b/archLintRules/src/test/java/me/ycdev/android/arch/lint/utils/TestFileStubs.kt @@ -0,0 +1,649 @@ +package me.ycdev.android.arch.lint.utils + +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles + +object TestFileStubs { + val nonNull: TestFile + get() = TestFiles.java( + "" + + "package android.support.annotation;\n" + + "\n" + + "import static java.lang.annotation.ElementType.ANNOTATION_TYPE;\n" + + "import static java.lang.annotation.ElementType.FIELD;\n" + + "import static java.lang.annotation.ElementType.LOCAL_VARIABLE;\n" + + "import static java.lang.annotation.ElementType.METHOD;\n" + + "import static java.lang.annotation.ElementType.PACKAGE;\n" + + "import static java.lang.annotation.ElementType.PARAMETER;\n" + + "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + + "\n" + + "import java.lang.annotation.Documented;\n" + + "import java.lang.annotation.Retention;\n" + + "import java.lang.annotation.Target;\n" + + "\n" + + "/**\n" + + " * Denotes that a parameter, field or method return value can never be null.\n" + + " *

    \n" + + " * This is a marker annotation and it has no specific attributes.\n" + + " */\n" + + "@Documented\n" + + "@Retention(CLASS)\n" + + "@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})\n" + + "public @interface NonNull {\n" + + "}\n" + ) + + val nullable: TestFile + get() = TestFiles.java( + "" + + "package android.support.annotation;\n" + + "\n" + + "import static java.lang.annotation.ElementType.ANNOTATION_TYPE;\n" + + "import static java.lang.annotation.ElementType.FIELD;\n" + + "import static java.lang.annotation.ElementType.LOCAL_VARIABLE;\n" + + "import static java.lang.annotation.ElementType.METHOD;\n" + + "import static java.lang.annotation.ElementType.PACKAGE;\n" + + "import static java.lang.annotation.ElementType.PARAMETER;\n" + + "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + + "\n" + + "import java.lang.annotation.Documented;\n" + + "import java.lang.annotation.Retention;\n" + + "import java.lang.annotation.Target;\n" + + "\n" + + "/**\n" + + " * Denotes that a parameter, field or method return value can never be null.\n" + + " *

    \n" + + " * This is a marker annotation and it has no specific attributes.\n" + + " */\n" + + "@Documented\n" + + "@Retention(CLASS)\n" + + "@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})\n" + + "public @interface Nullable {\n" + + "}\n\n" + ) + + val stringRes: TestFile + get() = TestFiles.java( + "" + + "package android.support.annotation;\n" + + "\n" + + "import static java.lang.annotation.ElementType.FIELD;\n" + + "import static java.lang.annotation.ElementType.LOCAL_VARIABLE;\n" + + "import static java.lang.annotation.ElementType.METHOD;\n" + + "import static java.lang.annotation.ElementType.PARAMETER;\n" + + "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + + "\n" + + "import java.lang.annotation.Documented;\n" + + "import java.lang.annotation.Retention;\n" + + "import java.lang.annotation.Target;\n" + + "\n" + + "/**\n" + + " * Denotes that an integer parameter, field or method return value is expected\n" + + " * to be a String resource reference (e.g. {@code android.R.string.ok}).\n" + + " */\n" + + "@Documented\n" + + "@Retention(CLASS)\n" + + "@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})\n" + + "public @interface StringRes {\n" + + "}\n" + ) + + val appCompatActivity: TestFile + get() = TestFiles.java( + "" + + "package android.support.v7.app;\n" + + "\n" + + "import android.annotation.SuppressLint;\n" + + "import android.app.Activity;\n" + + "\n" + + "@SuppressLint(\"MyBaseActivity\")" + + "public class AppCompatActivity extends Activity {" + + "}\n" + ) + + val appCompatActivityAndroidX: TestFile + get() = TestFiles.java( + "" + + "package androidx.appcompat.app;\n" + + "\n" + + "import android.annotation.SuppressLint;\n" + + "import android.app.Activity;\n" + + "\n" + + "@SuppressLint(\"MyBaseActivity\")" + + "public class AppCompatActivity extends Activity {" + + "}\n" + ) + + val libLogger: TestFile + get() = TestFiles.java( + "" + + "package me.ycdev.android.lib.common.utils;\n" + + "\n" + + "import android.support.annotation.NonNull;\n" + + "import android.support.annotation.Nullable;\n" + + "import android.util.Log;\n" + + "\n" + + "import java.util.Locale;\n" + + "\n" + + "public class LibLogger {\n" + + " private static final String TAG = \"AndroidLib\";\n" + + " private static boolean sJvmLogger = true;\n" + + "\n" + + " protected LibLogger() {\n" + + " // nothing to do\n" + + " }\n" + + "\n" + + " public static void enableJvmLogger() {\n" + + " sJvmLogger = true;\n" + + " }\n" + + "\n" + + " /**\n" + + " * Log enabled by default\n" + + " */\n" + + " public static void setLogEnabled(boolean enabled) {\n" + + " }\n" + + "\n" + + " public static boolean isLogEnabled() {\n" + + " return true;\n" + + " }\n" + + "\n" + + " public static void v(@NonNull String tag, @NonNull String msg, Object... args) {\n" + + " log(Log.VERBOSE, tag, msg, null, args);\n" + + " }\n" + + "\n" + + " public static void d(@NonNull String tag, @NonNull String msg, Object... args) {\n" + + " log(Log.DEBUG, tag, msg, null, args);\n" + + " }\n" + + "\n" + + " public static void i(@NonNull String tag, @NonNull String msg, Object... args) {\n" + + " log(Log.INFO, tag, msg, null, args);\n" + + " }\n" + + "\n" + + " public static void w(@NonNull String tag, @NonNull String msg, Object... args) {\n" + + " log(Log.WARN, tag, msg, null, args);\n" + + " }\n" + + "\n" + + " public static void w(@NonNull String tag, @NonNull String msg, @NonNull Throwable e,\n" + + " Object... args) {\n" + + " log(Log.WARN, tag, msg, e, args);\n" + + " }\n" + + "\n" + + " public static void w(@NonNull String tag, @NonNull Throwable e, Object... args) {\n" + + " log(Log.WARN, tag, null, e, args);\n" + + " }\n" + + "\n" + + " public static void e(@NonNull String tag, @NonNull String msg, Object... args) {\n" + + " log(Log.ERROR, tag, msg, null, args);\n" + + " }\n" + + "\n" + + " public static void e(@NonNull String tag, @NonNull String msg, @NonNull Throwable e,\n" + + " Object... args) {\n" + + " log(Log.ERROR, tag, msg, e, args);\n" + + " }\n" + + "\n" + + " public static void log(int level, @NonNull String tag, @Nullable String msg,\n" + + " @Nullable Throwable tr, Object... args) {\n" + + " }\n" + + "\n" + + "}\n" + ) + + val baseActivity: TestFile + get() = TestFiles.java( + "" + + "package me.ycdev.android.arch.activity;\n" + + "\n" + + "import android.app.Activity;\n" + + "\n" + + "/**\n" + + " * Base class for Activity which wants to inherit {@link android.app.Activity}.\n" + + " */\n" + + "public abstract class BaseActivity extends Activity {\n" + + " // nothing to do right now\n" + + "}\n" + ) + + val appCompatBaseActivity: TestFile + get() = TestFiles.java( + "package me.ycdev.android.arch.activity;\n" + + "\n" + + "import android.app.Activity;\n" + + "\n" + + "public abstract class AppCompatBaseActivity extends Activity {\n" + + "}\n" + ) + + val broadcastHelper: TestFile + get() = TestFiles.java( + "package me.ycdev.android.lib.common.wrapper;\n" + + "\n" + + "import android.content.BroadcastReceiver;\n" + + "import android.content.Context;\n" + + "import android.content.Intent;\n" + + "import android.content.IntentFilter;\n" + + "import android.support.annotation.NonNull;\n" + + "\n" + + "/**\n" + + " * A wrapper class to avoid security issues when sending/receiving broadcast.\n" + + " */\n" + + "@SuppressWarnings(\"unused\")\n" + + "public class BroadcastHelper {\n" + + " private static final String PERM_INTERNAL_BROADCAST_SUFFIX = \".permission.INTERNAL\";\n" + + "\n" + + " private BroadcastHelper() {\n" + + " // nothing to do\n" + + " }\n" + + "\n" + + " private static String getInternalBroadcastPerm(Context cxt) {\n" + + " return cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX;\n" + + " }\n" + + "\n" + + " /**\n" + + " * Register a receiver for internal broadcast.\n" + + " */\n" + + " public static Intent registerForInternal(@NonNull Context cxt,\n" + + " @NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) {\n" + + " String perm = cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX;\n" + + " return cxt.registerReceiver(receiver, filter, perm, null);\n" + + " }\n" + + "\n" + + " /**\n" + + " * Register a receiver for external broadcast (includes system broadcast).\n" + + " */\n" + + " public static Intent registerForExternal(@NonNull Context cxt,\n" + + " @NonNull BroadcastReceiver receiver, @NonNull IntentFilter filter) {\n" + + " return cxt.registerReceiver(receiver, filter);\n" + + " }\n" + + "\n" + + " /**\n" + + " * Send a broadcast to internal receivers.\n" + + " */\n" + + " public static void sendToInternal(@NonNull Context cxt, @NonNull Intent intent) {\n" + + " String perm = cxt.getPackageName() + PERM_INTERNAL_BROADCAST_SUFFIX;\n" + + " intent.setPackage(cxt.getPackageName()); // only works on Android 4.0 and higher versions\n" + + " cxt.sendBroadcast(intent, perm);\n" + + " }\n" + + "\n" + + " /**\n" + + " * Send a broadcast to external receivers.\n" + + " */\n" + + " public static void sendToExternal(@NonNull Context cxt, @NonNull Intent intent,\n" + + " @NonNull String perm) {\n" + + " cxt.sendBroadcast(intent, perm);\n" + + " }\n" + + "\n" + + " /**\n" + + " * Send a broadcast to external receivers.\n" + + " */\n" + + " public static void sendToExternal(@NonNull Context cxt, @NonNull Intent intent) {\n" + + " cxt.sendBroadcast(intent);\n" + + " }\n" + + "\n" + + "}\n" + ) + + val intentHelper: TestFile + get() = TestFiles.java( + "package me.ycdev.android.lib.common.wrapper;\n" + + "\n" + + "import android.content.Intent;\n" + + "import android.os.Bundle;\n" + + "import android.os.Parcelable;\n" + + "import android.support.annotation.NonNull;\n" + + "import android.support.annotation.Nullable;\n" + + "\n" + + "import java.io.Serializable;\n" + + "import java.util.ArrayList;\n" + + "\n" + + "import me.ycdev.android.lib.common.utils.LibLogger;\n" + + "\n" + + "/**\n" + + " * A wrapper class to avoid security issues when parsing Intent extras.\n" + + " *

    See details of the issue: http://code.google.com/p/android/issues/detail?id=177223.

    \n" + + " */\n" + + "@SuppressWarnings(\"unused\")\n" + + "public class IntentHelper {\n" + + " private static final String TAG = \"IntentUtils\";\n" + + "\n" + + " private IntentHelper() {\n" + + " // nothing to do\n" + + " }\n" + + "\n" + + " private static void onIntentAttacked(@NonNull Intent intent, Throwable e) {\n" + + " // prevent OOM for Android 5.0~?\n" + + " intent.replaceExtras((Bundle) null);\n" + + " LibLogger.w(TAG, \"attacked?\", e);\n" + + " }\n" + + "\n" + + " public static boolean hasExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return false;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.hasExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return false;\n" + + " }\n" + + "\n" + + " public static boolean getBooleanExtra(@Nullable Intent intent, @NonNull String key,\n" + + " boolean defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getBooleanExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static byte getByteExtra(@Nullable Intent intent, @NonNull String key,\n" + + " byte defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getByteExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static short getShortExtra(@Nullable Intent intent, @NonNull String key,\n" + + " short defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getShortExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static int getIntExtra(@Nullable Intent intent, @NonNull String key,\n" + + " int defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getIntExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static long getLongExtra(@Nullable Intent intent, @NonNull String key,\n" + + " long defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getLongExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static float getFloatExtra(@Nullable Intent intent, @NonNull String key,\n" + + " float defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getFloatExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static double getDoubleExtra(@Nullable Intent intent, @NonNull String key,\n" + + " double defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getDoubleExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " public static char getCharExtra(@Nullable Intent intent, @NonNull String key,\n" + + " char defValue) {\n" + + " if (intent == null) {\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getCharExtra(key, defValue);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return defValue;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static String getStringExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getStringExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static CharSequence getCharSequenceExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getCharSequenceExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static Serializable getSerializableExtra(@Nullable Intent intent,\n" + + " @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getSerializableExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static T getParcelableExtra(@Nullable Intent intent,\n" + + " @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getParcelableExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static boolean[] getBooleanArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getBooleanArrayExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static int[] getIntArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getIntArrayExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static long[] getLongArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getLongArrayExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static String[] getStringArrayExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getStringArrayExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static Parcelable[] getParcelableArrayExtra(@Nullable Intent intent,\n" + + " @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getParcelableArrayExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static ArrayList getStringArrayListExtra(@Nullable Intent intent,\n" + + " @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getStringArrayListExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static ArrayList getParcelableArrayListExtra(\n" + + " @Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getParcelableArrayListExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + " @Nullable\n" + + " public static Bundle getBundleExtra(@Nullable Intent intent, @NonNull String key) {\n" + + " if (intent == null) {\n" + + " return null;\n" + + " }\n" + + "\n" + + " try {\n" + + " return intent.getBundleExtra(key);\n" + + " } catch (Exception e) {\n" + + " onIntentAttacked(intent, e);\n" + + " }\n" + + " return null;\n" + + " }\n" + + "\n" + + "}\n" + ) + + val toastHelper: TestFile + get() = TestFiles.java( + "package me.ycdev.android.arch.wrapper;\n" + + "\n" + + "import android.content.Context;\n" + + "import android.support.annotation.NonNull;\n" + + "import android.support.annotation.StringRes;\n" + + "import android.widget.Toast;\n" + + "\n" + + "/**\n" + + " * A wrapper class for Toast so that we can customize and unify the UI in future.\n" + + " */\n" + + "@SuppressWarnings(\"unused\")\n" + + "public class ToastHelper {\n" + + " private ToastHelper() {\n" + + " // nothing to do\n" + + " }\n" + + "\n" + + " public static void show(@NonNull Context cxt, @StringRes int msgResId,\n" + + " int duration) {\n" + + " Toast.makeText(cxt, msgResId, duration).show();\n" + + " }\n" + + "\n" + + " public static void show(@NonNull Context cxt, @NonNull CharSequence msg,\n" + + " int duration) {\n" + + " Toast.makeText(cxt, msg, duration).show();\n" + + " }\n" + + "\n" + + "}\n" + ) +} From 921c34674af9707910b12ddf99e991f6d8d28886 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 28 Jul 2019 22:41:05 +0800 Subject: [PATCH 004/100] Prepare for v1.5.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 44c80d2..8440b1d 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ ext { 'projectScmDevConnection': 'ssh://git@github.com/yongce/AndroidLib.git', 'projectInceptionYear': '2013', 'groupId': 'me.ycdev.android', - 'version': '1.5.2', + 'version': '1.5.3', 'developerId': 'yongce', 'developerName': 'Yongce Tu', 'developerEmail': 'yongce.tu@gmail.com', From de8a405e4f21fa1309bef74fa1cb8a11c609614d Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sat, 26 Oct 2019 16:36:07 +0800 Subject: [PATCH 005/100] Add the hello.yml --- .github/workflows/hello.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/hello.yml diff --git a/.github/workflows/hello.yml b/.github/workflows/hello.yml new file mode 100644 index 0000000..b2340b2 --- /dev/null +++ b/.github/workflows/hello.yml @@ -0,0 +1,17 @@ +name: CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Run a one-line script + run: echo Hello, world! + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. From 4ef3582bb08c9ee7cbed222cefe3afb7f0cf179e Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sat, 26 Oct 2019 16:48:33 +0800 Subject: [PATCH 006/100] Update and rename hello.yml to ci.yml --- .github/workflows/ci.yml | 13 +++++++++++++ .github/workflows/hello.yml | 17 ----------------- 2 files changed, 13 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/hello.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..969aec6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,13 @@ +name: CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Build the project + run: ./gradlew build diff --git a/.github/workflows/hello.yml b/.github/workflows/hello.yml deleted file mode 100644 index b2340b2..0000000 --- a/.github/workflows/hello.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: CI - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Run a one-line script - run: echo Hello, world! - - name: Run a multi-line script - run: | - echo Add other actions to build, - echo test, and deploy your project. From fb9cfec958f6039c7da852d8b96c2307b918248a Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sat, 26 Oct 2019 17:08:26 +0800 Subject: [PATCH 007/100] Fix build errors --- .../me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt | 4 ++-- .../me/ycdev/android/arch/lint/base/InheritDetectorBase.kt | 2 +- .../me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt | 1 - bintray-upload.gradle | 4 +++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt index 959321c..e72ef22 100644 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/MyBroadcastHelperDetector.kt @@ -27,8 +27,8 @@ class MyBroadcastHelperDetector : WrapperDetectorBase() { internal val ISSUE = Issue.create( "MyBroadcastHelper", "BroadcastHelper should be used.", - "Please use the wrapper class 'BroadcastHelper' to register broadcast receivers" - + " and send broadcasts to avoid security issues.", + "Please use the wrapper class 'BroadcastHelper' to register broadcast receivers" + + " and send broadcasts to avoid security issues.", Category.CORRECTNESS, 5, Severity.ERROR, Implementation(MyBroadcastHelperDetector::class.java, Scope.JAVA_FILE_SCOPE) ) diff --git a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt index a980b4f..1c4622e 100644 --- a/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt +++ b/archLintRules/src/main/java/me/ycdev/android/arch/lint/base/InheritDetectorBase.kt @@ -17,7 +17,7 @@ abstract class InheritDetectorBase : Detector(), Detector.UastScanner { val wrappers = wrapperClasses val className = declaration.qualifiedName if (wrappers.contains(className)) { - return // ignore the wrapper classes + return // ignore the wrapper classes } val evaluator = context.evaluator diff --git a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt index e7e177a..366ca5f 100644 --- a/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt +++ b/archLintRules/src/test/java/me/ycdev/android/arch/lint/MyBaseActivityDetectorTest.kt @@ -257,7 +257,6 @@ class MyBaseActivityDetectorTest { ) } - @Test fun testLintViolationActivity_kotlin() { val testFile = TestFiles.kotlin( diff --git a/bintray-upload.gradle b/bintray-upload.gradle index e47bd11..26d0c90 100644 --- a/bintray-upload.gradle +++ b/bintray-upload.gradle @@ -35,7 +35,9 @@ artifacts { // For the plugin 'com.jfrog.bintray' Properties properties = new Properties() -properties.load(rootProject.file('local.properties').newDataInputStream()) +if (rootProject.file('local.properties').exists()) { + properties.load(rootProject.file('local.properties').newDataInputStream()) +} bintray { user = properties.getProperty('bintray.user') From 92b39158f4138c5d2b44a09f3b4a2a946463c327 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Fri, 6 Dec 2019 19:56:51 +0800 Subject: [PATCH 008/100] Upgrade AGP to v3.5.3 and library versions --- android_project_common.gradle | 57 ++++++++++++----------- archLintRulesTestDemo/build.gradle | 2 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 18 ++++++- gradlew.bat | 18 ++++++- jniLibDemo/build.gradle | 2 +- 8 files changed, 69 insertions(+), 33 deletions(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 3f92c15..16e467c 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -32,32 +32,33 @@ ext { versions = [ // compile - 'compileSdk' : 28, + 'compileSdk' : 29, // Android official support - 'kotlin' : "1.3.31", + 'kotlin' : "1.3.61", 'multidexLib' : "2.0.1", - 'androidxCore' : "1.0.2", - 'fragment' : "1.0.0", - 'preference' : "1.0.0", + 'androidxCore' : "1.1.0", + 'fragment' : "1.1.0", + 'preference' : "1.1.0", 'palette' : "1.0.0", - 'recyclerView' : "1.0.0", + 'recyclerView' : "1.1.0", 'constraintLayout' : "1.1.3", - 'vectorDrawable' : "1.0.0", - 'lintLib' : "26.4.0", - 'archCore' : "2.0.0", - 'lifecycle' : "2.0.0", - 'room' : "2.0.0", + 'vectorDrawable' : "1.1.0", + 'lintLib' : "26.5.3", + 'archCore' : "2.1.0", + 'lifecycle' : "2.1.0", + 'room' : "2.2.2", 'sqlite' : "2.0.1", - 'navigation' : "2.0.0", + 'navigation' : "2.1.0", 'paging' : "2.1.0", - 'work' : "2.0.1", + 'work' : "2.2.0", + 'media2' : "1.0.1", // test 'testCore' : "1.2.0", - 'espresso' : "3.1.0", + 'espresso' : "3.2.0", 'uiautomator' : "2.2.0", - 'truth' : "0.42", + 'truth' : "1.0", 'hamcrest' : "1.3", 'mockito' : "1.10.19", 'powermock' : "1.6.4", @@ -86,7 +87,7 @@ ext { 'okhttp' : "3.9.0", 'retrofit' : "2.3.0", 'glide' : "4.2.0", - 'glideTrans' : "3.0.1", + 'glideTrans' : "4.0.0", // rx 'rxjava' : "2.1.6", @@ -111,14 +112,14 @@ ext { 'fragment' : "androidx.fragment:fragment:${versions.fragment}", 'fragmentKtx' : "androidx.fragment:fragment-ktx:${versions.fragment}", 'localBroadcast' : "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0", - 'collection' : "androidx.collection:collection:1.0.0", - 'collectionKtx' : "androidx.collection:collection-ktx:1.0.0", + 'collection' : "androidx.collection:collection:1.1.0", + 'collectionKtx' : "androidx.collection:collection-ktx:1.1.0", // UI - 'appcompat' : "androidx.appcompat:appcompat:1.0.2", - 'design' : "com.google.android.material:material:1.0.0", + 'appcompat' : "androidx.appcompat:appcompat:1.1.0", + 'material' : "com.google.android.material:material:1.0.0", 'preference' : "androidx.preference:preference:${versions.preference}", 'preferenceKtx' : "androidx.preference:preference-ktx:${versions.preference}", - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:1.1.3", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:${versions.constraintLayout}", 'cardview' : "androidx.cardview:cardview:1.0.0", 'gridlayout' : "androidx.gridlayout:gridlayout:1.0.0", 'palette' : "androidx.palette:palette:${versions.palette}", @@ -126,18 +127,20 @@ ext { 'recyclerview' : "androidx.recyclerview:recyclerview:${versions.recyclerView}", 'recyclerviewSelection' : "androidx.recyclerview:recyclerview:${versions.recyclerView}", 'percent' : "androidx.percentlayout:percentlayout:1.0.0", - 'coordinatorLayout' : "androidx.coordinatorlayout:coordinatorlayout:1.0.0", + 'coordinatorLayout' : "androidx.coordinatorlayout:coordinatorlayout:1.1.0", 'drawerLayout' : "androidx.drawerlayout:drawerlayout:1.0.0", 'swipeRefreshLayout' : "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0", 'viewPager' : "androidx.viewpager:viewpager:1.0.0", + 'viewPager2' : "androidx.viewpager2:viewpager2:1.0.0", 'vectorDrawable' : "androidx.vectordrawable:vectordrawable:${versions.vectorDrawable}", 'animatedVectorDrawable' : "androidx.vectordrawable:vectordrawable-animated:${versions.vectorDrawable}", - 'customtabs' : "androidx.browser:browser:1.0.0", - 'transition' : "androidx.transition:transition:1.0.1", + 'browser' : "androidx.browser:browser:1.0.0", + 'transition' : "androidx.transition:transition:1.2.0", // others 'multidex' : "androidx.multidex:multidex:${versions.multidexLib}", - 'mediaCompat' : "androidx.media:media:1.0.1", - 'mediarouter' : "androidx.mediarouter:mediarouter:1.0.0", + 'mediaSession' : "androidx.media2:media2-session:${versions.media2}", + 'mediaExoPlayer' : "androidx.media2:media2-exoplayer:${versions.media2}", + 'mediarouter' : "androidx.mediarouter:mediarouter:1.1.0", 'exifinterface' : "androidx.exifinterface:exifinterface:1.0.0", 'wear' : "androidx.wear:wear:1.0.0", // legacy @@ -202,10 +205,10 @@ ext { 'rules' : "androidx.test:rules:${versions.testCore}", 'monitor' : "androidx.test:monitor:${versions.testCore}", 'orchestrator' : "androidx.test:orchestrator:${versions.testCore}", - 'truthAndroidX' : 'androidx.test.ext:truth:${versions.testCore}', // ext 'junit' : "androidx.test.ext:junit:1.1.1", 'junitKtx' : "androidx.test.ext:junit-ktx:1.1.1", + 'truthAndroidX' : 'androidx.test.ext:truth:1.2.0', 'truth' : "com.google.truth:truth:${versions.truth}", 'truthJava8' : "com.google.truth.extensions:truth-java8-extension:${versions.truth}", // espresso diff --git a/archLintRulesTestDemo/build.gradle b/archLintRulesTestDemo/build.gradle index 766e3a3..3f86719 100644 --- a/archLintRulesTestDemo/build.gradle +++ b/archLintRulesTestDemo/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "me.ycdev.android.arch.demo" minSdkVersion versions.minSdk - targetSdkVersion 28 + targetSdkVersion 29 versionCode 1 versionName "1.0" diff --git a/build.gradle b/build.gradle index 8440b1d..4439a1e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738cbd051603d91cc39de6cb000dd98fe6b02..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch delta 3320 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 Date: Fri, 6 Dec 2019 19:57:39 +0800 Subject: [PATCH 009/100] Prepare for v1.5.4 --- android_project_common.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 16e467c..09299d3 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -94,7 +94,7 @@ ext { 'rxandroid' : "2.1.1", // ycdev - 'androidLib' : "1.4.0", + 'androidLib' : "1.5.4", // others 'zxing' : "3.3.1", diff --git a/build.gradle b/build.gradle index 4439a1e..d50be5c 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ ext { 'projectScmDevConnection': 'ssh://git@github.com/yongce/AndroidLib.git', 'projectInceptionYear': '2013', 'groupId': 'me.ycdev.android', - 'version': '1.5.3', + 'version': '1.5.4', 'developerId': 'yongce', 'developerName': 'Yongce Tu', 'developerEmail': 'yongce.tu@gmail.com', From c363fe306cb2d699a7934db15ddcb2cb5780ec2b Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sat, 29 Feb 2020 00:46:57 +0800 Subject: [PATCH 010/100] Upgrade AGP to v3.6.0 --- .gitignore | 1 + android_project_common.gradle | 4 +-- archLintRulesTestDemo/build.gradle | 2 +- build.gradle | 4 +-- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 58702 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 35 ++++++++++------------- gradlew.bat | 2 +- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 36c33c6..170ebc0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ proguard/ # JNI objects obj/ .externalNativeBuild/ +.cxx/ diff --git a/android_project_common.gradle b/android_project_common.gradle index 09299d3..7e0350d 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -70,7 +70,7 @@ ext { 'wearableSupport' : "2.3.0", // infrastructure - 'butterknife' : "10.1.0", + 'butterknife' : "10.2.1", 'timber' : "4.7.1", 'guava' : "23.5-android", @@ -86,7 +86,7 @@ ext { // network & image 'okhttp' : "3.9.0", 'retrofit' : "2.3.0", - 'glide' : "4.2.0", + 'glide' : "4.10.0", 'glideTrans' : "4.0.0", // rx diff --git a/archLintRulesTestDemo/build.gradle b/archLintRulesTestDemo/build.gradle index 3f86719..eb64d85 100644 --- a/archLintRulesTestDemo/build.gradle +++ b/archLintRulesTestDemo/build.gradle @@ -49,7 +49,7 @@ dependencies { exclude group: 'com.android.support' }) - implementation "com.jakewharton:butterknife:${versions.butterknife}" + annotationProcessor "com.jakewharton:butterknife:${versions.butterknife}" implementation "com.jakewharton.timber:timber:${versions.timber}" implementation "com.google.guava:guava:${versions.guava}" diff --git a/build.gradle b/build.gradle index d50be5c..50f39d9 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:3.6.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' @@ -21,7 +21,7 @@ buildscript { // Add plugin for 'spotless' plugins { - id "com.diffplug.gradle.spotless" version "3.16.0" + id "com.diffplug.gradle.spotless" version "3.27.1" } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b 100644 GIT binary patch delta 16535 zcmZ9zbyyr-lRgZCTOhc*ySoH;cXxO94DJ#b+}+(FxVr{|dypU*+~LbU`|kes`Fj57 zyY8yfeVy*U&eSRCZ-Sbgglb@bL`kJuD(i%VfWU)-fM5ZAqre6!L1F?a*_h28Ox@k% z)ux=5zF-P1b$GIsh22W}rhGA$wY4AMj)Kul`ohep<{7-Ia88yvi6?!4@QO*mP1?8% z^+-G1h=Bla=)vYr;y%0F`7k?YyaR;riRpp3>1dAn4tcrPo2W>F8o&vIoo8FT(bXb?GlmSb7V9@<6RmZzUyg~x=I4k!GQX(!lDs)h5@qh6pkwH=O@3LDKNm1i;WQ8o$Fl=C^mx!!2RpT&LbaQ5~-gj zk}V-#Uq1+j(|;TD?e?fpp}ORH^Fq!uFQ{?+R=-AAXl>dQHNRxA%eOvJm2_4jRrfpH z5-aw5XpBp(8nzoT7~-#u+*s{L@q<(~8X0g_k%xjtgn)pDhk$?(g|LNWtR{hhfS~+K zG5zN~69PBXF|=_%h}_p27^B$eqeB|SWFatETD2Oq;%Vn$m>?Zn)|n^BYMi`It%~RE z{?zseJ_NVFBivK1vbQd!dzAq}2e$&>Wo6B}`={5MckUhxc|L^S-q?bQA7!N=FxZWT zU=VP`Gg4To%<=zBf<;qVDNMDbkkc&;M*Z23z5%huy5rEWEer-UUAsxdlvL`%T?_}| z(AC(*xAH|wk8S#%l@lNw>O44BZp257X zHvrr{{odBrGrE6ZV); zj8iGg2`q{Cm5o=D;JE|EG^sx`O)a|Vsgst~3Ake^OY!6;?G&szhN9ov0-!PbvBcU5 zGRjaV&=KpDs4zqyN`T#AmhHfP#k*wGhXF?Dga*x|Bj`& zHV~0hpwX|JkNK!dAqe;o8Ea%7b%IeQD~k(41Q0J{%pt1LS1Ggcq3FOT= z5A|Vo_JTwHTm_Y#V?{dbMum`oDTd}5=vi-t>w&h{Z8|8w&TVt0^eE-i3>R&hl&SM_ zmq)Meerq`|97S(0OKH~x2bnWXD<9`-`tCM{=8}{PSRq_%t`k~5fPh}{h3YIkjBTGneZ+JF+OuXd^<)_ZuX5$u&ZP+pP<2g_}pc)~MKJVi9<{(FJ?Nr^j) z=vL&X+rs>>ym1r>$ddJHuRN}3R53kb3p*4jpEpZzzA*8+3P^Zm_{$%#!r=GQC(O@C zx6Lk~7MUL^QcV)@DgnE*4-XV`3c`9c&QcG>RRmvV%AHUPa?0%()8%asP!noiK|7#1;^qznQT z0~b;d`W|`=o_E4xvzJ%-6v|@%kGFdG2L#9-_6miL%AA`Q8UkV!?(cf~&k72JLx7X8 zv@-Q{@Bp3R5(7&$x6}zVF+a8(xRIt{)nsT>+Jf4+pyjHxT1sjigKcbRQ&rGv`O^=% z9loFMTS2`MJnyO-KNl${u=ILJh5e4pedY`0;4eN1B{>+214bTnrh^ygc0ClRkGF-6 z^KM>p6MJ-DjzMz}f}!mS!&hQLdMYMBZn`5Ft}T)22E31R0j608`P&({6Sv z+~0D8pDl^uBMtG_h6A3r60>3 ze}0-}HvlSJitaX&`j_DjiW^0DaQ|}DHmI7NLj)$z@t4@n`b%CaxbCFQaar%#KMbFrP8;UV*=UXv2t~N7${I78|hP9xX|r*{0)ZBS-A2?pnEp z5{%38c<{72i%oG5F zBn@<(E_yi9g#uyMnN0S#v~L6&+}+@3~P5v<;rEzy3qM((!S^E7A$!`9*Z zfXHq{x|C#{_u}V_a3rgg{+P${gr=ns+3nmp7N*3$9I`A)xCG=A&A zk)vJy%fy1XNE<$2gK24($*r7zv|jZX)Cs&uID;Ff>s4pn&mdgKDt8oUo#5NiSA)&e zJ4iE)n<|_?dQ#*Q@65>|bKEX#^E_AO@K|ufg}Vxmu;OF$c;lKXEaaj*j#yz`L)}N4 z7`o+@_lsZgv4de;{vM}N<&38%r!Vzbcm11k4Keo+>iUiF?hz3GnEb7mTyS3bsTfEg z{lk+$yF=lE(k<$qGn=dX;d3Di>#8R3#qeA{5c+~3qq1%VjOdZv{)bd5jroreFdBBbJ#1)lyIhM5VZs&!Pcn5PR2S# z=^0_9q~0cs$>}}R&gvTxD)MaWj`V7B0z1~8qhjtKm}`Y~#bXcn!m-JZ7H@n7E8l%j zuSN6NIX__j?Xk_ZA`0VxOyNX<7f$G+m_p4e*zNKonge<-rut`Usij{fL)mOusi|$U zG_o_^vj(A89K0u3WqcXp5zrI^AV?;CtmPSO5tiQ?Io$v79p?$~+?+i;NYf5nDND9A+Xjmwo|s55SQS$L9~oncx`VWnLO|nBSK6IuerhlQz zwuQ>taA1U{x7}WC)8#rZke-dv7{a2#t2m)1`e*N@kb5${9SJvk21PuQAlo!osvVYo z*AA*9nWA8WYM6BTBaiE#Wsp*ug2Ni;mUP#+IfgQB%!hX-a;LhvHF~Uiw$=FPa8M+Q zbNf%N{comPbCObF8bT2$?fkH+i>L&@2A|M|ni2YeC028z<6$xMKt<;E(nAaKQ|x;N zC(5?n?3KK3q!h)jC#br?MSQ5~ROH_ujB;*1$-pNF2n=Ef z2(thDLBRw6dm~q?i{N9R?fIT)<*Qs=K4PwazZ%VvU@pCaFOWbq6^$`8cv-V*)=9!(~wffqAT0h85(jmhvt3`g!XYq7_pu(SpG zuFo4gz9bs{%})Pe%lop^TI8cg`F#@A=oJtIti85@I0G|4O1So9HM3OjX)lBAVSCYo zNc!rGzKXlPl|}C$?p8lKLiJ$;h3}y3K7d;xwj+16he&AiL^Os-U>abIdB9_^y`TH# zUS%N|z%vlSK_Z${z_JJto+}*4ZW3T+L?1i2$?x40Lis=+@)hM>3k9gH=m>P)CjkH- zrC&k8K<=vx2<|=O02Ls95dJH}J5x|O_z!h2Mn7;@BsJ_0{iHX_YkJdxzuluV*J~nv zZ+(RJ4=@zh^dfdJ9r~Aijm&+v5&I~Xpsfz4n0#e6%-Bk+Wn>UEAW9~lP78vslB;y~ zo1df|t7RsgDAXTT3*RqV<8tcwsXu_45jEVD7L)kuEBJ1qbUd)Eq-P496DbYJ-}BPO zXUZH{e_^Y0XEjZv=quW?TQ;N5JIKV6)dCoj75Gnk5ClN3>>=6re8pbedzbQtGSq7K zGS2*5XXa)F(uorON)mI(=YL`){fdAVXTtXR z?E>gtZZ#A~Wd{?Dh9T=cl@_C|pv$1#asILv1iP+hRKnFAZ)$A5PGi!~sPoXGhR()w z1HEsJtC>BKv>V0f6kr-PbMwil)~(80oiUwtVp(1yoW=XY642$zO00%CSjbM9Hw3~O zN{JssnFCFubzZ++sSh(;EyKsbeW~AV%|fD3h|W2=o>_m1xEg zS9JqIRzw!}X(6J|KG z9-ip9vJlnYdhKBhdc%p#m2DlLL6OW&Dmg0wd4-HxE=9wreebMg&URh&AI%XfWxo<% zTTsB>FK5HKq1$D>O=WW_LG?CzSi#~CA<- zK36RlA;PKAM?0TEf|`sPMp={ELiS6~jYefrI5~=W(mM~EG%)G7oz1DPkV-D58=U=? z>)PhLkx#h7)KFO|W~(XoErM-q##xTUbMp#Qy`e0QL5)aN+Vq_D}m#bjQA)?xQHbUF?>&b> zuiSSvN~gMti(Eo02wSosQnU^i4_LYr-&X zlj%ECr}SkjnA@NUOeSbPL2Np;qvFuYi~>C?<15|-ngY6(2gpwBR7V7+ou@-#=Z&~y zTY=GwE0CR+Y?}`Y2%9L2=FKk9Kk2whbTRSKtBU(Eo~D|o-O}0bFtL?!)y-4o=6d9Q z7EjP$WN{eyMfL53F13MF0~4>;#Cp(@U?a5=Dk7)h(39O}LY9vzi0nbvO%Il_(^ztc zo<&!Fb{9w`PplGJJ58Y0Y|0hqQouVl$XSONKyQmDFJ-CVayp#XYeVVBx|wep9f3+D zvQ4n!gOP{IyZ6JFhNun1$$o%*lY%g3Dz~Z_9-BdMR0b9$Y6rtlQ4^6&(&yc~I1iGo zS2$+!`m^OQ(Z#hke@*Su;D1+v+}2_`&#Q9~ECl**ts zd5);~Z&Y$GY?ngLCZ{N{FS|F49GF0g>0B3-AW>=bKBO%sbO|~TDgQ#DKcRzT5vLtZ zWi;OezJA%rP0L9~x_OMzPuKp!DXOE&(q^0^(}FqzqPTc*_~}(nO*F_?Tt8Q13Buex zQUspuM`!1e-_IhP9V}qyyG&Z-F{fq3c!dvJ4C3rxKB7k_S`SX75X@T8(5SbVQYx%t zCeZ}=>{c)@#SZrel(*pUOSWPr);$ex1I((16?Lz_*$JZrUmPO^*zQjI829Sb6a_x0)g36Wod$piD+WsTlnct7G#;>kCev7^LwzYL1n5)bF?A1y8or;AjG?4Vs zK2_1BkfMEqdD_ww5ie=v5MCpL{TrJNy8)DLx%r z&#XmHhq&O>tyfXJP99TItlVcYe}t>+7)ER@@>LM71QqZ1`tB|JYxf2mld0LT>F-6% zeyR4r9(H^slfuHPIK=E@zN~FH{!t|KOAR})zUFHy*C<1tU_SpC{;DonK{@?!$0AMw zqR!8h>aWX7Iuqh|o*UgBjVYgi;jd%BrR`F;(n*&~{V|a&Ipx($01mxGRR|IcbIlmP z1euEoX;?Gwm@nW97Ig!xY>C_-Pyn#uTqwTanQ~9CqF3(rCSY#@6-gNCFn3U#kmN{T zBmjJ^yR}JP>$vm{rzJz0(;RC|E5l}}IEU*P@5--R^aH<9j{#jsy{Za$t3Y>SgXPRv z;RB~xVJzrmmnWs^K859zwNclqytTpP!@*T!= zH3q9AcVI0dzC(PYg^8upVyP@yF}vlvreE4JcV%YNtUSF)J>trpjeRiIK)>b>1L-Z~ z8qrLt3(X&N`hx3e{5>B)rBO4QH1qTo$6pUv9(}qulWyoho-`6k#*}Rg?;d5l!v%IGJJVBekDVFlZ#etwfuSd$ z3Xf;KI`WL6Yo!llE#z5~U!+((O6HoJhjXT$fO`RrQ`??n9(ZzA(6UZEYcxWBQe2mmB|vYmQa4ZmP(5j#WEsOVNR2R9-EI9hUJfdBpie1 z;2+S%rpd?wDNNCI6O~^fUyj}IhT^bEK2pCtST6P|u6xV85Zl)8 z)-;%p$lE5`W&eJBp#O@P$Pul71x@DB$#CHR5BXT2W|`4%q@Q`xK?n>|wQyh-ru% z;F9*X++b7s7>P`1b*d!UX&Go%wd01Fbqya{(PjIF+=k43+@Q(3Ih*hJ+8HXc@ziXN z?`_1~T50UeYrJxQc4aE%p)?{r{=}HaQ1NI1sp-uFY*#S1Zn>BO_oAIU6xI=X2_eY; zyfm!YTG`#=SQX-p_YZkEYADZy-yE_2Znfy|O9G+61G@;}+V$V1Fck0m*{EBUU+@`*D>9RUFH^nE zxL%5K-x@%Mu5rs-V|pakt$o3FZ@3HwBWJ==Koc%L;QT5UV*_fw+?+qy~5L?@(IK~C3%Bpg^*dCPoO`VD;`j<(SQx=cYuEzJ3Kx9<4tk#9;6m~nFNpj+xdr`sp_liiuQ<%+_icThV{&~Licp|OR9`4yfb0$o7fGOyYqHYE!+r8=2#3HT za~SrGY&Pzj2)9k!Ff74qEn!^Ss%G4@ji+fZlCY9MetCHQZu}9bn92F~ctoQFG_oEwBkwH;L_&wCv)vIBgz2qdfj0G8Nawv#o%MPpxBlw(p1krpHS7RR z`$Yz*{t)EqY)fb@e5dgyY7_+b{ntJi^k)LUc@;Md3x&@Cb6@Lk)++)X0)qU%_rc6) zKpo!zOmD1@_ogvM5agnY7>-T0o`XBf9(~x5m>8QQIw@HgbV=^{r);ujjFZMmo3tF|(LT4oR>XL!ZRy=E4jC5@IbMLd>Z`&`u4=;+d zZ^wm^kTruMN2XAWPRX0y-w3j^F?kZ=fY>Eegh`(Vqr!^WElPad;-uRn!Q_|5(+n(o zN2QyD$48&=5V{qlc#LLea&KI4j0TFoTXv(@n zcXtv#>@z7mYUTCT5~_Ch5VCcLW-p*!9{lp2^ugI?GXGX9vn#aOtv&c6<^zN$0mAQv zk_E^}VF*tXkeJ%iPzGp>@^7*%A&5}#9iS`8J%)W5`Mj)Ss-wD$I}hSHji7EQIB4*b zh(FN^J0^gc%%mZUDNY!DPBvIR}ooqwwyh7X`mXLGVvE#bf9EqQCS;r zN6ckX>nGa>mD;=VL*#o=qk6#S^< z6W3B0EXNXzVuRUm1%)WC)|epi%nijOwwYyzXtmI-1|v^QYL}W2eg{IQVTya`>+zUn z)tUgTF$Ke#F@I9q>kL@?^g`upf?27t0ur+4Zq{+Yk}$@D=~w|U#;IT~7~?TMn4Nwe zD#4;%eIJd1b~d^_0mRPcb_sdL)N7E$ce5!mselG7fY7H6hI>^V06l_2 zL=IRa3;-En6dxYhlAO32lVz6Zyjq6Ws4w2e@mRDFXm zGReM}&?fI0F%D$29} zHP4JZ&oif!F0S4zU-Np0X^d4mnt$TtO0vGQTj}#cLufwTf}v1Z9w>nG~1 zV2ueg9Vu7TpDJ_A`fhu{7wOO~lbh|OL(9$8{WoeF-oHm0M*Bdw^PqFv#3(lv5LM^z z)f}5)Ele!-tg%;JHL){?B~g?V@k1lsE5$B*$K!hrBu@imygQpofyWcGCQ*-H@(1yx z|Kd#8Pd{LrJlQTL_?P+MbnN=rC%{Fw+mM1$@~ra9t4I z!&xVy1ImDP3ZY*8&n7~a*ScZPXT%b^us5?}mn71iJnHNj#+^Y~$k+)>-_x}M@eH_Q z?(Xn35{fdhp;`P0VyRtxt%sno6UikEmn)Za#NM#*!lJ+0=F_xX3(LG?fM2+mHbsIh z4X1$8Y=YGYQ{@UaSCMbJs%8LfD_Mqm@{m#FI_e_is-78poq$y!?A#UE`9q1}MtZXk zfI)9_>lm>GdN7!yL&*d)+t;I~;MlT)N~feGA|));Lt!qfrpUzw&>BedE|8f@I9|XU z>bD{-vhFbMl;UegpuF3b_9f{AKKho?Vh@^vU4nG*2LnM4H zEd&#WdK_UPsLe0cH0X!VX2)^+DJl0fa3Ygq?DPtwi)*5{hXd*^00D7iI`f*k?f3 z*wu(njYNj~q+YSm_sL~Wrp3~mi9-8?ej^mCG_%FVg29kinD?>3{h*E@eM1G35QXP- zQ=WUY5M?!`yJRnsiMlZ(d>GlqueV8#kW!x5FI@Ysw@Y>XQ61@S_99orI1jrJy5~bn zMd&R3qRDQ=D0PPrwosTw5BE+K$`!!B@%bmfy)3-!$yZpUqa7J9KC!`F7{)ZTR5X9s z+DIzSHzc_Ccz9J&3T_buevQV|Mdr&=B627E5I5e?yK*_J`u)!q%B)lo>tyLhW2WsS z5qp*VfX>fj)5 zV`*;x-_iNhlr7~Y72MJMW={qNqFo8eUg*pwl#&B+j3Qi$=mqFoGb@B`qDfQCu7sA{ zXA<9`aBB2;Y9qfr63c)&+qKb*V9PcC*^Rv82Vv(q+mF|`E2MrzVmz5*$|13c!6IZ- zi>{Jl#xYAMyqXgope3uF@Q(Y)l$0SWvLn&;!=@Yl3ep%>;_0BU_huPOnLIiXQeR6(?-dlLs{{utZJyF`F3`@R`*ClesEZAEnPqlDY;}SVS1R z7fby*m$Rzak^8=49GrF#{d4BI4!m=1sNHF|x>@VCljIu!RISg?TnR06R3B_G;@vS7 zSzb~moI}WGpY{~>T-U}ATdZ{$w71ey4?WMTKO%C4|h;X1fykFoJNyujJ_)Xbo zz|6sjU5A`rGd$)-&_E7(76{RmIErVZ8N&Sxn=2w3YVBCrtCz`ctAVe$gWcrt62v4M z6`kE-X$JojsE{$9#mZ`9hOW-Pf_qedGCqv!GzI=X4-xbG}5`%Gc?a0-${Tdx5A`@3y^MQbR*gn;zv=n^q_bYw^bG$>79N|uRn#;X~E;^ z7EwMtcx{QLkpBNi+z#1et&!=CR)jC#{i#vvuQNf&ebg5QdgB-7%dD2h5 z)N|MBd~<0(`4*>Bt+pZf$H!iLdIv4pd-|1+uf^~L2Y_R-B_CP&%7-JuM&um7$RE|n zYQXBmEH_uOi!5_Taz=Z9Q}C0C<*A6;FSf#7Bb)TLTJr8O4f+&>b^+a5QY&=bMtgcB z`M(eN@m6=ssk&9O>R(Phg%$Ufu!O~ld7e%!R$f~|co+=+lxq$K!tgxmq^C>S9?@+c zmV0j2xB$oJtgo?c2ftROCPn3QU(=FEmnO<`%*`(?~Se3Ol9tDni?7 zKRSqT#TsTm(r}m(E?HJuR4gW5gBWB+I$R`*E!O(R%#5@ zJ1w@>CpDL?YmB z!+|#vAAGs(3-qQyr{ae{KaO==8Vty}2k6Uf&RGX>^qE-JKJmaFE{4*iizD5{wJj#3N z@Pfbia)x5aaaUT{F~PZ`8mjj_Qk+0s5dkR9A>McrQrWg7-l*0X-BBd$o@e`8^{A0FPfY!tF}}#lf%(Y{n->BAA337N`XFrE~5JR6UU5j zQ7X-yet0g{ny>A+4AOFOvz=ov*$?tR4OA{g?c+@ygFE5+th)K|L)~})WyX^k%POGy zZAaD}H}$8zdh|SpmQ`y>G<0*v>kgxQRxvC8Q#q5*Ukvc=77xm595Bm|%N{D?+9(yk z%dPNMcvfI1B~EU{AI;p%qAiY2kq=zz=98mkZO{r7FS4z}dQ=H@Y^~2s46WEm)`&pm zy(!GDY};Y2EqJar>nvwQMp&KPO=;k-cYJ{mDuhMZ%xHv{V@q<=O5%DRF{ZZAEfg}S zNz}$Cb72ELtfrd%c3qZ4Nt3b9J;kLxR9I{S!bmvx*!~NEaF#!+9C+W;bX>2_b3)!@ zh*Vv}TG1N=;Zbewti+J?c_$La(4~5uB!?h+Y9;G=?qKalaoQjeG(%@iCN+Rt6uXe8 zyYW4;Sbm7vKf*3jfLY#;UXSz_@%&u}sUym2#81N68lVy$uATR($xx+y;+ZsfS+ zEH=DDvllZ_+_u0b3vr3q z1BF9VWF1*>M|r{_KxKpC6^OBOh}Csmt7kS$K=n=SgO5GJ65LWhE|~RE9LA zxHF%nkP>rMt%y?hxgN%W-3b{kYTZW&^~vUYt%cTCS51#8#X12s6WrB~T64@dmgz8K zabeR@_}?tJ%%9n+W0&9Y874MNldAg55i;fG7TxLJQs2uKDQ+v|`pQKrZh3_Y7hyaK z<#q}k={;4-<H-*c%C4Py4Sxwd zDp?R8BTDRj*VrBsQGIgimHy@LThIAW86fgU?FrHkWVz|<{P=hwnbFfN|9T&ibpz-zFcg(LczapPVmtrXF8I6{ZO|w>n zP8tw%NKE@LtezVuMSkU1zTzrO&YYE=AS~-=3gOy&=;1s30Pg;bKzLeswIOo3kil43 z51m=p66(J zlwL2r#!dF^TC2j|96t>C_YCiG#ssB2DN~iB5Rc0BqzKsYA2D;N`#py*a81Jo$ z7)<;?ny++*P!4pbjKCk`a-JnjH5T&;o|>ZX8|>410%{IC!XK+8(CxZtY`D{ZL;xA$ zzS7Lt_oT?B`_cE!eplg*LZE8cmPxu}UeoxhK0X@gyIcm=r~kUJ zJqyqTcPpSVqmjD68vmqM)GCFD9hXOSvMS19Axg6hf zk{!Bw{aLveknL@H0Kl4@syTr0$9E-B$ZZyEpx+Z!@i$BSOAU+rWGBbw&-Sf-8g$sWa_9j%-(UCzgV5~Z9H|c!VW3q3xUO?GQLEc5R^#7{vXX|M}^HoQZ7qb9#UGy81z8-?!LA0$_%eq&x(EXY)|H|>weX(z)&xD2Uu z8{ug2{@PN<2baC_6DBob^=kin<%B~UE0cfp%we^+ho~>``4&d?YOmFe{2{Y3 zg;0*x=(8=`Rq$`emRZ0VQYA@q{2S95E%0j>cRpF`6GDO+(VKUU05QM*AOZ2Ybz=)K zcQ8;Qu^&93wxMYoO-m199v+e8I*Y?9w2-u7ZFRlTi2Af}w!b_l zc14C)-#?J%W^HP$xvFb>b>zdC!|EA*vz;m?FiBBDjPq%0+CFue)oD&~fHl(e5!fZU zJ-8suZULRA?~J5N+ol@Nb4EImc2;kBU%H|~+MS;&c2!!*k5^=i0&(st-5WfNEnZ;X zi5)MgdK}?sDUHc%(4+Gt#GHV+$Kg8fK3CFWM}`4|qD0Ja$dM4=9oPNy#m}qchA8r! zr^cGz*O17HZmS?F5l?7;2}cI#6)OHoCuvmf8F56r(t;>@%200F6GcP=FzW zL`bXJGbeub&dShGz#KI>6Za%B-Ea96z)8I^Ps?$5UU)M2@OJzC9%5@uF2|BiRl+zS zq$edug*g%A&(G)$Z)bew{xu#5ljnYTJ@~tQNm2{QW*G7n*M_C^PthCk_ADG6&$DcJ zZi?Zm-f{&q-DyPqLzY6&0bd^%5KRP}@P}9Tg=YHvyaB;uLRZ5+Gl>*qE3Lb3_dl zXI7c$^=Vqp)Wz1K8*@?hDZb2M;nQv4Gi1l3E%zImmYb;~*+mJ7X!FAS4SyH028J#2 zRuB!#R@AanO*eu)SjhQo=-6yJF%!v6>ax6lk{Mr9`-g0CwW0f#c;vizFS~M`z!@yQ zIy%^6KBM!};NfoT4-f}Vu+D&%&&&H^V}yva4p}du{;b3#b3f~B>JFwG&bjPVyi#Cy z=5FTs=xdfr8qxS=LG&eo?Uyfj>^-3g)hM*=oRwbLiQe8KBr5#0#?$*v(@k*^MUG*s zikul)knv~+KGgB$Oq}6^tQuhn<=7cR1t3}_`|%RR6o_Rleqii+1(EqNWKg=k!D|N6 zJQJ%LcWnWm2g8<>uqwaf3X%;^T-bbn)yC;3Tx(X|Em?2TJVNk#D3%i#eo6VnDZ}%# zR}Y-B(QWLB(K-^(7Mw8E;VEpUcA-1wr25I%aAK42`_J(&Arbqcg;xPl)C?N$bSUS) zK%agqnAH#v_y8rqVjY9(hHgRB9E1Xb)-f-p^cC({KhMi6Un;>y)0kwbn?aTPz3O#P z8p)FVS^aJzivH*lrGZfvX3sro$Y!?_tckux z70r$aORx?t;L(+(ui$Y&x}rxAaTug>$VM0ISy?1&Jy6dotuvC1Mv6e8P8?I?WVb?` z6T#}tGEKT5)G-aGp%hwPasorcNM}=)V{(%U-JZjHfwA93%W>9WM6IEsY&JfakIOSJ zIg8)9p9wMD_p-P%WZ!rG`LV~g0!#0)4?u8P02y_&7u5h^=D<#w7yj-OQB#hJUZrvH={xrLh17RaF{e+d2OSbYY z3*9AgW~5b8Wz%#UK-fk4Iw)J#sZsK%vv(awe(pV;dD*sN{kdnkx@9tGxecHn`$29& z*p{jn+$?5iGyA>F+bHktL+9RK)&y)RRfM77f%&KoECV-gQ5kMm$isya5rE0HTS_4q z7*bum1uWV2mj<<*+*Gedp=(wti9K>RPYN2k$`0O&`K3q844a((t<*e-D-JEMSD5#_ z(&KY=2-sV_B9RF7U3-Cvp7z-5-!X1V=OrTyon5hMKYU5buKBfR)gFb*0eNr`Y0Dmq zKv^$6ql6aZ9qr2!OT(6;x>%(;&_k7y-kR)ka=+HVO0}uDGhD8k_K|?&%wFJI}R;O`cklo*lxj=`|yGhttzyB=IFvx&q{QEQL+ zvYvTr98=HFwaw4f72F6TD4YOCxSA~l;0sZ|=p!jDF#wsQj6K5&p{Nl1ssZ8K1|TXI z?uP*cg(38u0bs`<__+GSHs~I&3mdi@;pls69^4&LnzTN|Pd!5Bxh0lbwCSQtpt~NnV>oB6!3t! zL^-x8%cOqUyx86ZYV3%jXiD<=!Esq_i4i{#|IG6UIM&(kgSr_?Q}Ceq740^1jUMVp^dm&Yr!sa{j1bSW=ZK$fTb4Q| zKS)0U9nzV`F*U<(OA+eg#14fv@%*w^kJ}L>ntz807HYzg%Zm`-4)TEgMaiG~{;8L^hFJLn+MDIEebIka9DOIDrP13&`lWkA^rP(y zkZRk3Uj%RsC9~gVP?&VhhoX8SKD1>AsW& z>5$Q@Z-H~l=j0rc_@!4w;}TCnhkR~CqtJCv;;!K5s#rOd{^c1@WBJe+`I_t6K<|g| z5Jzj{O0`1Ag_=oC+1;xyv@bTus0F0eoY8PrIj>K)@`ppS-nwbyF=kX)R%Lx{)QEz;*8^w@&F3GGU*io054f9jY`f#8{WX7e7SH`qmK}`LF^-F=I+e zm0h_FJVcOYK#B4SnXuKY9IOkSU*WaPS1+sDb!cvTMz6*V)5eDrZ2#441A{aL9i!?J zcOyp{N@qQW`dX|F;D~GVWx`96t-x`T*FDDHN@0w*i zYP{jfBLwQiZ6>xhBo>Xg6`%9Xugh-Xq1=8%)cpaaQ4{O!NH$o@E40Gn!dpe88|K3Z z_Y;Dstv!p6^ZjUEiKh>UW&^n|U;lqC(3Ru7Al3<7!hbc){%xWCpQ9w00t%Ewf%Ugf z8Xpw1iU#t9MMM67%6RyHlz&^pKx`8@g#T(9`yZ>n=aOI-g#R)8zddB2%1JcBe>y+@ z<_#47cAIhjYY^P0{|q7nWlf+F{;T5uUxqGd|1pFIl}%xTo+j`CE+qd;-QZ&X*Ns3r zllTA=(tqd;Jkq}uJ;0jguSfs_PYMGV=>I}Skiir^0H5<8quePH!hcm){Og|3T>lsW znNdNnQ)q<$H~aB7ko><#NpP0Xe+=P~|8Fh?v^S1T_^;UW|Bm^u2WI-^KcnD464R^z zam|0kcsb;MrcyqQ5BQ_~4<$T<0+Le11-(tv1739hLkR&iP5*)UT124w8G3-F)juM5 zMgm}B`yU7gQk&%ke0KwZt*JopbA+Io*-rohcaVw=!(WjeVBrqpoD%?m+(E8$h5%x( zzb8D9gFPh(Wu6`|=LcGdBm|MV;D8+dik1QYi03w_f3;|!rFneFk-vo}L?EOEZU9o) zUnK>|YJm-K|KCu_4QCH_N!7nK1y z$so}sTfj@^Kg`^cB;Yv*B$`DB68Z53@R1J+{$UP4E&hi=T^0Z!m;QxZ|6C|(86N;& z@mFL4Z7%Zz9;*Jif^xxUP|y+@$Y2E@AYc0rmAxVZ2ygfc$w6>GSphqPAhLdPkp5qI zKKU0i|D7uuXzC|E0Bsg@{L>0>I0sT*wFI;;fX+wB{_7c{QT^*JA}oT0$7rxsw{>jWwr$(CHL*R>GqL%^nPg(yp4hf0w(Z=x^S!sedb_%6ueJ8>bGpu- zK4gE=!rLT>yjqw?mVPQf5 zX)Y2R70ivs6xp<-Rof`nMFPqQYA>;lG)fwyWH~oFAb*AJ`vKkkSfp%N;Sbwby|%dg z8T}b8Wb>3UDuNbN!LXFU{&v3pbm9NFe`WPs7}6O|m?mO3Cj`~mVeu`7=D4pj1`^V$j%II2Y2Z38#sJz8&P(2` zjWTte&|ACL*V{O3EAU(0Bt1_^5W*A+ua!<1e=mw01vYM>Y=_8Pb&ToFs;x~1|J`f7 zY?AfR)Y)PFCC+XaQ}TvpL0`heiV~}#`+d+TVE&1)%ivJyHOQd@GtJ1-y??B|eb3eE zC#eCdewcY=(FEZ~P7aqxMfy~GoGIq8f23&%GcFbJ)9q|FndHj4REFq{xKW*a^7y5t zd6?4Iefg!zkuHJ4% zOHwMayunN-G{&guwqoPv`hi-n)Q(bIk2R!0(>1lJLMaEHS9PXZj@Gnd7bdQpCwv+A z(V-tbc+ES%uZIxVOEaBjv{qw!jg9Cb9y&pRM-vv`rXh1U%GYk4`ll^4j*zn2FqA%d=A9qhSB`SEnJuTg#bv zyJ(g);;1KM6PMgd6ZT61aakbWse! z21a|sW*uz@$$fE=jeO5&BR;C1}M+mUOzX5{@4C9$5tvaygH|<>=JGuDttX|c*Xgv^;8wE%QhO4T>1AboCFT}l;{ey-3eF;)44K!L3pQ~_naGR!jO+UdE>`85q0kq!+6fX-<{wI+ zRUF_kRRle+a`^DLuklYo#4fOwLV_Ry21T5a46gpS^ii1xm(XZeo%^Iioi5Wt5~uh~ z1U)aVWJjooE7YsX?w<;1Z{TxnARr*3Ae_wtSv^P~AU_E~KuCekrdYtZMI=DB zF07xyux`k`~{KojTikl?ts%y3!_ooUc0Am2@y)KX$=NU+nx~Cirvojs!O=PSwZ>%=?E9*I$ zWGnu+#-uUsbN%b52g>x0Q_!=%pCl(hTha#Lv`ZZHEd34)1aRH>pk&=J2LMU|4?iMn zpl)iOTWsI?KglDkZhldH%Bz0rU)*y_zGMd0(EEQ%bADB1eyLA#Yuts|c9&&3(Plel ziZ#4SDwMGl&7l~hyxr)kzrV}!@vL@`9;DB_E-Gs{pjm#HFK%usV0V*^*l zL4zA})ioWHYdWJ7*TSzKN(R)@+9B#%jlGhDSp?JKE4E2q;O9}*k0$FYwoN8a7TdEP zc&ayN&gF8gSjrTTDuPweCpvFTwPwrl(u$T&D;nkSCOlGQhhXD3brsT=;-B+w&HI)g zZOr6-T5CHYueMLGV_!74W~W<6`#3VN)+wvZXDAd3@b4h5-ZYxaH2`v(Ykoh;eC1i+ z8yu-Rk|k8j9oUI_3~%rBhrdosb|?{-L*U844FJ*6kq)ZPl-ki9(5nTpyw;f79`76X znmx{BqgZ(^>q-b-)4E896$g`GML!y|emZAsl=G+F{tQ_wDcTT%2Bx9i6bdf2{K)2q zzKo+Z+X@hs?nlF8-~#xwep^rISLMG@7!(jM9><^tHP9cL^ui zr-q$(!w%cwpI?p1MpCXL4e!RKnyi?c%W)RV)6zFsOvrw(lK?1bIh^QG_2i8gOf_ci z@4j|UREHe3!tyH}%sKk?R&N?;WhwDq2EtOOl_9*#`1l!oQy9!ZIt9uoKk&;v;jJk- zecx0v>&voWxZ_>QP@pHBI5OWS18hwqX}`2atyR;aj<3n^6v%1Psbnbl25CaN`OI&* zuNBM_`bN!TvI3Zlb<;28CY15!%w#G^9m4FnEy79p%bdoDyr4GIP4>Wyo%D~D`6w($ z2$L0md99SK9QS!U(&JYTN|p9NO2eCn8SpmIv*u6~$E?s=JynZGsv3f}a3_yex`L<) z?|83DUcwG%Da@tWML!!@2`Je(tn%LK$5~F@;jQNB!vU1L$dB4&Bn@XT&pnV=9R-S8 zwXj?;(P*bzOCnfv$;YQo^D*(*IvyYj>g8)=Bn30$)^pf(t_P|Pz}0M<9}UFFGkGT! znJEqR(CJo{tSU?-#a9V~qPX@chA{NBt)O{z47h|fb0L$;7=CC`st*o;U(x^ta1@I- zRi#sK+yMN)R;p}?;nQwPZHXGT$-edWe}}hOG#H?S{}Vra+$}qu<(REylE=ZluO#oe zM;^39xovZ|>lW^65l`x+Td%#wxJvD%?;3yJa?RA)->1B1#n7gGNiK45Rw#~L$F60d z$k1;#L6f8QMy#S3PMPgG(-(ei3eRjB$D|U~Vh#AE?<#|&?dc7s~3ETI=NS=1CQD|*ip_V$X z@qw(zMp1(BJ({xLbuEeARSQJ^G7VIoNX4`^3Vk}sExlo1ba6#)8g&t0a}o#t@=RyM zL<_L3Ju9!v#)KY3UxIZ1iT0JA8C3ui63ojfWuY;zpm6HaaIsgcLQK?yKR1HbFfaM33q#Nq$8bvySvYeD$8}$(k9OtkH?sG2xX+zghZ5eiGb=J&=5eRS4Uf7J^gmqRt)Gg zq+%%>DN5&Vlh`&dlOa2iR6992q427gogLZK$It4K>}zUKKgAQT!%#%UdEKX9KEKjA?K7|y!r^p!l7s+u{Z4OE_;-i2?zhcdHxm@*s|-#6WHz>mt?0st61M_1nC zcv!|9{fGxn2Da6yhg4DEb)LOBl-R8(Ri|D=a(AA5SEW_oE_n~G7MdCxDY`476&SlO zzgKG@XwXNH&X>Lu#%QGYEmisghsu|veE8Gk=DCfzF z0uR28B-fCJSBx3nCQtv~a|49VYV<=$Ix-t=@Y-~!9;^?Ps=J!<<+f>7t7jEo?N*6j z+)|_bp*7-@M2&>~c6JN-)L=fGJoPE>IAIQkckiH`malPZBll`8kfF9rHAKP3cS2Li zx+0vZ@O{;YSd?YCL9_BmI-c7oyy~QWAUum^WRkF=}y-)wP+kPmmN6DL2|B_Adt6b)wdHwc_CIvg! zEC~R!p=~*tA!!%orF-9~bC-R1Jgl>8b_*u{yCsHrI@!gcZ8*YJXE>%Lz*SdsO6&p2 z!GKR1ZseDLF}FJtCOsg<|86>|$9pcjz6+8n`9=d5-PK?v%R=EJXf{nDoSExgs<%OY(kwqrbR9G0E7Ffc?M~ zZ#@LpoMp1B)tS;Y#6aGS>@+WYrfDOZ?<=PfdP!@VqBl^$iwd~fk9j3^Hs52Q!^^79 ztFJr2^NTh8!}*M#RYTeXYi@KYg@hO-HQCTjkS~+7p%Voluiog+F||b|U|kkD*AuXsJl6#wib3ua027 z$)3K0iTdp#QyY*9d7E5lymv{C_zUX%?LAL=eluBUH4AzgMvfABwaC!Qw- zDSEU95iiuAUW>0q3r}>%C)2!LjloxJg#7qitqDUe@C3|zELhc63bKUHToa@st6xXy zR-VH`v*|2e+S$XsS=MDT8P7Y0_~$vVjF>pAr1iFYegW#C{Ko9L7p?m*O%`)b%LO@2 z0V@+Gd)JrcQAeyEge?{*-{I(m!xZ!M*;^fuvckpnEnVKmD{Qs24C|g2D$AGtoN6x8 z*Lswn3Qp&h-Jq8uIE?4sBvbMEmdnC!h{*V7YC+XhmcLMBf?306rO;QfSqJPKc06RJ zBIxyh;saRvKM~gS9CH(sFPOKRAKP#5!ZMMUyWaDa+NbwC+Rr`wGyx5y{><}mE8{Qz z`>o-Zf2JYY(iYxkV!&4-k*3`11tXXUq=@5YcBEMcW^v-`UgOxa+cUNV5#*V3NQUQm zB9Zfni7AhUS$}A|MAa+r!Se(&?=W=7Kwo42EC67Y+<44w_2{AskOce$(yf@8N|f}( zt7YkR26^pC<1A!*W5u((Aj)<3wNa-tA=fVfVgQ=SuUzjuzM^A(5W<1KBse`fW1ecY z#qEsxm1nhn$;J4|)uqYPKGxG}k}i6qU5OW!HcnMvM@N=e1C6PlDoWc&W9<+sxoi7- z*a1*EoYw*1)41MSBEJLCQHT#VEMl1kDKpRTk6UFG!J~0uRk>{xM-ea#5&X8P;Hv{> z6+Ve^S2hX-zdbS15vYH(CRWVt-RINQD7vk%Zlw1rnYuxLdEQ(peO?^?${hc1X`~iqnY*<;Jzs2)o4qMBjp%3;~?w^zO;|8|! zx=#~4B2Vvb&G_RISW{qlU1y0>SGW=5GlObbbH1W!#ha z0ZFhLkBwu(2kW(S#KF~VXzn?PUuqeng%Pu&K-GQKphD{chv$c{)_xwJ!_da{^VzeIlP3s8DQ(B=w#W#f?z+tQu^ zq|iezjP=f?nEp!Mb9|aKwdQe`16|QKDvqLx-lhm%Q>3ycGE@X$El|jxsAA2VGf*7VGyv{<@Lb=)##@p$T3Bs~i|`+lUge*^NjWD8P0bOR zFVyTxKEA@D5t}QUKJGyp3s--P(Zd`72!7?pjrA**w#we5@Nw(HEo;b0JKY-GV9HQf z)1_IkWbqf~9LhktNn59fFGSARGz(60JHsbB8ZsGs4-k|(O>Zm6a~W5&bpWP}7%e8~ z{MEYCK>d>1f5(5j$1uIj$X8fZoe2n^`etNWdgI}ruMd%=jKx-jcdN)@=l{n0f_CWY z6ObsTVYWrw{tM4DoM>h(M|~}f$YT8xe)V(@Ikr@pghS8i6omcDf7X;(`16=$o`R16 zrok!%eAcvqmd}9L+S0sHqQ=nNz8kJV^IG8H9b};SYuOWktyw_edEE9ZYfO@gD+!6 z^wTd%C9-FS24~`YOhjjqodC|2jARfWI(p|3xMDoVZhco>-=O$aUfJ$ zGfL6SWU7Vl%u+Elqbz-*qFxeJULFl_^TaZ9bb^n69UNKUS_^|2ri5Bjl6J*jz5GXh zX$0I@%_m`i5ZLM6)VU*9mV^C=>7P4afvY$F?mu3SO@QCmWIq(W?QrqMxum}Vfs=*y z3abRsrU3S03?0_ebS;x%l>X$OJg&*wH>j%}u0YPKh2Qi5-UoMPCVDhi`D z0UVX0JWx&cts#O{;D0}9fzNT&RdXz{$=Y%Zd_$LqW$Fx(Y8caHeo={5^@@WF@y%v% z^8dcp7~8vhAF@LXD8zx+CpBuX zP+C;j_I`0*{O+gU8jqt+A<9iN)KZ&M(Ohy0jN$MN#2Plyt46o$bsS$xHav2D7L{I@ zpddSE?vXzxWIUa>Lhl}gp`fT}FFKgEW_54;U|^)Vl$4kbm;IsrCVjhmi&vcpA^_x; zPu<Gf{}DZO_eSEMWz0pw1^D#V`C309 ze$VH=;YI|ceL4ZX8hy$b@-AKz;45|64pU^3=|L;D#p2k)kFZ|_gFSj&=&A2M7Ji;* zMhBCpuvO>z1{lHGJL$CIrT&yWA(9)(oKIr!3~m>Y7f}km6ZKy!RgQhxrE^$UxT%&1 zrfaq?n-HWc&p~H^HTY$%0gyZ!H*L^8u1M$)AJ0VNga@5E7-;j#-`0_w<|*|BcH#&E zS>Y<*@O571(+p?v3CusMwK!S0jL$K2kEINNi`;eBqQ{j0_yXNgUvr`hsmNv*9C~Z~ z?i3s9w7VJ)QJk>{n=+OGX4@Dqd)}C-F{wbp?C?%mv90ef32*e=faX227j8g-Z8KkI z^`#tknAEP?s1e&^Lcek>pPB5KhKbYXpW3rzY+=Q6UB%5uiHiWrBH99l(@@bpiUxN3 zH$%vtNi>n=0}zr|kF@kZqEZXp&74l}0$+4G%`yyL24JarXa;g~S_JkfNS^P1{%Cg7 z5?TLfzBf?pw(mHX2P8`}m1YDF!M24U1-v+h^-M-IH;+MMnf$KWxXXC(?QRU19$vb7 z!MkG?jrc9NB7dRJizkha@yJcJJS|4ylqsoRZ-DNST;7UDXF7xWZYD4a>1k6o@7i>uimEw8L9T zU?3P=M)}dG{c#_%w}Vzq1YA10&Z)Q7{|RPDX&|15rUjW*QS{>dEU*-Uf(*S>O<2*B z+3z9v$@J?g2OuNhN_2&p-pj=6^Q&iE#W&wWsk#K{oood=lT0{R;HJax`6|qu!YD1* znm6z~Lk!q3(B86!+n`d~%gK?+KA}*Af+@Obe(2@U$k}S_F^$zrlaL7C)C}}43?d(x z#Q%O4SmSMhM4P$Ef))QW5T(mZCg%D|cf~3^R`c`MGyp=kJ)1!hm?b?j&cMqnt0g3( zBqX7gL#b{=sl7!a{V6)>HAB5*@=GWDgDi4gg4q#UoJVHdhBXZI1_Wxbfrlh#IKdmT zf7gQm&B<)RY6q2}U{n8E)KWA(b!pEtE`OmT`V)FYxV~m$HpCk$cmtD%OlcPcDXB;| zahOm7A3&A_FoWrbnIDED$Txr>UznpIK98O2$I*8D@rpDDw~#8hYv?W3n|)mi2Bh008~(Y&4=qDFc8J0|dmK9t4EsKVN0&|5SYcHz}>LxF}5B&^da& z0!E5(76DNoP6!(jLLtKeE29&GvGeVa5;uc#s*@D9$(B*euBl3&QE$22x=2$6jU>u$ zQE#KXYE7}Cd8zzY^9R;PRPoo{)`Ue80@yA2QTJP}iJ4w+39CX>s&#*~K}ZCYDd()fW} zDn~<6273(BtwHEfn|F5~yv2|h_vF5MAs{gtK)>InvtmeQUeZn*pVt1&@ttY>P|oP` zkgnQuuS#kM(@`&?i^a2@gTAN?6V3`Il-6@Ii-Pz_j$L|Z($RLG5zfxh(ef8Z0CyD- zK(wi-`15QR>wB{t`|zX#f%DCGrY$;q=my>aQ>iUC-}1%mR{_acyOq7;9rgEU)Q% zbN1@3{feU1DaGnkp0u5YJ2f3Aei`di*dsws5uMoWC+OWWLd;1m(Ssb=wC{>kOBJWa+vAAxS0ofcT`3 zdsUcdoyb55>e00`OX8)gMfa_LSQ8MA?c&N<1+b$+N3p~?Ajt@fT+2^00$pUzIF*B-8-ZEGUBCWrk4VvGI2c|KYhKM2T7(`xv}Nq#`{l^4nOg< zp2#hxaWlB9AG$2Z(a?EY9APDx2!(3tqrUbIKGf*Y*V^#%&FT9MV$PAHfTjEN%V=qE zDedoqwJ;=F(0UK)r1bg&$8BYTw*40_;O-ubA*x|`KPPWeu>yUTh7PWq51Dj~**S{s z?QLCpI09g_$0s$-j-|x!9IBSr6o1nCmG%A6Iu;_S(&VP=|9tS_n3+qd9^g!b>EX0X z*cLw^3M%V#FVH??HRhOc1gy?oB1@1S(bz!_1s`~Ts)O!9y^3l3&JlM8A2Q*#uFnm^ z8HXLLGd!Z_=q?t&H4hCq-ob~l`6&c$H_DCFquf`##I#~@s3s6b4-^P(4!p8-H5fkO zw*Mh;fn;nI<#Vzuy_c`JJ|J1du|~9$5-3MryxGPSw+JgTZ&#g%1@PeJ7ccs7U_=Z; z^f~AEE|4gt_SpHA{}BtlG%m0UpvN0R08lsN1@L3QNG6CN0Ju*+OGMdhTW4fACPG#$q9GEJ%SM2Gu zK`X-HU3A2JfNr+io0l$02ZNBQTSppPxA@Cupy!a@h0Snm!3cYA3GUaQMGe%4nmzOXgZm*it-E>Mx%(KS7PF zZaMv``j$tBALzakoK#+<{lMpLWI9i9UPuS9JvxC=i&+SeQh(|-sKP!(RABAUuOvbp0 z>7}(Ot{3}ec?h0!HmY_M1IRKcm!p02(V}q?(vuGw6inoJ!wugsX4SZyzb_rE1`lHYWp}`)(kFlu7xC zt0r(kIxH?OuA4&1Xe907kEXR>u&+^6zUv)WJ?o|bXk`e}+TQzE1;wSBhBN}=0F)s} z@^|kbd1?n4W6al0BUkxifnU+1HsIq7fE42-8};taIko3+DS*kE()V(Rj?TP9(!8Mj zav6bR?rfYUnxEvlF+S^W6{=416nZ-;r8oGYfQnnYcM!Cj)7j|SpZfA6zo#%15PI}P-# zffwxz^$so{lYX*^eA#f)&aWsu0CqtFmYXHX372qD9y%~4A)A_Re}4bTjbVZ+y&m|A zqp8C49A);ND{B+}SqF(5|FUJS8)S1AX)x+n^cMS5)IO^uBiZ{y%EjF1wA_4Ho9Q={ z?L}+oxB)g_)4)qP+n(&G1bhHr>j^C(qZbJ7S}LYZ);vOJ%U23 zVJX{oHrIajJ$~rocJY^i0F^lR!Yq@qXj{}AKX|byBlzBUO#P~BJh=`Bvl?9ZK&xq> zjz|47ID95?Gyltqw#AAWhDG^YUn0v`UoPcBYY+l9oMkEa&w^sAc>v}rASK`38WjA6 z*mP9_pa(H24-X3NggR^`)HWVq{u+*^EjD+C_Pdn*%0Kldie=aakt|BNvQcSK1{&*@ zd)E%EwsHV6LZ{Z1S=+oU7Q^AqRjUEncjg1$(;K5pO0p^~65VW?;%qKTicoy8NQUS=5 zVq9;2j(WxDMd^GWMHS>;D3H(E+ASLjA!vN^gGsoBZ<{5&;`&v-hRVV*VFutSCF6YC z)o0e;9?wCjvq=Tus`@2BYko|$#9#q;Q2*d`rU7j%LkV72F~G2I9KrG=HPYH4dWoaJ zu*v1YJz=Bv_L-SV?H+GeX?T6K&*)|{yFG{Cy7;LOo{>gpd~$x0|2_lVrZo9uI=>(G z1%zvUc36rLo;-DM_z6eo?G0CO^?*#GB(OUF3N^#24?WANPc!v}%5Qb%&HokDCnW1* zp9*riXmFFG9zZl%8kQe!4Phjuy(0MNI9BF7Vy+O1{?RWuWrVk`vG3wTKsi_>n7ppI zM^w-W4RxangBvZ<2GN;1CqV~()Sw`wt=CcXY#^sS&$&G!8hxzSj-;`{5nml1;Gm-~ zAzYZ9U{AK+ndsP8X~Pj25W`Kq8MEkF*$HXq{NA*`1Aw178X76$-FpI-bf-~qU_Q+Z zK&^wl9jo5gR`ey>O}D2|rT7qRa@Yh4E(gf}p{67XXT%m$+FE>al;u_|`;n}k~gd0GtQ_Qp8L>^2RL_Il{r zR&A#>1}vDdFV+W16>LH@PZuRN;?Asqq1$q#WZF=@+Np_*GQFwomib`Sq^MQH}eENGKSt|%BAzR{_Vt3m^^P{ z28f(&@mDd!(yA_WJPmYxEYRk}q!xspA-5eVt|aF$%nMeBidd0Hrk3!7<-?$|mHSm( zo}WZSS5uo7^=G0z@eoX{fqQ>KRY5iiKkNKBeSKx0#=+jz=bTJ8)SP(|U1F-`ssz$k zt(KOp&JUJrL$u#yp)P`kXdoH)`cIp84glsi zuB=iJgUPoP=jNo`MWxQxy-Q;M#FSwtO+^YnN!{$M2WU!tFJSKKm1hk zsBz`e-)SKN#t@8u_xzc^kHIW%2s1CRzbA$|SCT|no0tEtILIsSd)(;bcwF>NaZ0+h zel)d#0BW)5D&?a%gEbINbk1)<| zFqdEHHUpj@uHXcBy04V(9gw4EyzCr}vle^^&uz8qcs@BsKkDd@6?|sz%jsF3zP)n3 zR)^~v7i%l<5G#Rhv#`*D-~sZklVOK%WDmk^mDR+mp=C7_)8)4V4`elotvuFFqu?pM%H-FN|WJg9lk zI~+RHiGG^bzftG_qJ}`t_CQ%whj^mJ#1K-XX08-!Fj5Ue68MaGMv?%(z|cA_!^sG| znHabP%Ms#Jeb(njDMu8kF*A-CG6bNn&q+J>oA5_X*Sq?uw!+F9-gGl958-CtP3_+W zg2v!$2cw&w-h!?|PG}c~C_+w15t5L4g}E1!V)%ks5DMEB5`DNsR$sNtO*?Vt`Uw4m zi**n)y(aoV#3Byud=&a1{n*!)JJhVX*l`km7rML z#`HZ6w&yEHuREevWN}Kq*}k(jK=+KJCEdDyyQz4_3Kk3F^(%xGgN6P;g3c@G8I{G6 z*O@nmZJhLmhuvl|(B`#$_i%}(P^!nU9%G0lX;FQxDK{V zcKSOmW5=nixe3@xXRZ!*+F$gr?!~|1< z{*Mj|1!3sLC=i!GBdS|8J7NwlGkM>0eOp-=P0WsQy>b4d;J? zpn+;DEMNw5|7gYv7Z{8paCXH43`6;^Ap`2JvVb{i{dKYdyH@GI0`!4_mdrr-RTLo2 z8Xnkpqra2@XtKrwwqOO!TvG<)um+y3X@dD%1I5<)!78nRfOSJKZaZL&8!qr^T?y>i z2^i={0EG6%{x?X}1|C>|%U_8eNWXvr-1$qlT!B0OH2=J~At(s{_tu4h6yJfWn;Kxq zK7S24aBNcotl9q`+=xH}wk)9lHMj7<%6 Date: Sun, 1 Mar 2020 00:05:47 +0800 Subject: [PATCH 011/100] Fix NDK build error and switch to cmake --- archLintRulesTestDemo/build.gradle | 2 ++ build.gradle | 3 ++- jniLib/CMakeLists.txt | 15 ++++++++++++++ jniLib/build.gradle | 20 +++++++++---------- jniLib/src/main/{jni => cpp}/CommonJni.cpp | 0 jniLib/src/main/{jni => cpp}/CommonJni.h | 0 .../main/{jni => cpp}/FileStatusHelper.cpp | 0 .../{jni => cpp}/SysResourceLimitHelper.cpp | 0 jniLib/src/main/jni/Android.mk | 18 ----------------- jniLib/src/main/jni/Application.mk | 1 - jniLibDemo/build.gradle | 2 ++ 11 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 jniLib/CMakeLists.txt rename jniLib/src/main/{jni => cpp}/CommonJni.cpp (100%) rename jniLib/src/main/{jni => cpp}/CommonJni.h (100%) rename jniLib/src/main/{jni => cpp}/FileStatusHelper.cpp (100%) rename jniLib/src/main/{jni => cpp}/SysResourceLimitHelper.cpp (100%) delete mode 100644 jniLib/src/main/jni/Android.mk delete mode 100644 jniLib/src/main/jni/Application.mk diff --git a/archLintRulesTestDemo/build.gradle b/archLintRulesTestDemo/build.gradle index eb64d85..8495dd2 100644 --- a/archLintRulesTestDemo/build.gradle +++ b/archLintRulesTestDemo/build.gradle @@ -13,6 +13,8 @@ android { multiDexEnabled true } + ndkVersion versions.ndkVersion + buildTypes { release { minifyEnabled false diff --git a/build.gradle b/build.gradle index 50f39d9..b20f68d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.6.0' + classpath 'com.android.tools.build:gradle:3.6.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' @@ -33,6 +33,7 @@ allprojects { ext { versions.minSdk = 21 + versions.ndkVersion = "21.0.6113669" // For bintray upload bintrayMaven = [ diff --git a/jniLib/CMakeLists.txt b/jniLib/CMakeLists.txt new file mode 100644 index 0000000..b63664c --- /dev/null +++ b/jniLib/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.4.1) + +file(GLOB inc "src/main/cpp/*.h") +file(GLOB src "src/main/cpp/*.cpp") + +add_library(ycdev-commonjni + SHARED + ${src} + ) + +include_directories(${inc}) + +target_link_libraries(ycdev-commonjni + log + android) diff --git a/jniLib/build.gradle b/jniLib/build.gradle index e2fcb43..e25f2c9 100644 --- a/jniLib/build.gradle +++ b/jniLib/build.gradle @@ -9,10 +9,16 @@ android { defaultConfig { minSdkVersion versions.minSdk - externalNativeBuild { - ndkBuild { - abiFilters "armeabi-v7a", "arm64-v8a", "x86" - } + ndk { + abiFilters "armeabi-v7a", "arm64-v8a", "x86" + } + } + + ndkVersion versions.ndkVersion + externalNativeBuild { + cmake { + version "3.10.2" + path file('CMakeLists.txt') } } @@ -22,12 +28,6 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - - externalNativeBuild { - ndkBuild { - path file("src/main/jni/Android.mk") - } - } } dependencies { diff --git a/jniLib/src/main/jni/CommonJni.cpp b/jniLib/src/main/cpp/CommonJni.cpp similarity index 100% rename from jniLib/src/main/jni/CommonJni.cpp rename to jniLib/src/main/cpp/CommonJni.cpp diff --git a/jniLib/src/main/jni/CommonJni.h b/jniLib/src/main/cpp/CommonJni.h similarity index 100% rename from jniLib/src/main/jni/CommonJni.h rename to jniLib/src/main/cpp/CommonJni.h diff --git a/jniLib/src/main/jni/FileStatusHelper.cpp b/jniLib/src/main/cpp/FileStatusHelper.cpp similarity index 100% rename from jniLib/src/main/jni/FileStatusHelper.cpp rename to jniLib/src/main/cpp/FileStatusHelper.cpp diff --git a/jniLib/src/main/jni/SysResourceLimitHelper.cpp b/jniLib/src/main/cpp/SysResourceLimitHelper.cpp similarity index 100% rename from jniLib/src/main/jni/SysResourceLimitHelper.cpp rename to jniLib/src/main/cpp/SysResourceLimitHelper.cpp diff --git a/jniLib/src/main/jni/Android.mk b/jniLib/src/main/jni/Android.mk deleted file mode 100644 index bd4544c..0000000 --- a/jniLib/src/main/jni/Android.mk +++ /dev/null @@ -1,18 +0,0 @@ -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_MODULE := ycdev-commonjni - -define all-cpp-files-under -$(patsubst ./%,%, \ - $(shell cd $(LOCAL_PATH) ; \ - find $(1) -name "*.cpp" -and -not -name ".*") \ - ) -endef - -LOCAL_SRC_FILES := $(call all-cpp-files-under, .) - -LOCAL_LDLIBS := \ - -llog - -include $(BUILD_SHARED_LIBRARY) diff --git a/jniLib/src/main/jni/Application.mk b/jniLib/src/main/jni/Application.mk deleted file mode 100644 index 2133d20..0000000 --- a/jniLib/src/main/jni/Application.mk +++ /dev/null @@ -1 +0,0 @@ -APP_PLATFORM := android-21 diff --git a/jniLibDemo/build.gradle b/jniLibDemo/build.gradle index ce0e561..518661d 100644 --- a/jniLibDemo/build.gradle +++ b/jniLibDemo/build.gradle @@ -13,6 +13,8 @@ android { versionName "1.0" } + ndkVersion versions.ndkVersion + buildTypes { release { minifyEnabled false From 9dab8dc8dacd2be36d2ccc9f77ec9da78db9f0de Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 1 Mar 2020 01:01:02 +0800 Subject: [PATCH 012/100] Upgrade lint API libraries --- android_project_common.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 7e0350d..4943085 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -44,7 +44,7 @@ ext { 'recyclerView' : "1.1.0", 'constraintLayout' : "1.1.3", 'vectorDrawable' : "1.1.0", - 'lintLib' : "26.5.3", + 'lintLib' : "26.6.1", 'archCore' : "2.1.0", 'lifecycle' : "2.1.0", 'room' : "2.2.2", From be07ee87de29e93d049d88247184f842470eea26 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 1 Mar 2020 22:59:42 +0800 Subject: [PATCH 013/100] Make some functions to be open for override --- .../commonui/activity/GridEntriesActivity.kt | 28 +++++++++---------- .../lib/commonui/base/ListAdapterBase.kt | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt index 517b265..ead9b63 100644 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt @@ -26,10 +26,10 @@ import me.ycdev.android.arch.ArchConstants.IntentType abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { - protected lateinit var mAdapter: SystemEntriesAdapter - protected lateinit var mGridView: GridView + protected lateinit var adapter: SystemEntriesAdapter + protected lateinit var gridView: GridView - protected val contentViewLayout: Int + protected open val contentViewLayout: Int @LayoutRes get() = R.layout.commonui_grid_entries protected abstract val intents: List @@ -52,12 +52,12 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem super.onCreate(savedInstanceState) setContentView(contentViewLayout) - mAdapter = SystemEntriesAdapter(this) + adapter = SystemEntriesAdapter(this) - mGridView = findViewById(R.id.grid) - mGridView.adapter = mAdapter - mGridView.onItemClickListener = this - mGridView.onItemLongClickListener = this + gridView = findViewById(R.id.grid) + gridView.adapter = adapter + gridView.onItemClickListener = this + gridView.onItemLongClickListener = this loadItems() } @@ -71,16 +71,16 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem } override fun onPostExecute(result: List) { - mAdapter.setData(intents) + adapter.setData(intents) } }.execute() } else { - mAdapter.setData(intents) + adapter.setData(intents) } } override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { - val item = mAdapter.getItem(position) + val item = adapter.getItem(position) onItemClicked(item) } @@ -90,7 +90,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem position: Int, id: Long ): Boolean { - val item = mAdapter.getItem(position) + val item = adapter.getItem(position) ToastHelper.show(this, item.desc, Toast.LENGTH_LONG) return true } @@ -99,11 +99,11 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem * Decide if we need to invoke [.getIntent] async. * @return true for async and false for sync. false by default */ - protected fun needLoadIntentsAsync(): Boolean { + protected open fun needLoadIntentsAsync(): Boolean { return false } - protected fun onItemClicked(item: IntentEntry) { + protected open fun onItemClicked(item: IntentEntry) { if (IntentUtils.canStartActivity(this, item.intent)) { startActivity(item.intent) } else { diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt index 6845aa0..e918c8f 100644 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt @@ -12,7 +12,7 @@ import android.view.ViewGroup import android.widget.BaseAdapter @Suppress("unused", "MemberVisibilityCanBePrivate") -abstract class ListAdapterBase(context: Context) : +abstract class ListAdapterBase(protected val context: Context) : BaseAdapter() { protected var inflater: LayoutInflater = if (context is Activity) { context.layoutInflater @@ -31,7 +31,7 @@ abstract class ListAdapterBase(context: Context) return list } - fun setData(data: List?) { + open fun setData(data: List?) { list = data notifyDataSetChanged() } From a672d0d0ac98c9b32c88ab4c992ab4f60ebcef92 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Fri, 6 Mar 2020 22:24:06 +0800 Subject: [PATCH 014/100] Fix some bugs --- .../ycdev/android/lib/common/utils/PackageUtils.kt | 12 ++++++++++++ .../android/lib/common/wrapper/BroadcastHelper.kt | 2 +- .../lib/commonui/activity/GridEntriesActivity.kt | 6 ++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt index 523f0eb..0776263 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/PackageUtils.kt @@ -96,6 +96,10 @@ object PackageUtils { val pm = cxt.packageManager val flags = PackageManager.GET_RECEIVERS or PackageManager.MATCH_DISABLED_COMPONENTS val pkgInfo = pm.getPackageInfo(pkgName, flags) + if (pkgInfo.receivers == null) { + return emptyArray() + } + if (onlyExported) { val tmpArray = arrayOfNulls(pkgInfo.receivers.size) var size = 0 @@ -122,6 +126,10 @@ object PackageUtils { val pm = cxt.packageManager val flags = PackageManager.GET_SERVICES or PackageManager.MATCH_DISABLED_COMPONENTS val pkgInfo = pm.getPackageInfo(pkgName, flags) + if (pkgInfo.services == null) { + return emptyArray() + } + if (onlyExported) { val tmpArray = arrayOfNulls(pkgInfo.services.size) var size = 0 @@ -152,6 +160,10 @@ object PackageUtils { val pm = cxt.packageManager val flags = PackageManager.GET_ACTIVITIES or PackageManager.MATCH_DISABLED_COMPONENTS val pkgInfo = pm.getPackageInfo(pkgName, flags) + if (pkgInfo.activities == null) { + return emptyArray() + } + if (onlyExported) { val tmpArray = arrayOfNulls(pkgInfo.activities.size) var size = 0 diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt index e060636..a20230a 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/wrapper/BroadcastHelper.kt @@ -54,7 +54,7 @@ object BroadcastHelper { fun sendToExternal( cxt: Context, intent: Intent, - perm: String + perm: String? ) { cxt.sendBroadcast(intent, perm) } diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt index ead9b63..76ee0df 100644 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt @@ -64,7 +64,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem @SuppressLint("StaticFieldLeak") private fun loadItems() { - if (needLoadIntentsAsync()) { + if (needLoadIntentsAsync) { object : AsyncTask>() { override fun doInBackground(vararg params: Void): List { return intents @@ -99,9 +99,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem * Decide if we need to invoke [.getIntent] async. * @return true for async and false for sync. false by default */ - protected open fun needLoadIntentsAsync(): Boolean { - return false - } + protected open val needLoadIntentsAsync: Boolean = false protected open fun onItemClicked(item: IntentEntry) { if (IntentUtils.canStartActivity(this, item.intent)) { From a24b90058f7696cdbb8e1a13b0d327828389f68d Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Fri, 6 Mar 2020 23:40:45 +0800 Subject: [PATCH 015/100] Add SingletonHolderP1 --- .../android/lib/common/apps/AppsLoader.kt | 32 +++---------------- .../android/lib/common/dbmgr/SQLiteDbMgr.kt | 20 ++---------- .../lib/common/kotlin/SingletonHolderP1.kt | 11 +++++++ .../lib/common/tracker/BatteryInfoTracker.kt | 20 ++---------- 4 files changed, 20 insertions(+), 63 deletions(-) create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt index 8b78ac1..772211f 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt @@ -1,12 +1,12 @@ package me.ycdev.android.lib.common.apps -import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build +import me.ycdev.android.lib.common.kotlin.SingletonHolderP1 import me.ycdev.android.lib.common.utils.MiscUtils import me.ycdev.android.lib.common.utils.PackageUtils import me.ycdev.android.lib.common.utils.StringUtils @@ -15,15 +15,9 @@ import java.util.ArrayList import java.util.HashMap class AppsLoader private constructor(cxt: Context) { - private val appContext: Context - private val pm: PackageManager - private val myselfPkgName: String - - init { - appContext = cxt.applicationContext - pm = cxt.packageManager - myselfPkgName = cxt.packageName - } + private val appContext: Context = cxt.applicationContext + private val pm: PackageManager = cxt.packageManager + private val myselfPkgName: String = cxt.packageName @TargetApi(Build.VERSION_CODES.N) fun loadInstalledApps( @@ -134,21 +128,5 @@ class AppsLoader private constructor(cxt: Context) { return item } - companion object { - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: AppsLoader? = null - - fun getInstance(cxt: Context): AppsLoader { - if (instance == null) { - synchronized(AppsLoader::class.java) { - if (instance == null) { - instance = AppsLoader(cxt) - } - } - } - return instance!! - } - } + companion object : SingletonHolderP1(::AppsLoader) } diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt index c487775..a34d79e 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt @@ -1,10 +1,9 @@ package me.ycdev.android.lib.common.dbmgr -import android.annotation.SuppressLint import android.content.Context import android.database.sqlite.SQLiteDatabase +import me.ycdev.android.lib.common.kotlin.SingletonHolderP1 import timber.log.Timber - import java.util.HashMap @Suppress("unused") @@ -57,24 +56,9 @@ class SQLiteDbMgr private constructor(cxt: Context) { } } - companion object { + companion object : SingletonHolderP1(::SQLiteDbMgr) { private const val TAG = "SQLiteDbMgr" - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: SQLiteDbMgr? = null - - private fun getInstance(cxt: Context): SQLiteDbMgr { - if (instance == null) { - synchronized(SQLiteDbMgr::class.java) { - if (instance == null) { - instance = SQLiteDbMgr(cxt) - } - } - } - return instance!! - } - fun acquireDatabase( cxt: Context, dbInfoClass: Class diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt new file mode 100644 index 0000000..284cec8 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt @@ -0,0 +1,11 @@ +package me.ycdev.android.lib.common.kotlin + +open class SingletonHolderP1(private val creator: (P) -> T) { + @Volatile + private var instance: T? = null + + fun getInstance(param: P): T = + instance ?: synchronized(this) { + instance ?: creator(param).also { instance = it } + } +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt index 0d4e008..0224c19 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt @@ -1,12 +1,11 @@ package me.ycdev.android.lib.common.tracker -import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager - +import me.ycdev.android.lib.common.kotlin.SingletonHolderP1 import me.ycdev.android.lib.common.utils.LibLogger import me.ycdev.android.lib.common.wrapper.BroadcastHelper import me.ycdev.android.lib.common.wrapper.IntentHelper @@ -109,22 +108,7 @@ class BatteryInfoTracker private constructor(cxt: Context) : } } - companion object { + companion object : SingletonHolderP1(::BatteryInfoTracker) { private const val TAG = "BatteryInfoTracker" - - @SuppressLint("StaticFieldLeak") - private var instance: BatteryInfoTracker? = null - - @Synchronized - fun getInstance(cxt: Context): BatteryInfoTracker { - if (instance == null) { - synchronized(BatteryInfoTracker::class.java) { - if (instance == null) { - instance = BatteryInfoTracker(cxt) - } - } - } - return instance!! - } } } From 932c9a659d07efdedce0ad79e612ec382802d460 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 8 Mar 2020 00:49:52 +0800 Subject: [PATCH 016/100] Refactor some code --- android_module_common.gradle | 5 +- android_project_common.gradle | 8 +- .../lib/common/androidx/app/JobScheduler.kt | 26 +++++ .../android/lib/common/apps/AppsLoader.kt | 2 +- .../android/lib/common/dbmgr/SQLiteDbMgr.kt | 2 +- .../lib/common/ipc/ServiceClientBase.kt | 17 +++- .../android/lib/common/kotlinx/Arrays.kt | 36 +++++++ .../{kotlin => pattern}/SingletonHolderP1.kt | 2 +- .../lib/common/tracker/BatteryInfoTracker.kt | 2 +- .../android/lib/common/utils/IntentUtils.kt | 98 ++++++++++++++++++- .../commonui/activity/GridEntriesActivity.kt | 73 ++++++++------ 11 files changed, 228 insertions(+), 43 deletions(-) create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/androidx/app/JobScheduler.kt create mode 100644 baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt rename baseLib/src/main/java/me/ycdev/android/lib/common/{kotlin => pattern}/SingletonHolderP1.kt (86%) diff --git a/android_module_common.gradle b/android_module_common.gradle index 1156838..e5fb9f6 100644 --- a/android_module_common.gradle +++ b/android_module_common.gradle @@ -1,6 +1,7 @@ -/** +/* * File: 'android_module_common.gradle' - * Version: 2018.5.26 + * Location: https://raw.githubusercontent.com/yongce/AndroidLib/master/android_module_common.gradle + * Version: 2020.03.08 * All android projects can copy and include this file. */ diff --git a/android_project_common.gradle b/android_project_common.gradle index 4943085..41d51b3 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -1,6 +1,7 @@ -/** +/* * File: 'android_project_common.gradle' - * Version: 2018.5.26 + * Location: https://raw.githubusercontent.com/yongce/AndroidLib/master/android_project_common.gradle + * Version: 2020.03.07 * All android projects can copy and include this file. */ @@ -36,6 +37,7 @@ ext { // Android official support 'kotlin' : "1.3.61", + 'kotlinCoroutine' : "1.3.4", 'multidexLib' : "2.0.1", 'androidxCore' : "1.1.0", 'fragment' : "1.1.0", @@ -103,6 +105,8 @@ ext { // Android official support 'kotlin': [ 'stdlib' : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}", + 'coroutinesCore' : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutine}", + 'coroutinesAndroid' : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.kotlinCoroutine}" ], 'androidx': [ // core diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/androidx/app/JobScheduler.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/androidx/app/JobScheduler.kt new file mode 100644 index 0000000..8cc0cf1 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/androidx/app/JobScheduler.kt @@ -0,0 +1,26 @@ +@file:Suppress("unused") + +package me.ycdev.android.lib.common.androidx.app + +import android.app.job.JobInfo +import android.app.job.JobScheduler +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES + +fun JobScheduler.getPendingJobCompat(jobId: Int): JobInfo? { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return getPendingJob(jobId) + } else { + val jobInfoList = allPendingJobs + for (jobInfo in jobInfoList) { + if (jobInfo.id == jobId) { + return jobInfo + } + } + return null + } +} + +fun JobScheduler.isJobScheduled(jobId: Int): Boolean { + return getPendingJobCompat(jobId) != null +} diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt index 772211f..7d9de81 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt @@ -6,7 +6,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build -import me.ycdev.android.lib.common.kotlin.SingletonHolderP1 +import me.ycdev.android.lib.common.pattern.SingletonHolderP1 import me.ycdev.android.lib.common.utils.MiscUtils import me.ycdev.android.lib.common.utils.PackageUtils import me.ycdev.android.lib.common.utils.StringUtils diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt index a34d79e..6fae363 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/dbmgr/SQLiteDbMgr.kt @@ -2,7 +2,7 @@ package me.ycdev.android.lib.common.dbmgr import android.content.Context import android.database.sqlite.SQLiteDatabase -import me.ycdev.android.lib.common.kotlin.SingletonHolderP1 +import me.ycdev.android.lib.common.pattern.SingletonHolderP1 import timber.log.Timber import java.util.HashMap diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt index 90a153f..3058007 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt @@ -51,7 +51,15 @@ open class ServiceClientBase protected constructor( * Disconnect the Service connection. This may cause the pending operations to lost! */ fun disconnect() { - serviceConnector.disconnect() + disconnectDelayed(0); + } + + /** + * Disconnect the Service connection. This may cause the pending operations to lost! + */ + fun disconnectDelayed(delayMs: Long) { + operationHandler.removeMessages(MSG_DELAY_DISCONNECT); + operationHandler.sendEmptyMessageDelayed(MSG_DELAY_DISCONNECT, delayMs); } fun addOperation(operation: IpcOperation) { @@ -62,6 +70,7 @@ open class ServiceClientBase protected constructor( serviceConnector.connect() } operationHandler.removeMessages(MSG_AUTO_DISCONNECT) + operationHandler.removeMessages(MSG_DELAY_DISCONNECT) Message.obtain(operationHandler, MSG_NEW_OPERATION, operation).sendToTarget() } @@ -133,6 +142,11 @@ open class ServiceClientBase protected constructor( Timber.tag(TAG).d("auto disconnect") serviceConnector.disconnect() } + + MSG_DELAY_DISCONNECT -> { + Timber.tag(TAG).d("delayed disconnect") + serviceConnector.disconnect() + } } if (isAutoDisconnectEnabled && msg.what != MSG_AUTO_DISCONNECT) { @@ -149,5 +163,6 @@ open class ServiceClientBase protected constructor( private const val MSG_NEW_OPERATION = 1 private const val MSG_PENDING_OPERATIONS = 2 private const val MSG_AUTO_DISCONNECT = 3 + private const val MSG_DELAY_DISCONNECT = 4 } } diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt new file mode 100644 index 0000000..77212e6 --- /dev/null +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt @@ -0,0 +1,36 @@ +@file:Suppress("unused") + +package me.ycdev.android.lib.common.kotlinx + +fun BooleanArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun CharArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun ByteArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun ShortArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun IntArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun LongArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun FloatArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + +fun DoubleArray?.isNullOrEmpty(): Boolean { + return this == null || this.isEmpty() +} + diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/pattern/SingletonHolderP1.kt similarity index 86% rename from baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt rename to baseLib/src/main/java/me/ycdev/android/lib/common/pattern/SingletonHolderP1.kt index 284cec8..dbbea9d 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/kotlin/SingletonHolderP1.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/pattern/SingletonHolderP1.kt @@ -1,4 +1,4 @@ -package me.ycdev.android.lib.common.kotlin +package me.ycdev.android.lib.common.pattern open class SingletonHolderP1(private val creator: (P) -> T) { @Volatile diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt index 0224c19..297868b 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/tracker/BatteryInfoTracker.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager -import me.ycdev.android.lib.common.kotlin.SingletonHolderP1 +import me.ycdev.android.lib.common.pattern.SingletonHolderP1 import me.ycdev.android.lib.common.utils.LibLogger import me.ycdev.android.lib.common.wrapper.BroadcastHelper import me.ycdev.android.lib.common.wrapper.IntentHelper diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt index 5d792f8..6c47328 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/utils/IntentUtils.kt @@ -1,16 +1,110 @@ +@file:Suppress("unused") + package me.ycdev.android.lib.common.utils +import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.PowerManager +import timber.log.Timber +@Suppress("MemberVisibilityCanBePrivate") object IntentUtils { - fun canStartActivity(cxt: Context, activityIntent: Intent): Boolean { + private const val TAG = "IntentUtils" + + const val EXTRA_FOREGROUND_SERVICE = "extra.foreground_service" + + fun canStartActivity(cxt: Context, intent: Intent): Boolean { // Use PackageManager.MATCH_DEFAULT_ONLY to behavior same as Context#startAcitivty() val resolveInfo = cxt.packageManager.resolveActivity( - activityIntent, + intent, PackageManager.MATCH_DEFAULT_ONLY ) return resolveInfo != null } + + fun startActivity(context: Context, intent: Intent): Boolean { + return if (canStartActivity(context, intent)) { + context.startActivity(intent) + true + } else { + Timber.tag(TAG).w("cannot start Activity: $intent") + false + } + } + + fun needForegroundService( + context: Context, + ai: ApplicationInfo, + listenSensor: Boolean + ): Boolean { + // no background limitation before Android O + if (VERSION.SDK_INT < VERSION_CODES.O) { + return false + } + + // Need foreground service on Android P to listen sensors + if (listenSensor && VERSION.SDK_INT >= VERSION_CODES.P) { + return true + } + + // The background limitation only works when the targetSdk is 26 or higher + if (ai.targetSdkVersion < VERSION_CODES.O) { + return false + } + + // no background limitation if the app is in the battery optimization whitelist. + val powerMgr = context.getSystemService(Context.POWER_SERVICE) as PowerManager + if (powerMgr.isIgnoringBatteryOptimizations(ai.packageName)) { + return false + } + + // yes, we need foreground service + return true + } + + fun needForegroundService(context: Context, listenSensor: Boolean): Boolean { + return needForegroundService(context, context.applicationInfo, listenSensor) + } + + @SuppressLint("NewApi") + fun startService(context: Context, intent: Intent): Boolean { + val resolveInfo = context.packageManager.resolveService(intent, 0) ?: return false + intent.setClassName( + resolveInfo.serviceInfo.packageName, + resolveInfo.serviceInfo.name + ) + // Here, we should set "listenSensor" to false. + // The target service still can set "listenSensor" to true for its checking. + if (needForegroundService(context, resolveInfo.serviceInfo.applicationInfo, false)) { + intent.putExtra(EXTRA_FOREGROUND_SERVICE, true) + context.startForegroundService(intent) + } else { + intent.putExtra(EXTRA_FOREGROUND_SERVICE, false) + context.startService(intent) + } + return true + } + + fun startForegroundService(context: Context, intent: Intent): Boolean { + val resolveInfo = context.packageManager.resolveService(intent, 0) ?: return false + intent.setClassName( + resolveInfo.serviceInfo.packageName, + resolveInfo.serviceInfo.name + ) + if (VERSION.SDK_INT < VERSION_CODES.O) { + intent.putExtra(EXTRA_FOREGROUND_SERVICE, false) + context.startService(intent) + } else { + // here, we add an extra so that the target service can know + // it needs to call #startForeground() + intent.putExtra(EXTRA_FOREGROUND_SERVICE, true) + context.startForegroundService(intent) + } + return true + } } diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt index 76ee0df..40f4163 100644 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt @@ -10,18 +10,17 @@ import android.widget.AdapterView import android.widget.GridView import android.widget.TextView import android.widget.Toast - import androidx.annotation.LayoutRes import me.ycdev.android.arch.ArchConstants +import me.ycdev.android.arch.ArchConstants.IntentType import me.ycdev.android.arch.activity.AppCompatBaseActivity import me.ycdev.android.arch.wrapper.ToastHelper import me.ycdev.android.lib.common.utils.IntentUtils +import me.ycdev.android.lib.common.wrapper.BroadcastHelper import me.ycdev.android.lib.commonui.R import me.ycdev.android.lib.commonui.base.ListAdapterBase import me.ycdev.android.lib.commonui.base.ViewHolderBase -import me.ycdev.android.arch.ArchConstants.IntentType - @Suppress("MemberVisibilityCanBePrivate", "unused") abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { @@ -32,19 +31,38 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem protected open val contentViewLayout: Int @LayoutRes get() = R.layout.commonui_grid_entries - protected abstract val intents: List - - class IntentEntry(var intent: Intent, var title: String, var desc: String) { - @IntentType - var type = ArchConstants.INTENT_TYPE_ACTIVITY - var perm: String? = null + protected abstract val intents: List + + open class Entry( + open val title: CharSequence, + open val desc: CharSequence, + open val clickAction: ((Context) -> Unit)? = null, + open val longClickAction: ((Context) -> Unit)? = null + ) + + open class IntentEntry( + @IntentType val type: Int = ArchConstants.INTENT_TYPE_ACTIVITY, + val intent: Intent, + title: String, + desc: String, + val perm: String? = null + ) : Entry(title, desc) { + constructor(intent: Intent, title: String, desc: String) : + this(ArchConstants.INTENT_TYPE_ACTIVITY, intent, title, desc) + + override val clickAction: ((Context) -> Unit)? = ::onItemClicked + override val longClickAction: ((Context) -> Unit)? = ::onItemLongClicked + + fun onItemClicked(context: Context) { + if (type == ArchConstants.INTENT_TYPE_ACTIVITY) { + IntentUtils.startActivity(context, intent) + } else if (type == ArchConstants.INTENT_TYPE_BROADCAST) { + BroadcastHelper.sendToExternal(context, intent, perm) + } + } - constructor(@IntentType type: Int, intent: Intent, title: String, desc: String) : this( - intent, - title, - desc - ) { - this.type = type + private fun onItemLongClicked(context: Context) { + ToastHelper.show(context, desc, Toast.LENGTH_LONG) } } @@ -65,12 +83,12 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem @SuppressLint("StaticFieldLeak") private fun loadItems() { if (needLoadIntentsAsync) { - object : AsyncTask>() { - override fun doInBackground(vararg params: Void): List { + object : AsyncTask>() { + override fun doInBackground(vararg params: Void): List { return intents } - override fun onPostExecute(result: List) { + override fun onPostExecute(result: List) { adapter.setData(intents) } }.execute() @@ -81,7 +99,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val item = adapter.getItem(position) - onItemClicked(item) + item.clickAction?.invoke(this) } override fun onItemLongClick( @@ -91,7 +109,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem id: Long ): Boolean { val item = adapter.getItem(position) - ToastHelper.show(this, item.desc, Toast.LENGTH_LONG) + item.longClickAction?.invoke(this) return true } @@ -101,25 +119,16 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem */ protected open val needLoadIntentsAsync: Boolean = false - protected open fun onItemClicked(item: IntentEntry) { - if (IntentUtils.canStartActivity(this, item.intent)) { - startActivity(item.intent) - } else { - ToastHelper.show(this, item.desc, Toast.LENGTH_LONG) - } - } - protected open class SystemEntriesAdapter(cxt: Context) : - ListAdapterBase(cxt) { + ListAdapterBase(cxt) { - override val itemLayoutResId: Int - get() = R.layout.commonui_grid_entries_item + override val itemLayoutResId: Int = R.layout.commonui_grid_entries_item override fun createViewHolder(itemView: View, position: Int): ViewHolder { return ViewHolder(itemView, position) } - override fun bindView(item: IntentEntry, holder: ViewHolder) { + override fun bindView(item: Entry, holder: ViewHolder) { holder.titleView.text = item.title } From 5c5233f723721af022fec8ca53896e32cbef4851 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 8 Mar 2020 01:10:38 +0800 Subject: [PATCH 017/100] Fix spotlessKotlin issues --- .../me/ycdev/android/lib/common/ipc/ServiceClientBase.kt | 6 +++--- .../main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt index 3058007..2c9a360 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/ipc/ServiceClientBase.kt @@ -51,15 +51,15 @@ open class ServiceClientBase protected constructor( * Disconnect the Service connection. This may cause the pending operations to lost! */ fun disconnect() { - disconnectDelayed(0); + disconnectDelayed(0) } /** * Disconnect the Service connection. This may cause the pending operations to lost! */ fun disconnectDelayed(delayMs: Long) { - operationHandler.removeMessages(MSG_DELAY_DISCONNECT); - operationHandler.sendEmptyMessageDelayed(MSG_DELAY_DISCONNECT, delayMs); + operationHandler.removeMessages(MSG_DELAY_DISCONNECT) + operationHandler.sendEmptyMessageDelayed(MSG_DELAY_DISCONNECT, delayMs) } fun addOperation(operation: IpcOperation) { diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt index 77212e6..6c59a91 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/kotlinx/Arrays.kt @@ -33,4 +33,3 @@ fun FloatArray?.isNullOrEmpty(): Boolean { fun DoubleArray?.isNullOrEmpty(): Boolean { return this == null || this.isEmpty() } - From 7823c27650876aa4b0ae861c3b51bbee3a44a158 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 8 Mar 2020 01:17:35 +0800 Subject: [PATCH 018/100] Prepare release for v1.5.5 --- android_project_common.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 41d51b3..9a26c06 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -96,7 +96,7 @@ ext { 'rxandroid' : "2.1.1", // ycdev - 'androidLib' : "1.5.4", + 'androidLib' : "1.5.5", // others 'zxing' : "3.3.1", diff --git a/build.gradle b/build.gradle index b20f68d..4047ae3 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ ext { 'projectScmDevConnection': 'ssh://git@github.com/yongce/AndroidLib.git', 'projectInceptionYear': '2013', 'groupId': 'me.ycdev.android', - 'version': '1.5.4', + 'version': '1.5.5', 'developerId': 'yongce', 'developerName': 'Yongce Tu', 'developerEmail': 'yongce.tu@gmail.com', From 71c571c35c7762508eaa1a0d8dbb66fd19335d90 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 8 Mar 2020 15:10:42 +0800 Subject: [PATCH 019/100] Upgrade Kotlin version to v1.3.70 --- android_project_common.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 9a26c06..5ca94a5 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -1,7 +1,7 @@ /* * File: 'android_project_common.gradle' * Location: https://raw.githubusercontent.com/yongce/AndroidLib/master/android_project_common.gradle - * Version: 2020.03.07 + * Version: 2020.03.08 * All android projects can copy and include this file. */ @@ -36,7 +36,7 @@ ext { 'compileSdk' : 29, // Android official support - 'kotlin' : "1.3.61", + 'kotlin' : "1.3.70", 'kotlinCoroutine' : "1.3.4", 'multidexLib' : "2.0.1", 'androidxCore' : "1.1.0", From 697038852487c96e9d0a6345121a1be0c7ca8ad0 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 8 Mar 2020 15:36:36 +0800 Subject: [PATCH 020/100] Add Kotlin option jvmTarget to all modules --- android_module_common.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android_module_common.gradle b/android_module_common.gradle index e5fb9f6..c41d95f 100644 --- a/android_module_common.gradle +++ b/android_module_common.gradle @@ -138,6 +138,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = "1.8" + } + testOptions { unitTests.all { // All the usual Gradle options. From 09712b70f2b4eac266ffb6875432820b132cd429 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Thu, 12 Mar 2020 00:44:11 +0800 Subject: [PATCH 021/100] Add the library 'flexbox' --- android_project_common.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 5ca94a5..2ed7169 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -1,7 +1,7 @@ /* * File: 'android_project_common.gradle' * Location: https://raw.githubusercontent.com/yongce/AndroidLib/master/android_project_common.gradle - * Version: 2020.03.08 + * Version: 2020.03.12 * All android projects can copy and include this file. */ @@ -264,6 +264,9 @@ ext { 'glide' : "com.github.bumptech.glide:glide:${versions.glide}", 'glideTrans' : "jp.wasabeef:glide-transformations:${versions.glideTrans}", + // UI + 'flexbox' : "com.google.android:flexbox:2.0.1", + // rx 'rx': [ 'rxjava' : "io.reactivex.rxjava2:rxjava:${versions.rxjava}", From af153fea50e44e360f8cbc389e607a293e0f54db Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Thu, 12 Mar 2020 22:56:52 +0800 Subject: [PATCH 022/100] Update the Android X libraries --- android_project_common.gradle | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/android_project_common.gradle b/android_project_common.gradle index 2ed7169..7c71c92 100644 --- a/android_project_common.gradle +++ b/android_project_common.gradle @@ -1,7 +1,7 @@ /* * File: 'android_project_common.gradle' * Location: https://raw.githubusercontent.com/yongce/AndroidLib/master/android_project_common.gradle - * Version: 2020.03.12 + * Version: 2020.03.13 * All android projects can copy and include this file. */ @@ -39,8 +39,8 @@ ext { 'kotlin' : "1.3.70", 'kotlinCoroutine' : "1.3.4", 'multidexLib' : "2.0.1", - 'androidxCore' : "1.1.0", - 'fragment' : "1.1.0", + 'androidxCore' : "1.2.0", + 'fragment' : "1.2.2", 'preference' : "1.1.0", 'palette' : "1.0.0", 'recyclerView' : "1.1.0", @@ -48,13 +48,13 @@ ext { 'vectorDrawable' : "1.1.0", 'lintLib' : "26.6.1", 'archCore' : "2.1.0", - 'lifecycle' : "2.1.0", - 'room' : "2.2.2", - 'sqlite' : "2.0.1", - 'navigation' : "2.1.0", - 'paging' : "2.1.0", - 'work' : "2.2.0", - 'media2' : "1.0.1", + 'lifecycle' : "2.2.0", + 'room' : "2.2.4", + 'sqlite' : "2.1.0", + 'navigation' : "2.2.1", + 'paging' : "2.1.1", + 'work' : "2.3.3", + 'media2' : "1.0.3", // test 'testCore' : "1.2.0", @@ -139,7 +139,7 @@ ext { 'vectorDrawable' : "androidx.vectordrawable:vectordrawable:${versions.vectorDrawable}", 'animatedVectorDrawable' : "androidx.vectordrawable:vectordrawable-animated:${versions.vectorDrawable}", 'browser' : "androidx.browser:browser:1.0.0", - 'transition' : "androidx.transition:transition:1.2.0", + 'transition' : "androidx.transition:transition:1.3.1", // others 'multidex' : "androidx.multidex:multidex:${versions.multidexLib}", 'mediaSession' : "androidx.media2:media2-session:${versions.media2}", @@ -159,16 +159,22 @@ ext { ], 'lifecycle': [ 'runtime' : "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}", + 'runtimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:${versions.lifecycle}", 'compiler' : "androidx.lifecycle:lifecycle-compiler:${versions.lifecycle}", 'commonJava8' : "androidx.lifecycle:lifecycle-common-java8:${versions.lifecycle}", 'extensions' : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}", 'reactiveStreams' : "androidx.lifecycle:lifecycle-reactivestreams:${versions.lifecycle}", + 'liveData' : "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}", + 'liveDataKtx' : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}", + 'viewModel' : "androidx.lifecycle:lifecycle-viewmodel:${versions.lifecycle}", + 'viewModelKtx' : "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.lifecycle}", ], 'room': [ 'runtime' : "androidx.room:room-runtime:${versions.room}", 'compiler' : "androidx.room:room-compiler:${versions.room}", 'rxjava' : "androidx.room:room-rxjava2:${versions.room}", 'testing' : "androidx.room:room-testing:${versions.room}", + 'ktx' : "androidx.room:room-ktx:${versions.room}", ], 'sqlite': [ 'sqlite' : "androidx.sqlite:sqlite:${versions.sqlite}", From 020c951cefd9fd8f5c1ddd1a1fd3fc7591cf6f19 Mon Sep 17 00:00:00 2001 From: Yongce Tu Date: Sun, 15 Mar 2020 17:11:14 +0800 Subject: [PATCH 023/100] Use RecyclerView to replace ListView & GridView --- .../android/lib/common/apps/AppsLoader.kt | 4 +- uiLib/build.gradle | 4 + .../commonui/activity/GridEntriesActivity.kt | 111 +++++++++++------- .../lib/commonui/base/ListAdapterBase.kt | 74 ------------ .../lib/commonui/base/ViewHolderBase.kt | 5 - .../recyclerview/MarginItemDecoration.kt | 32 +++++ .../res/drawable/commonui_scrollbar_thumb.xml | 4 + .../commonui_scrollbar_thumb_normal.xml | 6 + .../res/drawable/commonui_scrollbar_track.xml | 4 + .../commonui_scrollbar_track_normal.xml | 5 + .../main/res/layout/commonui_grid_entries.xml | 37 +++++- uiLib/src/main/res/values/commonui_attrs.xml | 4 +- uiLib/src/main/res/values/commonui_dimens.xml | 9 +- uiLib/src/main/res/values/commonui_styles.xml | 39 ++---- uiLib/src/main/res/values/commonui_themes.xml | 8 +- uiLib/src/main/res/values/publics.xml | 1 - 16 files changed, 179 insertions(+), 168 deletions(-) delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt delete mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt create mode 100644 uiLib/src/main/java/me/ycdev/android/lib/commonui/recyclerview/MarginItemDecoration.kt create mode 100644 uiLib/src/main/res/drawable/commonui_scrollbar_thumb.xml create mode 100644 uiLib/src/main/res/drawable/commonui_scrollbar_thumb_normal.xml create mode 100644 uiLib/src/main/res/drawable/commonui_scrollbar_track.xml create mode 100644 uiLib/src/main/res/drawable/commonui_scrollbar_track_normal.xml diff --git a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt index 7d9de81..e7e764b 100644 --- a/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt +++ b/baseLib/src/main/java/me/ycdev/android/lib/common/apps/AppsLoader.kt @@ -40,7 +40,7 @@ class AppsLoader private constructor(cxt: Context) { if (listener != null) { i++ - val percent = MiscUtils.calcProgressPercent(1, 50, i, n) + val percent = MiscUtils.calcProgressPercent(1, 90, i, n) listener.onProgressUpdated(percent, item) } } @@ -67,7 +67,7 @@ class AppsLoader private constructor(cxt: Context) { } if (listener != null && item != null) { i++ - val percent = MiscUtils.calcProgressPercent(51, 100, i, n) + val percent = MiscUtils.calcProgressPercent(91, 100, i, n) listener.onProgressUpdated(percent, item) } } diff --git a/uiLib/build.gradle b/uiLib/build.gradle index fc36f39..e4928d8 100644 --- a/uiLib/build.gradle +++ b/uiLib/build.gradle @@ -10,6 +10,8 @@ android { minSdkVersion versions.minSdk } + viewBinding.enabled = true + lintOptions { disable 'UnusedResources' } @@ -21,6 +23,8 @@ dependencies { implementation deps.kotlin.stdlib implementation deps.androidx.appcompat + implementation deps.androidx.material + implementation deps.androidx.recyclerview } project.ext { diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt index 40f4163..db3e9a8 100644 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/activity/GridEntriesActivity.kt @@ -5,12 +5,15 @@ import android.content.Context import android.content.Intent import android.os.AsyncTask import android.os.Bundle +import android.view.LayoutInflater import android.view.View -import android.widget.AdapterView -import android.widget.GridView -import android.widget.TextView +import android.view.ViewGroup +import android.widget.ProgressBar import android.widget.Toast import androidx.annotation.LayoutRes +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import me.ycdev.android.arch.ArchConstants import me.ycdev.android.arch.ArchConstants.IntentType import me.ycdev.android.arch.activity.AppCompatBaseActivity @@ -18,15 +21,15 @@ import me.ycdev.android.arch.wrapper.ToastHelper import me.ycdev.android.lib.common.utils.IntentUtils import me.ycdev.android.lib.common.wrapper.BroadcastHelper import me.ycdev.android.lib.commonui.R -import me.ycdev.android.lib.commonui.base.ListAdapterBase -import me.ycdev.android.lib.commonui.base.ViewHolderBase +import me.ycdev.android.lib.commonui.databinding.CommonuiGridEntriesItemBinding +import me.ycdev.android.lib.commonui.recyclerview.MarginItemDecoration @Suppress("MemberVisibilityCanBePrivate", "unused") -abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItemClickListener, - AdapterView.OnItemLongClickListener { +abstract class GridEntriesActivity : AppCompatBaseActivity() { - protected lateinit var adapter: SystemEntriesAdapter - protected lateinit var gridView: GridView + protected lateinit var entriesAdapter: SystemEntriesAdapter + protected lateinit var gridView: RecyclerView + protected lateinit var loadingView: ProgressBar protected open val contentViewLayout: Int @LayoutRes get() = R.layout.commonui_grid_entries @@ -53,7 +56,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem override val clickAction: ((Context) -> Unit)? = ::onItemClicked override val longClickAction: ((Context) -> Unit)? = ::onItemLongClicked - fun onItemClicked(context: Context) { + protected open fun onItemClicked(context: Context) { if (type == ArchConstants.INTENT_TYPE_ACTIVITY) { IntentUtils.startActivity(context, intent) } else if (type == ArchConstants.INTENT_TYPE_BROADCAST) { @@ -61,7 +64,7 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem } } - private fun onItemLongClicked(context: Context) { + protected open fun onItemLongClicked(context: Context) { ToastHelper.show(context, desc, Toast.LENGTH_LONG) } } @@ -70,71 +73,91 @@ abstract class GridEntriesActivity : AppCompatBaseActivity(), AdapterView.OnItem super.onCreate(savedInstanceState) setContentView(contentViewLayout) - adapter = SystemEntriesAdapter(this) + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + entriesAdapter = SystemEntriesAdapter(this) gridView = findViewById(R.id.grid) - gridView.adapter = adapter - gridView.onItemClickListener = this - gridView.onItemLongClickListener = this + loadingView = findViewById(R.id.progress) + + gridView.apply { + adapter = entriesAdapter + layoutManager = GridLayoutManager(this@GridEntriesActivity, 3) + addItemDecoration(MarginItemDecoration.create(getGridEntriesMargin())) + } loadItems() } + open fun getGridEntriesMargin(): Int { + val a = obtainStyledAttributes(intArrayOf(R.attr.commonuiGridEntriesItemMargin)) + val margin: Int = a.getDimensionPixelSize(0, 0) + a.recycle() + return margin + } + @SuppressLint("StaticFieldLeak") - private fun loadItems() { + protected open fun loadItems() { if (needLoadIntentsAsync) { + loadingView.visibility = View.VISIBLE + object : AsyncTask>() { override fun doInBackground(vararg params: Void): List { return intents } override fun onPostExecute(result: List) { - adapter.setData(intents) + loadingView.visibility = View.GONE + entriesAdapter.data = intents + entriesAdapter.notifyDataSetChanged() } }.execute() } else { - adapter.setData(intents) + loadingView.visibility = View.GONE + entriesAdapter.data = intents } } - override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { - val item = adapter.getItem(position) - item.clickAction?.invoke(this) - } - - override fun onItemLongClick( - parent: AdapterView<*>, - view: View, - position: Int, - id: Long - ): Boolean { - val item = adapter.getItem(position) - item.longClickAction?.invoke(this) - return true - } - /** * Decide if we need to invoke [.getIntent] async. * @return true for async and false for sync. false by default */ protected open val needLoadIntentsAsync: Boolean = false - protected open class SystemEntriesAdapter(cxt: Context) : - ListAdapterBase(cxt) { + protected open class SystemEntriesAdapter(val context: Context) : + RecyclerView.Adapter() { + + var data: List? = null + + private fun getItem(position: Int): Entry { + return data!![position] + } - override val itemLayoutResId: Int = R.layout.commonui_grid_entries_item + override fun getItemCount(): Int { + return data?.size ?: 0 + } - override fun createViewHolder(itemView: View, position: Int): ViewHolder { - return ViewHolder(itemView, position) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val itemView = LayoutInflater.from(context) + .inflate(R.layout.commonui_grid_entries_item, parent, false) + return ViewHolder(itemView) } - override fun bindView(item: Entry, holder: ViewHolder) { - holder.titleView.text = item.title + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + holder.binding.title.text = item.title + + holder.binding.root.setOnClickListener { item.clickAction?.invoke(context) } + holder.binding.root.setOnLongClickListener { + item.longClickAction?.invoke(context) + return@setOnLongClickListener true + } } - class ViewHolder(itemView: View, position: Int) : - ViewHolderBase(itemView, position) { - var titleView: TextView = itemView.findViewById(R.id.title) + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val binding: CommonuiGridEntriesItemBinding = CommonuiGridEntriesItemBinding.bind(itemView) } } } diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt deleted file mode 100644 index e918c8f..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ListAdapterBase.kt +++ /dev/null @@ -1,74 +0,0 @@ -package me.ycdev.android.lib.commonui.base - -import java.util.Collections -import java.util.Comparator - -import android.app.Activity -import android.content.Context -import androidx.annotation.LayoutRes -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter - -@Suppress("unused", "MemberVisibilityCanBePrivate") -abstract class ListAdapterBase(protected val context: Context) : - BaseAdapter() { - protected var inflater: LayoutInflater = if (context is Activity) { - context.layoutInflater - } else { - LayoutInflater.from(context) - } - protected var list: List? = null - - @get:LayoutRes - protected abstract val itemLayoutResId: Int - - /** - * @return null will be returned if no data set. - */ - fun getData(): List? { - return list - } - - open fun setData(data: List?) { - list = data - notifyDataSetChanged() - } - - fun sort(comparator: Comparator) { - Collections.sort(list!!, comparator) - notifyDataSetChanged() - } - - override fun getCount(): Int { - return if (list != null) list!!.size else 0 - } - - override fun getItem(position: Int): ItemType { - return list!![position] - } - - override fun getItemId(position: Int): Long { - return position.toLong() - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var itemView = convertView - val holder: VH - if (itemView == null) { - itemView = inflater.inflate(itemLayoutResId, parent, false) - holder = createViewHolder(itemView!!, position) - itemView.tag = holder - } else { - @Suppress("UNCHECKED_CAST") - val tmp = itemView.tag as VH - holder = tmp - } - bindView(getItem(position), holder) - return itemView - } - - protected abstract fun createViewHolder(itemView: View, position: Int): VH - protected abstract fun bindView(item: ItemType, holder: VH) -} diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt deleted file mode 100644 index bde125c..0000000 --- a/uiLib/src/main/java/me/ycdev/android/lib/commonui/base/ViewHolderBase.kt +++ /dev/null @@ -1,5 +0,0 @@ -package me.ycdev.android.lib.commonui.base - -import android.view.View - -open class ViewHolderBase(var itemView: View, var position: Int) diff --git a/uiLib/src/main/java/me/ycdev/android/lib/commonui/recyclerview/MarginItemDecoration.kt b/uiLib/src/main/java/me/ycdev/android/lib/commonui/recyclerview/MarginItemDecoration.kt new file mode 100644 index 0000000..22dd934 --- /dev/null +++ b/uiLib/src/main/java/me/ycdev/android/lib/commonui/recyclerview/MarginItemDecoration.kt @@ -0,0 +1,32 @@ +package me.ycdev.android.lib.commonui.recyclerview + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class MarginItemDecoration( + private val marginLeft: Int, + private val marginTop: Int, + private val marginRight: Int, + private val marginBottom: Int +) : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + outRect.apply { + left = marginLeft + top = marginTop + right = marginRight + bottom = marginBottom + } + } + + companion object { + fun create(margin: Int): MarginItemDecoration { + return MarginItemDecoration(margin, margin, margin, margin) + } + } +} diff --git a/uiLib/src/main/res/drawable/commonui_scrollbar_thumb.xml b/uiLib/src/main/res/drawable/commonui_scrollbar_thumb.xml new file mode 100644 index 0000000..24694e8 --- /dev/null +++ b/uiLib/src/main/res/drawable/commonui_scrollbar_thumb.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/uiLib/src/main/res/drawable/commonui_scrollbar_thumb_normal.xml b/uiLib/src/main/res/drawable/commonui_scrollbar_thumb_normal.xml new file mode 100644 index 0000000..43a3951 --- /dev/null +++ b/uiLib/src/main/res/drawable/commonui_scrollbar_thumb_normal.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/uiLib/src/main/res/drawable/commonui_scrollbar_track.xml b/uiLib/src/main/res/drawable/commonui_scrollbar_track.xml new file mode 100644 index 0000000..cb1677e --- /dev/null +++ b/uiLib/src/main/res/drawable/commonui_scrollbar_track.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/uiLib/src/main/res/drawable/commonui_scrollbar_track_normal.xml b/uiLib/src/main/res/drawable/commonui_scrollbar_track_normal.xml new file mode 100644 index 0000000..7c60578 --- /dev/null +++ b/uiLib/src/main/res/drawable/commonui_scrollbar_track_normal.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/uiLib/src/main/res/layout/commonui_grid_entries.xml b/uiLib/src/main/res/layout/commonui_grid_entries.xml index 53d68c4..6778431 100644 --- a/uiLib/src/main/res/layout/commonui_grid_entries.xml +++ b/uiLib/src/main/res/layout/commonui_grid_entries.xml @@ -1,6 +1,35 @@ - + android:layout_height="match_parent" + tools:context=".activity.GridEntriesActivity"> + + + + + + + + + + + diff --git a/uiLib/src/main/res/values/commonui_attrs.xml b/uiLib/src/main/res/values/commonui_attrs.xml index 78b1fdb..b532504 100644 --- a/uiLib/src/main/res/values/commonui_attrs.xml +++ b/uiLib/src/main/res/values/commonui_attrs.xml @@ -7,11 +7,9 @@ - - - + diff --git a/uiLib/src/main/res/values/commonui_dimens.xml b/uiLib/src/main/res/values/commonui_dimens.xml index 9adc7d3..8bc166a 100644 --- a/uiLib/src/main/res/values/commonui_dimens.xml +++ b/uiLib/src/main/res/values/commonui_dimens.xml @@ -1,13 +1,14 @@ - 16dp - 16dp + 10dp + 10dp + + 80dp 6dp - 4dp - 100dp 50dp + 3dp 6dp \ No newline at end of file diff --git a/uiLib/src/main/res/values/commonui_styles.xml b/uiLib/src/main/res/values/commonui_styles.xml index 7a5b53f..7aaa435 100644 --- a/uiLib/src/main/res/values/commonui_styles.xml +++ b/uiLib/src/main/res/values/commonui_styles.xml @@ -12,8 +12,8 @@ @@ -23,21 +23,6 @@ false - - - - - - - - - \ No newline at end of file diff --git a/uiLib/src/main/res/values/commonui_themes.xml b/uiLib/src/main/res/values/commonui_themes.xml index bd16bcc..a1bc41e 100644 --- a/uiLib/src/main/res/values/commonui_themes.xml +++ b/uiLib/src/main/res/values/commonui_themes.xml @@ -7,15 +7,17 @@ @dimen/commonui_divider_margin @color/commonui_divider_color - @style/Widget.CommonUiTheme.GridEntries @style/Widget.CommonUiTheme.GridEntriesItem - @dimen/commonui_grid_entries_spacing - @dimen/commonui_grid_entries_item_width @dimen/commonui_grid_entries_item_height + @dimen/commonui_grid_entries_item_margin @drawable/commonui_grid_entries_item_bkg @android:style/TextAppearance.Small @dimen/commonui_list_view_item_vertical_margin + + - - - - - - - - - - - - - \ No newline at end of file diff --git a/uiLib/src/main/res/values/commonui_themes.xml b/uiLib/src/main/res/values/commonui_themes.xml deleted file mode 100644 index a1bc41e..0000000 --- a/uiLib/src/main/res/values/commonui_themes.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/uiLib/src/main/res/values/ycdev_themes.xml b/uiLib/src/main/res/values/ycdev_themes.xml new file mode 100644 index 0000000..5393f73 --- /dev/null +++ b/uiLib/src/main/res/values/ycdev_themes.xml @@ -0,0 +1,23 @@ + + + + +