pala 2 年之前
當前提交
40e79dcfc4
共有 100 個文件被更改,包括 4782 次插入0 次删除
  1. 16 0
      .gitignore
  2. 1 0
      app/.gitignore
  3. 46 0
      app/build.gradle
  4. 21 0
      app/proguard-rules.pro
  5. 16 0
      app/src/main/AndroidManifest.xml
  6. 30 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  7. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  8. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  9. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  10. 二進制
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  11. 二進制
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  12. 二進制
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  13. 二進制
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  14. 二進制
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  15. 二進制
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  16. 二進制
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  17. 二進制
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  18. 二進制
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  19. 二進制
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  20. 16 0
      app/src/main/res/values-night/themes.xml
  21. 10 0
      app/src/main/res/values/colors.xml
  22. 3 0
      app/src/main/res/values/strings.xml
  23. 16 0
      app/src/main/res/values/themes.xml
  24. 13 0
      app/src/main/res/xml/backup_rules.xml
  25. 19 0
      app/src/main/res/xml/data_extraction_rules.xml
  26. 6 0
      build.gradle
  27. 1 0
      common/.gitignore
  28. 33 0
      common/build.gradle
  29. 4 0
      common/config.gradle
  30. 23 0
      common/consumer-rules.pro
  31. 21 0
      common/proguard-rules.pro
  32. 55 0
      common/publish.gradle
  33. 10 0
      common/src/main/AndroidManifest.xml
  34. 19 0
      common/src/main/java/com/atmob/common/AtmobCommon.java
  35. 19 0
      common/src/main/java/com/atmob/common/data/KVClient.java
  36. 297 0
      common/src/main/java/com/atmob/common/data/KVUtils.java
  37. 129 0
      common/src/main/java/com/atmob/common/device/AppSignUtil.java
  38. 13 0
      common/src/main/java/com/atmob/common/device/Constants.java
  39. 219 0
      common/src/main/java/com/atmob/common/device/DeviceInfoUtil.java
  40. 105 0
      common/src/main/java/com/atmob/common/device/IPUtil.java
  41. 132 0
      common/src/main/java/com/atmob/common/device/MacUtils.java
  42. 155 0
      common/src/main/java/com/atmob/common/logging/AtmobLog.java
  43. 25 0
      common/src/main/java/com/atmob/common/permission/PermissionUtil.java
  44. 96 0
      common/src/main/java/com/atmob/common/runtime/ActivityUtil.java
  45. 76 0
      common/src/main/java/com/atmob/common/runtime/ContextUtil.java
  46. 70 0
      common/src/main/java/com/atmob/common/runtime/ProcessUtil.java
  47. 48 0
      common/src/main/java/com/atmob/common/text/TextProguard.java
  48. 96 0
      common/src/main/java/com/atmob/common/text/TextUtil.java
  49. 58 0
      common/src/main/java/com/atmob/common/thread/ThreadPoolUtil.java
  50. 62 0
      common/src/main/java/com/atmob/common/ui/SizeUtil.java
  51. 22 0
      gradle.properties
  52. 二進制
      gradle/wrapper/gradle-wrapper.jar
  53. 6 0
      gradle/wrapper/gradle-wrapper.properties
  54. 185 0
      gradlew
  55. 89 0
      gradlew.bat
  56. 1 0
      network/.gitignore
  57. 53 0
      network/build.gradle
  58. 4 0
      network/config.gradle
  59. 14 0
      network/consumer-rules.pro
  60. 21 0
      network/proguard-rules.pro
  61. 59 0
      network/publish.gradle
  62. 5 0
      network/src/main/AndroidManifest.xml
  63. 193 0
      network/src/main/java/com/atmob/network/okhttp/AtmobOkHttpClient.java
  64. 246 0
      network/src/main/java/com/atmob/network/okhttp/logging/HttpLoggingInterceptor.java
  65. 25 0
      network/src/main/java/com/atmob/network/okhttp/logging/I.java
  66. 40 0
      network/src/main/java/com/atmob/network/okhttp/logging/Level.java
  67. 13 0
      network/src/main/java/com/atmob/network/okhttp/logging/Logger.java
  68. 234 0
      network/src/main/java/com/atmob/network/okhttp/logging/Printer.java
  69. 1 0
      room-rx/.gitignore
  70. 22 0
      room-rx/build.gradle
  71. 2 0
      room-rx/consumer-rules.pro
  72. 23 0
      room-rx/proguard-rules.pro
  73. 58 0
      room-rx/publish.gradle
  74. 5 0
      room-rx/src/main/AndroidManifest.xml
  75. 39 0
      room-rx/src/main/java/atmob/room/rxjava3/EmptyResultSetException.java
  76. 193 0
      room-rx/src/main/java/atmob/room/rxjava3/RxRoom.java
  77. 1 0
      rxjava/.gitignore
  78. 32 0
      rxjava/build.gradle
  79. 4 0
      rxjava/config.gradle
  80. 2 0
      rxjava/consumer-rules.pro
  81. 23 0
      rxjava/proguard-rules.pro
  82. 59 0
      rxjava/publish.gradle
  83. 5 0
      rxjava/src/main/AndroidManifest.xml
  84. 221 0
      rxjava/src/main/java/atmob/rxjava/utils/RxJavaUtil.java
  85. 65 0
      rxjava/src/main/java/atmob/rxjava/utils/ViewClickObservable.java
  86. 79 0
      rxjava/src/main/java/atmob/rxjava/utils/ViewLongClickObservable.java
  87. 31 0
      settings.gradle
  88. 1 0
      user/.gitignore
  89. 32 0
      user/build.gradle
  90. 4 0
      user/config.gradle
  91. 0 0
      user/consumer-rules.pro
  92. 22 0
      user/proguard-rules.pro
  93. 53 0
      user/publish.gradle
  94. 5 0
      user/src/main/AndroidManifest.xml
  95. 126 0
      user/src/main/java/com/atmob/user/AtmobUser.java
  96. 111 0
      user/src/main/java/com/atmob/user/param/AtmobParams.java
  97. 80 0
      user/src/main/java/com/atmob/user/sm/SmAntiFraudHelper.java
  98. 76 0
      user/src/main/java/com/atmob/user/strategy/ChinaStrategy.java
  99. 38 0
      user/src/main/java/com/atmob/user/strategy/ComplianceStrategy.java
  100. 0 0
      user/src/main/java/com/atmob/user/strategy/GlobalStrategy.java

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/.idea
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 46 - 0
app/build.gradle

@@ -0,0 +1,46 @@
+plugins {
+    id 'com.android.application'
+}
+
+android {
+    namespace 'plus.common'
+    compileSdk 32
+
+    defaultConfig {
+        applicationId "plus.common"
+        minSdk 21
+        targetSdk 32
+        versionCode 1
+        versionName "1.0"
+
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+configurations.all {
+    resolutionStrategy {
+        // don't cache changing modules at all
+        cacheChangingModulesFor 10, 'seconds'
+    }
+}
+
+dependencies {
+
+    implementation 'androidx.appcompat:appcompat:1.4.1'
+    implementation 'com.google.android.material:material:1.5.0'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+//    implementation("com.squareup.okhttp3:okhttp:4.10.0")
+}

+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 16 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.Xmcommon"
+        tools:targetApi="31" />
+
+</manifest>

File diff suppressed because it is too large
+ 30 - 0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml


+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

二進制
app/src/main/res/mipmap-hdpi/ic_launcher.webp


二進制
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


二進制
app/src/main/res/mipmap-mdpi/ic_launcher.webp


二進制
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


二進制
app/src/main/res/mipmap-xhdpi/ic_launcher.webp


二進制
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


二進制
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


二進制
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


二進制
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


二進制
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 16 - 0
app/src/main/res/values-night/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.Xmcommon" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_200</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/black</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_200</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 10 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">xm-common</string>
+</resources>

+ 16 - 0
app/src/main/res/values/themes.xml

@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Theme.Xmcommon" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/purple_500</item>
+        <item name="colorPrimaryVariant">@color/purple_700</item>
+        <item name="colorOnPrimary">@color/white</item>
+        <!-- Secondary brand color. -->
+        <item name="colorSecondary">@color/teal_200</item>
+        <item name="colorSecondaryVariant">@color/teal_700</item>
+        <item name="colorOnSecondary">@color/black</item>
+        <!-- Status bar color. -->
+        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
+        <!-- Customize your theme here. -->
+    </style>
+</resources>

+ 13 - 0
app/src/main/res/xml/backup_rules.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample backup rules file; uncomment and customize as necessary.
+   See https://developer.android.com/guide/topics/data/autobackup
+   for details.
+   Note: This file is ignored for devices older that API 31
+   See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+    <!--
+   <include domain="sharedpref" path="."/>
+   <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>

+ 19 - 0
app/src/main/res/xml/data_extraction_rules.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample data extraction rules file; uncomment and customize as necessary.
+   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+   for details.
+-->
+<data-extraction-rules>
+    <cloud-backup>
+        <!-- TODO: Use <include> and <exclude> to control what is backed up.
+        <include .../>
+        <exclude .../>
+        -->
+    </cloud-backup>
+    <!--
+    <device-transfer>
+        <include .../>
+        <exclude .../>
+    </device-transfer>
+    -->
+</data-extraction-rules>

+ 6 - 0
build.gradle

@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+    id 'com.android.application' version '7.3.1' apply false
+    id 'com.android.library' version '7.3.1' apply false
+    id "org.jetbrains.kotlin.jvm" version "1.8.0" apply false
+}

+ 1 - 0
common/.gitignore

@@ -0,0 +1 @@
+/build

+ 33 - 0
common/build.gradle

@@ -0,0 +1,33 @@
+plugins {
+    id 'com.android.library'
+}
+apply from: 'publish.gradle'
+
+android {
+    compileSdk 32
+
+    defaultConfig {
+        minSdk 21
+        targetSdk 32
+
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    compileOnly 'androidx.annotation:annotation:+'
+    compileOnly "extension.oaid:oaid:1.0.23"
+    compileOnly 'com.google.android.gms:play-services-ads-identifier:+'
+    compileOnly "com.tencent:mmkv:+"
+}

+ 4 - 0
common/config.gradle

@@ -0,0 +1,4 @@
+ext {
+    atmob_common_version_code = 201
+    atmob_common_version_name = "2.0.2-SNAPSHOT"
+}

+ 23 - 0
common/consumer-rules.pro

@@ -0,0 +1,23 @@
+## MMKV
+-keep class com.tencent.mmkv.**{*;}
+
+## OAID
+-keep class com.bun.miitmdid.** {*;}
+-keep class com.bun.lib.**{*;}
+-keep class XI.CA.XI.**{*;}
+-keep class XI.K0.XI.**{*;}
+-keep class XI.XI.K0.**{*;}
+-keep class XI.vs.K0.**{*;}
+-keep class XI.xo.XI.XI.**{*;}
+-keep class com.asus.msa.SupplementaryDID.**{*;}
+-keep class com.asus.msa.sdid.**{*;}
+-keep class com.bun.lib.**{*;}
+-keep class com.bun.miitmdid.**{*;}
+-keep class com.huawei.hms.ads.identifier.**{*;}
+-keep class com.samsung.android.deviceidservice.**{*;}
+-keep class org.json.**{*;}
+-keep public class com.netease.nis.sdkwrapper.Utils {
+public <methods>;
+}
+
+-dontshrink

+ 21 - 0
common/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 55 - 0
common/publish.gradle

@@ -0,0 +1,55 @@
+apply plugin: 'maven-publish'
+apply from: 'config.gradle'
+
+task androidJavadocs(type: Javadoc) {
+    source = android.sourceSets.main.java.sourceFiles
+    ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar"
+    classpath += files(ext.androidJar)
+}
+
+task androidJavadocsJar(type: Jar) {
+    getArchiveClassifier().set("javadoc")
+    from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+    getArchiveClassifier().set("sources")
+    from android.sourceSets.main.java.srcDirs
+}
+
+String GROUP_ID = "extra.common"
+String ARTIFACT_ID = "core"
+String ver = "$ext.atmob_common_version_name"
+
+String publishUrl = !ver.endsWith("-SNAPSHOT") ? "$atmob_maven_url/repository/android-release/"
+        : "$atmob_maven_url/repository/android-snapshot/"
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            push(MavenPublication) {
+                from components.release
+
+                groupId = GROUP_ID
+                artifactId = ARTIFACT_ID
+                version = ver
+
+                artifact androidSourcesJar
+                artifact androidJavadocsJar
+            }
+
+        }
+        repositories {
+            maven {
+                name = "nexus"
+                allowInsecureProtocol true
+                credentials {
+                    username = "$atmob_maven_username"
+                    password = "$atmob_maven_password"
+                }
+                url = publishUrl
+            }
+        }
+    }
+}

+ 10 - 0
common/src/main/AndroidManifest.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.atmob.common">
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
+        tools:ignore="ProtectedPermissions" />
+</manifest>

+ 19 - 0
common/src/main/java/com/atmob/common/AtmobCommon.java

@@ -0,0 +1,19 @@
+package com.atmob.common;
+
+import android.app.Application;
+
+import com.atmob.common.logging.AtmobLog;
+import com.atmob.common.runtime.ActivityUtil;
+import com.atmob.common.runtime.ContextUtil;
+
+public class AtmobCommon {
+    private AtmobCommon() {
+
+    }
+
+    public static void init(Application application, boolean logEnable) {
+        AtmobLog.logEnable(logEnable);
+        ContextUtil.init(application);
+        ActivityUtil.init(application);
+    }
+}

+ 19 - 0
common/src/main/java/com/atmob/common/data/KVClient.java

@@ -0,0 +1,19 @@
+package com.atmob.common.data;
+
+interface KVClient {
+    boolean getBoolean(String key, boolean defaultValue);
+
+    void putBoolean(String key, boolean value);
+
+    int getInt(String key, int defaultValue);
+
+    void putInt(String key, int value);
+
+    String getString(String key, String defaultValue);
+
+    void putString(String key, String value);
+
+    void putLong(String key, long value);
+
+    long getLong(String key, long defaultValue);
+}

+ 297 - 0
common/src/main/java/com/atmob/common/data/KVUtils.java

@@ -0,0 +1,297 @@
+package com.atmob.common.data;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.atmob.common.runtime.ContextUtil;
+import com.atmob.common.text.TextProguard;
+
+import java.lang.reflect.Method;
+
+public class KVUtils implements KVClient {
+
+    private static final boolean mmkvEnable;
+
+    private static KVUtils DEFAULT;
+
+    static {
+        mmkvEnable = MMKVClient.isMMKVEnable();
+    }
+
+    public static KVUtils getDefault() {
+        if (DEFAULT == null) {
+            synchronized (KVUtils.class) {
+                if (DEFAULT == null) {
+                    DEFAULT = new KVUtils();
+                }
+            }
+        }
+        return DEFAULT;
+    }
+
+    private KVClient getClient() {
+        if (mmkvEnable) {
+            return MMKVClient.getInstance();
+        }
+        return SPKVClient.getInstance();
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean defaultValue) {
+        return getClient().getBoolean(key, defaultValue);
+    }
+
+    @Override
+    public void putBoolean(String key, boolean value) {
+        getClient().putBoolean(key, value);
+    }
+
+    @Override
+    public int getInt(String key, int defaultValue) {
+        return getClient().getInt(key, defaultValue);
+    }
+
+    @Override
+    public void putInt(String key, int value) {
+        getClient().putInt(key, value);
+    }
+
+    @Override
+    public String getString(String key, String defaultValue) {
+        return getClient().getString(key, defaultValue);
+    }
+
+    @Override
+    public void putString(String key, String value) {
+        getClient().putString(key, value);
+    }
+
+    @Override
+    public void putLong(String key, long value) {
+        getClient().putLong(key, value);
+    }
+
+    @Override
+    public long getLong(String key, long defaultValue) {
+        return getClient().getLong(key, defaultValue);
+    }
+
+    private static class MMKVClient implements KVClient {
+
+        private static Class<?> mmkvClass;
+
+        private static MMKVClient INSTANCE;
+
+        public MMKVClient() {
+            if (isMMKVEnable()) {
+                init();
+            }
+        }
+
+        private static boolean isMMKVEnable() {
+            if (mmkvClass != null) {
+                return true;
+            }
+            try {
+                mmkvClass = Class.forName("com.tencent.mmkv.MMKV");
+                return true;
+            } catch (ClassNotFoundException e) {
+                return false;
+            }
+        }
+
+        private static void init() {
+            if (mmkvClass != null) {
+                try {
+                    Method initialize = mmkvClass.getMethod("initialize", Context.class);
+                    initialize.invoke(null, ContextUtil.getContext());
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        private Object getDefault() {
+            try {
+                Method defaultMMKV = mmkvClass.getMethod("defaultMMKV");
+                return defaultMMKV.invoke(null);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            return null;
+        }
+
+        public static KVClient getInstance() {
+            if (!isMMKVEnable()) {
+                return SPKVClient.getInstance();
+            }
+            if (INSTANCE == null) {
+                synchronized (MMKVClient.class) {
+                    if (INSTANCE == null) {
+                        INSTANCE = new MMKVClient();
+                    }
+                }
+            }
+            return INSTANCE;
+        }
+
+        @Override
+        public boolean getBoolean(String key, boolean defaultValue) {
+            try {
+                Method getBoolean = mmkvClass.getMethod("getBoolean", String.class, boolean.class);
+                return (boolean) getBoolean.invoke(getDefault(), key, defaultValue);
+            } catch (Exception e) {
+                e.printStackTrace();
+                return SPKVClient.getInstance().getBoolean(key, defaultValue);
+            }
+        }
+
+        @Override
+        public void putBoolean(String key, boolean value) {
+            try {
+                Method putBoolean = mmkvClass.getMethod("putBoolean", String.class, boolean.class);
+                putBoolean.invoke(getDefault(), key, value);
+            } catch (Exception e) {
+                e.printStackTrace();
+                SPKVClient.getInstance().putBoolean(key, value);
+            }
+        }
+
+        @Override
+        public int getInt(String key, int defaultValue) {
+            try {
+                Method getInt = mmkvClass.getMethod("getInt", String.class, int.class);
+                return (int) getInt.invoke(getDefault(), key, defaultValue);
+            } catch (Exception e) {
+                e.printStackTrace();
+                return SPKVClient.getInstance().getInt(key, defaultValue);
+            }
+        }
+
+        @Override
+        public void putInt(String key, int value) {
+            try {
+                Method putInt = mmkvClass.getMethod("putInt", String.class, int.class);
+                putInt.invoke(getDefault(), key, value);
+            } catch (Exception e) {
+                e.printStackTrace();
+                SPKVClient.getInstance().putInt(key, value);
+            }
+        }
+
+        @Override
+        public String getString(String key, String defaultValue) {
+            try {
+                Method getString = mmkvClass.getMethod("getString", String.class, String.class);
+                return (String) getString.invoke(getDefault(), key, defaultValue);
+            } catch (Exception e) {
+                e.printStackTrace();
+                return SPKVClient.getInstance().getString(key, defaultValue);
+            }
+        }
+
+        @Override
+        public void putString(String key, String value) {
+            try {
+                Method putString = mmkvClass.getMethod("putString", String.class, String.class);
+                putString.invoke(getDefault(), key, value);
+            } catch (Exception e) {
+                e.printStackTrace();
+                SPKVClient.getInstance().getString(key, value);
+            }
+        }
+
+        @Override
+        public void putLong(String key, long value) {
+            try {
+                Method putLong = mmkvClass.getMethod("putLong", String.class, long.class);
+                putLong.invoke(getDefault(), key, value);
+            } catch (Exception e) {
+                e.printStackTrace();
+                SPKVClient.getInstance().putLong(key, value);
+            }
+        }
+
+        @Override
+        public long getLong(String key, long defaultValue) {
+            try {
+                Method getLong = mmkvClass.getMethod("getLong", String.class, long.class);
+                return (long) getLong.invoke(getDefault(), key, defaultValue);
+            } catch (Exception e) {
+                e.printStackTrace();
+                return SPKVClient.getInstance().getLong(key, defaultValue);
+            }
+        }
+    }
+
+    private static class SPKVClient implements KVClient {
+
+        private static final String NAMESPACE = TextProguard.encode("atmob_default_sp");
+
+        private static SPKVClient INSTANCE;
+
+        private SharedPreferences defaultSP;
+
+        private SPKVClient() {
+        }
+
+        public static KVClient getInstance() {
+            if (INSTANCE == null) {
+                synchronized (SPKVClient.class) {
+                    if (INSTANCE == null) {
+                        INSTANCE = new SPKVClient();
+                    }
+                }
+            }
+            return INSTANCE;
+        }
+
+        private SharedPreferences getDefault() {
+            if (defaultSP == null) {
+                Context context = ContextUtil.getContext();
+                defaultSP = context.getSharedPreferences(NAMESPACE, Context.MODE_PRIVATE);
+            }
+            return defaultSP;
+        }
+
+        @Override
+        public boolean getBoolean(String key, boolean defaultValue) {
+            return getDefault().getBoolean(key, defaultValue);
+        }
+
+        @Override
+        public void putBoolean(String key, boolean value) {
+            getDefault().edit().putBoolean(key, value).apply();
+        }
+
+        @Override
+        public int getInt(String key, int defaultValue) {
+            return getDefault().getInt(key, defaultValue);
+        }
+
+        @Override
+        public void putInt(String key, int value) {
+            getDefault().edit().putInt(key, value).apply();
+        }
+
+        @Override
+        public String getString(String key, String defaultValue) {
+            return getDefault().getString(key, defaultValue);
+        }
+
+        @Override
+        public void putString(String key, String value) {
+            getDefault().edit().putString(key, value).apply();
+        }
+
+        @Override
+        public void putLong(String key, long value) {
+            getDefault().edit().putLong(key, value).apply();
+        }
+
+        @Override
+        public long getLong(String key, long defaultValue) {
+            return getDefault().getLong(key, defaultValue);
+        }
+    }
+}

+ 129 - 0
common/src/main/java/com/atmob/common/device/AppSignUtil.java

@@ -0,0 +1,129 @@
+package com.atmob.common.device;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class AppSignUtil {
+    private static final HashMap<String, ArrayList<String>> mSignMap = new HashMap<>();
+    public final static String MD5 = "MD5";
+    public final static String SHA1 = "SHA1";
+    public final static String SHA256 = "SHA256";
+
+    /**
+     * 返回一个签名的对应类型的字符串, 因为一个安装包可以被多个签名文件签名, 所以返回一个签名信息的list
+     */
+    public static ArrayList<String> getSignInfo(Context context, String type) {
+        if (context == null || type == null) {
+            return null;
+        }
+        String packageName = context.getPackageName();
+        if (packageName == null) {
+            return null;
+        }
+        if (mSignMap.get(type) != null) {
+            return mSignMap.get(type);
+        }
+        ArrayList<String> mList = new ArrayList<>();
+        try {
+            Signature[] signs = getSignatures(context, packageName);
+            if (signs == null) {
+                return null;
+            }
+            for (Signature sig : signs) {
+                String tmp = "error!";
+                switch (type) {
+                    case MD5:
+                        tmp = getSignatureByteString(sig, MD5);
+                        break;
+                    case SHA1:
+                        tmp = getSignatureByteString(sig, SHA1);
+                        break;
+                    case SHA256:
+                        tmp = getSignatureByteString(sig, SHA256);
+                        break;
+                }
+                mList.add(tmp);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        mSignMap.put(type, mList);
+        return mList;
+    }
+
+    /**
+     * 获取签名sha1值
+     */
+    public static String getSha1(Context context) {
+        String res = "";
+        ArrayList<String> mlist = getSignInfo(context, SHA1);
+        if (mlist != null && mlist.size() != 0) {
+            res = mlist.get(0);
+        }
+        return res;
+    }
+
+    /**
+     * 返回对应包的签名信息
+     */
+    @SuppressLint("PackageManagerGetSignatures")
+    private static Signature[] getSignatures(Context context, String packageName) {
+        PackageInfo packageInfo;
+        try {
+            packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+            return packageInfo.signatures;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 获取相应的类型的字符串(把签名的byte[]信息转换成16进制)
+     */
+    private static String getSignatureString(Signature sig, String type) {
+        byte[] hexBytes = sig.toByteArray();
+        String fingerprint = "error!";
+        try {
+            MessageDigest digest = MessageDigest.getInstance(type);
+            byte[] digestBytes = digest.digest(hexBytes);
+            StringBuilder sb = new StringBuilder();
+            for (byte digestByte : digestBytes) {
+                sb.append((Integer.toHexString((digestByte & 0xFF) | 0x100)).substring(1, 3));
+            }
+            fingerprint = sb.toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return fingerprint;
+    }
+
+    /**
+     * 获取相应的类型的字符串(把签名的byte[]信息转换成 95:F4:D4:FG 这样的字符串形式)
+     */
+    private static String getSignatureByteString(Signature sig, String type) {
+        byte[] hexBytes = sig.toByteArray();
+        String fingerprint = "error!";
+        try {
+            MessageDigest digest = MessageDigest.getInstance(type);
+            byte[] digestBytes = digest.digest(hexBytes);
+            StringBuilder sb = new StringBuilder();
+            for (byte digestByte : digestBytes) {
+                sb.append(((Integer.toHexString((digestByte & 0xFF) | 0x100)).substring(1, 3)).toUpperCase());
+                sb.append(":");
+            }
+            fingerprint = sb.substring(0, sb.length() - 1).toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return fingerprint;
+    }
+}

+ 13 - 0
common/src/main/java/com/atmob/common/device/Constants.java

@@ -0,0 +1,13 @@
+package com.atmob.common.device;
+
+import com.atmob.common.text.TextProguard;
+
+abstract class Constants {
+    final static String PREFIX = TextProguard.encode("atmob_device_");
+
+    final static String KEY_OAID_CACHE = PREFIX + TextProguard.encode("oaid_cache");
+    final static String KEY_AAID_CACHE = PREFIX + TextProguard.encode("aaid_cache");
+    final static String KEY_IMEI_CACHE = PREFIX + TextProguard.encode("imei_cache");
+    final static String KEY_ANDROID_ID_CACHE = PREFIX + TextProguard.encode("android_id_cache");
+    final static String KEY_MAC_ADDRESS_CACHE = PREFIX + TextProguard.encode("mac_address_cache");
+}

+ 219 - 0
common/src/main/java/com/atmob/common/device/DeviceInfoUtil.java

@@ -0,0 +1,219 @@
+package com.atmob.common.device;
+
+import static com.atmob.common.runtime.ContextUtil.getContext;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.atmob.common.data.KVUtils;
+import com.atmob.common.permission.PermissionUtil;
+import com.atmob.common.thread.ThreadPoolUtil;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+
+public class DeviceInfoUtil {
+
+    private static final String SIM_STATE = "getSimState";
+    private static final String SIM_IMEI = "getImei";
+
+    private static final HashMap<Integer, String> imeiCache = new HashMap<>(2);
+
+    private static String imei;
+
+    private static volatile String oaid;
+
+    private static String androidId;
+
+    private static volatile String aaid;
+
+    private static boolean oaidEnable = true;
+
+    private static boolean aaidEnable = true;
+
+    static {
+        imei = KVUtils.getDefault().getString(Constants.KEY_IMEI_CACHE, null);
+        androidId = KVUtils.getDefault().getString(Constants.KEY_ANDROID_ID_CACHE, null);
+        oaid = KVUtils.getDefault().getString(Constants.KEY_OAID_CACHE, null);
+        aaid = KVUtils.getDefault().getString(Constants.KEY_AAID_CACHE, null);
+        oaid = getOaid();
+        aaid = getAaid();
+    }
+
+    private DeviceInfoUtil() {
+
+    }
+
+    @SuppressLint("HardwareIds")
+    public static String getImei() {
+        if (!TextUtils.isEmpty(imei)) {
+            return imei;
+        }
+        Context context = getContext();
+        TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        if (PermissionUtil.hasPermission(Manifest.permission.READ_PHONE_STATE)) {
+            try {
+                if (Build.VERSION.SDK_INT >= 26) {
+                    imei = telephonyManager.getImei();
+                } else {
+                    imei = telephonyManager.getDeviceId();
+                }
+            } catch (SecurityException e) {
+                e.printStackTrace();
+            }
+        }
+        KVUtils.getDefault().putString(Constants.KEY_IMEI_CACHE, imei);
+        return imei;
+    }
+
+    @SuppressLint("HardwareIds")
+    public static String getSimImei(int slotIdx) {
+        String imei = imeiCache.get(slotIdx);
+        if (!TextUtils.isEmpty(imei)) {
+            return imei;
+        }
+        String imeiCache = KVUtils.getDefault().getString(Constants.KEY_IMEI_CACHE + slotIdx, null);
+        if (!TextUtils.isEmpty(imeiCache)) {
+            DeviceInfoUtil.imeiCache.put(slotIdx, imeiCache);
+            return imeiCache;
+        }
+        Context context = getContext();
+        if (PermissionUtil.hasPermission(Manifest.permission.READ_PHONE_STATE)) {
+            try {
+                if (getSimStateBySlotIdx(context, slotIdx)) {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                        TelephonyManager telephony =
+                                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+                        imei = telephony.getImei(slotIdx);
+                    } else {
+                        Object getImei = getSimByMethod(SIM_IMEI, slotIdx);
+                        if (getImei != null) {
+                            imei = getImei.toString();
+                        }
+                    }
+                }
+            } catch (Exception ignore) {
+
+            }
+        }
+        if (TextUtils.isEmpty(imei)) {
+            return "";
+        }
+        DeviceInfoUtil.imeiCache.put(slotIdx, imei);
+        KVUtils.getDefault().putString(Constants.KEY_IMEI_CACHE + slotIdx, imei);
+        return imei;
+    }
+
+    private static boolean getSimStateBySlotIdx(Context context, int slotIdx) {
+        int simState = TelephonyManager.SIM_STATE_UNKNOWN;
+        if (Build.VERSION.SDK_INT >= 26) {
+            TelephonyManager telephony =
+                    (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            simState = telephony.getSimState(slotIdx);
+        } else {
+            Object getSimState = getSimByMethod(SIM_STATE, slotIdx);
+            if (getSimState != null) {
+                try {
+                    simState = Integer.parseInt(getSimState.toString());
+                } catch (Exception ignored) {
+                }
+            }
+        }
+        return simState != TelephonyManager.SIM_STATE_UNKNOWN;
+    }
+
+    private static Object getSimByMethod(String method, int param) {
+        TelephonyManager telephony =
+                (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE);
+        try {
+            Class<?> telephonyClass = Class.forName(telephony.getClass().getName());
+            Class<?>[] clazz = new Class[]{Integer.class};
+
+            Method getSimState = telephonyClass.getMethod(method, clazz);
+            getSimState.setAccessible(true);
+
+            Object[] obParameter = new Object[]{param};
+            Object ob_phone = getSimState.invoke(telephony, obParameter);
+
+            if (ob_phone != null) {
+                return ob_phone;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    @SuppressLint("HardwareIds")
+    public static String getAndroidId() {
+        if (!TextUtils.isEmpty(androidId)) {
+            return androidId;
+        }
+        Context context = getContext();
+        androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+        KVUtils.getDefault().putString(Constants.KEY_ANDROID_ID_CACHE, androidId);
+        return androidId;
+    }
+
+    public static String getOaid() {
+        if (!TextUtils.isEmpty(oaid) || !oaidEnable) {
+            return oaid;
+        }
+        Context context = getContext();
+        try {
+            Class<?> mdidSdkHelper = Class.forName("com.bun.miitmdid.core.MdidSdkHelper");
+            Class<?> iIdentifierListener = Class.forName("com.bun.miitmdid.interfaces.IIdentifierListener");
+            Method initSdk = mdidSdkHelper.getMethod("InitSdk", Context.class, boolean.class, iIdentifierListener);
+            Object identifierListener = Proxy.newProxyInstance(mdidSdkHelper.getClassLoader(), new Class[]{iIdentifierListener}, (proxy, method, args) -> {
+                if ("OnSupport".equals(method.getName())) {
+                    if (args.length == 2) {
+                        Object idSupplierObj = args[1];
+                        if (idSupplierObj != null) {
+                            Class<?> idSupplier = Class.forName("com.bun.miitmdid.interfaces.IdSupplier");
+                            Method isSupported = idSupplier.getMethod("isSupported");
+                            boolean isSupportedResult = (boolean) isSupported.invoke(idSupplierObj);
+                            if (isSupportedResult) {
+                                Method getOAID = idSupplier.getMethod("getOAID");
+                                oaid = (String) getOAID.invoke(idSupplierObj);
+                                KVUtils.getDefault().putString(Constants.KEY_OAID_CACHE, oaid);
+                            }
+                        }
+                    }
+                }
+                return null;
+            });
+            initSdk.invoke(null, context, true, identifierListener);
+        } catch (Exception e) {
+            oaidEnable = false;
+        }
+        return oaid;
+    }
+
+    public static String getAaid() {
+        if (!TextUtils.isEmpty(aaid) || !aaidEnable) {
+            return aaid;
+        }
+        ThreadPoolUtil.getInstance().execute(() -> {
+            Context context = getContext();
+            try {
+                Class<?> adIdClientClass = Class.forName("com.google.android.gms.ads.identifier.AdvertisingIdClient");
+                Method getAdvertisingIdInfo = adIdClientClass.getMethod("getAdvertisingIdInfo", Context.class);
+                Object info = getAdvertisingIdInfo.invoke(null, context);
+                Class<?> infoClass = Class.forName("com.google.android.gms.ads.identifier.AdvertisingIdClient$Info");
+                Method getId = infoClass.getMethod("getId");
+                DeviceInfoUtil.aaid = (String) getId.invoke(info);
+            } catch (Exception ignore) {
+                aaidEnable = false;
+            }
+        });
+        return aaid;
+    }
+}

+ 105 - 0
common/src/main/java/com/atmob/common/device/IPUtil.java

@@ -0,0 +1,105 @@
+package com.atmob.common.device;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+
+import com.atmob.common.runtime.ContextUtil;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class IPUtil {
+
+    private IPUtil() {
+
+    }
+
+    //获取内网ip
+    public static String getIpV4() {
+        Context context = ContextUtil.getContext().getApplicationContext();
+        ConnectivityManager conMann = (ConnectivityManager)
+                context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo mobileNetworkInfo = conMann.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
+        NetworkInfo wifiNetworkInfo = conMann.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+        if (mobileNetworkInfo != null && mobileNetworkInfo.isConnected()) {
+            return getLocalIpAddress();
+        } else if (wifiNetworkInfo != null && wifiNetworkInfo.isConnected()) {
+            WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+            WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+            int ipAddress = wifiInfo.getIpAddress();
+            return intToIp(ipAddress);
+        }
+        return null;
+    }
+
+    //获取Wifi名称
+    public static String getWifiName() {
+        Context context = ContextUtil.getContext().getApplicationContext();
+        ConnectivityManager conMann = (ConnectivityManager)
+                context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo mobileNetworkInfo = conMann.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
+        NetworkInfo wifiNetworkInfo = conMann.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+        if (mobileNetworkInfo != null && mobileNetworkInfo.isConnected()) {
+            String wifiName = mobileNetworkInfo.getExtraInfo();
+            if (!TextUtils.isEmpty(wifiName)) {
+                if (wifiName.startsWith("\"")) {
+                    wifiName = wifiName.substring(1);
+                }
+                if (wifiName.endsWith("\"")) {
+                    wifiName = wifiName.substring(0, wifiName.length() - 1);
+                }
+                return wifiName;
+            }
+            return "";
+        } else if (wifiNetworkInfo != null && wifiNetworkInfo.isConnected()) {
+            WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+            WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+            String wifiName = wifiInfo.getSSID();
+            if (!TextUtils.isEmpty(wifiName)) {
+                if (wifiName.startsWith("\"")) {
+                    wifiName = wifiName.substring(1);
+                }
+                if (wifiName.endsWith("\"")) {
+                    wifiName = wifiName.substring(0, wifiName.length() - 1);
+                }
+                return wifiName;
+            }
+            return "";
+        }
+        return null;
+    }
+
+    private static String getLocalIpAddress() {
+        try {
+            String ipv4;
+            ArrayList<NetworkInterface> nilist = Collections.list(NetworkInterface.getNetworkInterfaces());
+            for (NetworkInterface ni : nilist) {
+                ArrayList<InetAddress> ialist = Collections.list(ni.getInetAddresses());
+                for (InetAddress address : ialist) {
+                    if (!address.isLoopbackAddress() && (address instanceof Inet4Address)) {
+                        ipv4 = address.getHostAddress();
+                        return ipv4;
+                    }
+                }
+            }
+        } catch (SocketException ignored) {
+
+        }
+        return null;
+    }
+
+    private static String intToIp(int ipInt) {
+        return (ipInt & 0xFF) + "." +
+                ((ipInt >> 8) & 0xFF) + "." +
+                ((ipInt >> 16) & 0xFF) + "." +
+                ((ipInt >> 24) & 0xFF);
+    }
+}

+ 132 - 0
common/src/main/java/com/atmob/common/device/MacUtils.java

@@ -0,0 +1,132 @@
+package com.atmob.common.device;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.text.TextUtils;
+
+import com.atmob.common.data.KVUtils;
+import com.atmob.common.runtime.ContextUtil;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.net.NetworkInterface;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public class MacUtils {
+
+    private static String macAddress;
+
+    static {
+        macAddress = KVUtils.getDefault().getString(Constants.KEY_MAC_ADDRESS_CACHE, null);
+    }
+
+    /**
+     * Android 6.0 之前(不包括6.0)获取mac地址
+     * 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
+     */
+    @SuppressLint("HardwareIds")
+    private static String getMacDefault() {
+        Context context = ContextUtil.getContext().getApplicationContext();
+        String mac = "";
+        if (context == null) {
+            return mac;
+        }
+        WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        WifiInfo info = null;
+        try {
+            info = wifi.getConnectionInfo();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        if (info == null) {
+            return null;
+        }
+        mac = info.getMacAddress();
+        if (!TextUtils.isEmpty(mac)) {
+            mac = mac.toUpperCase(Locale.ENGLISH);
+        }
+        return mac;
+    }
+
+    /**
+     * Android 6.0-Android 7.0 获取mac地址
+     */
+    private static String getMacAddress() {
+        String macSerial = null;
+        String str = "";
+
+        try {
+            Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address");
+            InputStreamReader ir = new InputStreamReader(pp.getInputStream());
+            LineNumberReader input = new LineNumberReader(ir);
+
+            while (null != str) {
+                str = input.readLine();
+                if (str != null) {
+                    macSerial = str.trim();//去空格
+                    break;
+                }
+            }
+        } catch (IOException ex) {
+            // 赋予默认值
+            ex.printStackTrace();
+        }
+
+        return macSerial;
+    }
+
+    /**
+     * Android 7.0之后获取Mac地址
+     * 遍历循环所有的网络接口,找到接口是 wlan0
+     * 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
+     */
+    private static String getMacFromHardware() {
+        try {
+            List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
+            for (NetworkInterface nif : all) {
+                if (!nif.getName().equals("wlan0"))
+                    continue;
+                byte[] macBytes = nif.getHardwareAddress();
+                if (macBytes == null) return "";
+                StringBuilder res1 = new StringBuilder();
+                for (Byte b : macBytes) {
+                    res1.append(String.format("%02X:", b));
+                }
+                if (!TextUtils.isEmpty(res1)) {
+                    res1.deleteCharAt(res1.toString().length() - 1);
+                }
+                return res1.toString();
+            }
+        } catch (Exception ignore) {
+
+        }
+        return "";
+    }
+
+    /**
+     * 获取mac地址(适配所有Android版本)
+     */
+    public static String getMac() {
+        if (!TextUtils.isEmpty(macAddress)) {
+            return macAddress;
+        }
+        String mac;
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            mac = getMacDefault();
+        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            mac = getMacAddress();
+        } else {
+            mac = getMacFromHardware();
+        }
+        macAddress = mac;
+        KVUtils.getDefault().putString(Constants.KEY_MAC_ADDRESS_CACHE, mac);
+        return mac;
+    }
+}

+ 155 - 0
common/src/main/java/com/atmob/common/logging/AtmobLog.java

@@ -0,0 +1,155 @@
+package com.atmob.common.logging;
+
+import android.util.Log;
+
+public class AtmobLog {
+    private static boolean LogEnable;
+
+    public static void v(String TAG, String format, Object... args) {
+        if (LogEnable) {
+            Log.v(TAG, String.format(format, args));
+        }
+    }
+
+    public static void v(String TAG, String format, Throwable tr, Object... args) {
+        if (LogEnable) {
+            Log.v(TAG, String.format(format, args), tr);
+        }
+    }
+
+    public static void v(String TAG, String msg) {
+        if (LogEnable) {
+            Log.v(TAG, msg);
+        }
+    }
+
+    public static void v(String TAG, String msg, Throwable tr) {
+        if (LogEnable) {
+            Log.v(TAG, msg, tr);
+        }
+    }
+
+    public static void i(String TAG, String format, Object... args) {
+        if (LogEnable) {
+            Log.i(TAG, String.format(format, args));
+        }
+    }
+
+    public static void i(String TAG, String format, Throwable tr, Object... args) {
+        if (LogEnable) {
+            Log.i(TAG, String.format(format, args), tr);
+        }
+    }
+
+    public static void i(String TAG, String msg) {
+        if (LogEnable) {
+            Log.i(TAG, msg);
+        }
+    }
+
+    public static void i(String TAG, String msg, Throwable tr) {
+        if (LogEnable) {
+            Log.i(TAG, msg, tr);
+        }
+    }
+
+    public static void d(String TAG, String format, Object... args) {
+        if (LogEnable) {
+            d(TAG, String.format(format, args));
+        }
+    }
+
+    public static void d(String TAG, String format, Throwable tr, Object... args) {
+        if (LogEnable) {
+            d(TAG, String.format(format, args), tr);
+        }
+    }
+
+    public static void d(String TAG, String msg) {
+        if (LogEnable) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    public static void d(String TAG, String msg, Throwable tr) {
+        if (LogEnable) {
+            Log.d(TAG, msg, tr);
+        }
+    }
+
+    public static void w(String TAG, String format, Object... args) {
+        if (LogEnable) {
+            Log.w(TAG, String.format(format, args));
+        }
+    }
+
+    public static void w(String TAG, String format, Throwable tr, Object... args) {
+        if (LogEnable) {
+            Log.w(TAG, String.format(format, args), tr);
+        }
+    }
+
+    public static void w(String TAG, String msg) {
+        if (LogEnable) {
+            Log.w(TAG, msg);
+        }
+    }
+
+    public static void w(String TAG, String msg, Throwable tr) {
+        if (LogEnable) {
+            Log.w(TAG, msg, tr);
+        }
+    }
+
+    public static void e(String TAG, String format, Object... args) {
+        if (LogEnable) {
+            Log.e(TAG, String.format(format, args));
+        }
+    }
+
+    public static void e(String TAG, String format, Throwable tr, Object... args) {
+        if (LogEnable) {
+            Log.e(TAG, String.format(format, args), tr);
+        }
+    }
+
+    public static void e(String TAG, String msg) {
+        if (LogEnable) {
+            Log.e(TAG, msg);
+        }
+    }
+
+    public static void e(String TAG, String msg, Throwable tr) {
+        if (LogEnable) {
+            Log.e(TAG, msg, tr);
+        }
+    }
+
+    public static void wtf(String TAG, String format, Object... args) {
+        if (LogEnable) {
+            Log.wtf(TAG, String.format(format, args));
+        }
+    }
+
+    public static void wtf(String TAG, String format, Throwable tr, Object... args) {
+        if (LogEnable) {
+            Log.wtf(TAG, String.format(format, args), tr);
+        }
+    }
+
+    public static void wtf(String TAG, String msg) {
+        if (LogEnable) {
+            Log.wtf(TAG, msg);
+        }
+    }
+
+    public static void wtf(String TAG, String msg, Throwable tr) {
+        if (LogEnable) {
+            Log.wtf(TAG, msg, tr);
+        }
+    }
+
+    public static void logEnable(boolean enable) {
+        LogEnable = enable;
+    }
+}

+ 25 - 0
common/src/main/java/com/atmob/common/permission/PermissionUtil.java

@@ -0,0 +1,25 @@
+package com.atmob.common.permission;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import com.atmob.common.runtime.ContextUtil;
+
+public class PermissionUtil {
+    private PermissionUtil() {
+        throw new RuntimeException("cannot be INSTANTIATED.");
+    }
+
+    public static boolean hasPermission(String... permissions) {
+        Context context = ContextUtil.getContext();
+        PackageManager packageManager = context.getPackageManager();
+        for (String permission : permissions) {
+            boolean hasPermission = packageManager.checkPermission(permission, context.getPackageName())
+                    == PackageManager.PERMISSION_GRANTED;
+            if (!hasPermission) {
+                return false;
+            }
+        }
+        return true;
+    }
+}

+ 96 - 0
common/src/main/java/com/atmob/common/runtime/ActivityUtil.java

@@ -0,0 +1,96 @@
+package com.atmob.common.runtime;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Stack;
+
+public class ActivityUtil {
+
+    private final Stack<Activity> activityStack;
+
+    private static class Holder {
+        private static final ActivityUtil INSTANCE = new ActivityUtil();
+    }
+
+    private ActivityUtil() {
+        activityStack = new Stack<>();
+    }
+
+    public static void init(Application application) {
+        Holder.INSTANCE.initInternal(application);
+    }
+
+    private void initInternal(Application application) {
+        application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
+            @Override
+            public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
+                activityStack.push(activity);
+            }
+
+            @Override
+            public void onActivityStarted(@NonNull Activity activity) {
+
+            }
+
+            @Override
+            public void onActivityResumed(@NonNull Activity activity) {
+                if (activityStack.size() > 0) {
+                    int location = activityStack.search(activity);
+                    if (location == -1) {
+                        activityStack.push(activity);
+                        return;
+                    }
+                    if (location != 1) {
+                        activityStack.remove(activity);
+                        activityStack.push(activity);
+                    }
+                }
+            }
+
+            @Override
+            public void onActivityPaused(@NonNull Activity activity) {
+
+            }
+
+            @Override
+            public void onActivityStopped(@NonNull Activity activity) {
+
+            }
+
+            @Override
+            public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
+
+            }
+
+            @Override
+            public void onActivityDestroyed(@NonNull Activity activity) {
+                activityStack.remove(activity);
+            }
+        });
+    }
+
+    public static Activity getTopActivity() {
+        return Holder.INSTANCE.activityStack.peek();
+    }
+
+    public static int getActivityCount() {
+        return Holder.INSTANCE.activityStack.size();
+    }
+
+    public static boolean isActivityExist(Class<? extends Activity> activityClass) {
+        for (Activity activity : Holder.INSTANCE.activityStack) {
+            if (activity == null) {
+                continue;
+            }
+            if (activity.getClass() == activityClass) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 76 - 0
common/src/main/java/com/atmob/common/runtime/ContextUtil.java

@@ -0,0 +1,76 @@
+package com.atmob.common.runtime;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.content.Context;
+
+import java.lang.reflect.Method;
+
+public class ContextUtil {
+
+    private static Application application;
+
+    private ContextUtil() {
+    }
+
+    public static void init(Application application) {
+        if (application == null) {
+            throw new IllegalArgumentException("application can't be null.");
+        }
+        ContextUtil.application = application;
+    }
+
+    public static Context getContext() {
+        if (application == null) {
+            application = ContextHelper.getApplication();
+        }
+        if (application == null) {
+            throw new IllegalStateException("can't getContext() before init().");
+        }
+        return application.getApplicationContext();
+    }
+
+    public static Application getApplication() {
+        if (application == null) {
+            application = ContextHelper.getApplication();
+        }
+        if (application == null) {
+            throw new IllegalStateException("can't getApplication() before init().");
+        }
+        return application;
+    }
+
+    private static final class ContextHelper {
+        @SuppressLint({"StaticFieldLeak"})
+        private static volatile Application context;
+
+        public static Application getApplication() {
+            return context;
+        }
+
+        private static Object getActivityThread() {
+            Object activityThread = null;
+            try {
+                @SuppressLint("PrivateApi")
+                Method method = Class.forName("android.app.ActivityThread")
+                        .getMethod("currentActivityThread");
+                method.setAccessible(true);
+                activityThread = method.invoke(null);
+            } catch (Throwable ignored) {
+
+            }
+            return activityThread;
+        }
+
+        static {
+            try {
+                Object activityThread = getActivityThread();
+                Object application = activityThread.getClass()
+                        .getMethod("getApplication").invoke(activityThread);
+                context = (Application) application;
+            } catch (Throwable ignored) {
+
+            }
+        }
+    }
+}

+ 70 - 0
common/src/main/java/com/atmob/common/runtime/ProcessUtil.java

@@ -0,0 +1,70 @@
+package com.atmob.common.runtime;
+
+import android.app.Application;
+import android.content.Context;
+import android.os.Build;
+import android.os.Looper;
+import android.os.Process;
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class ProcessUtil {
+    private ProcessUtil() {
+
+    }
+
+    /**
+     * 获取进程号对应的进程名
+     *
+     * @param pid 进程号
+     * @return 进程名
+     */
+    private static String getProcessName(int pid) {
+        BufferedReader reader = null;
+        try {
+            reader = new BufferedReader(new FileReader("/proc/" + pid + "/cmdline"));
+            String processName = reader.readLine();
+            if (!TextUtils.isEmpty(processName)) {
+                processName = processName.trim();
+            }
+            return processName;
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+        } finally {
+            try {
+                if (reader != null) {
+                    reader.close();
+                }
+            } catch (IOException exception) {
+                exception.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    public static boolean isMainProcess(Context context) {
+//        Context context = ContextUtil.getContext();
+
+
+        String processName = getProcessName(Process.myPid());
+        String packageName = context.getPackageName();
+
+        boolean isMainProcess = processName == null || processName.equals(packageName);
+
+        if (!isMainProcess && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            Application application = (Application) context.getApplicationContext();
+            processName = application.getPackageName();
+            isMainProcess = processName == null || processName.equals(packageName);
+        }
+
+
+        return isMainProcess;
+    }
+
+    public static boolean isMainThread() {
+        return Looper.getMainLooper() == Looper.myLooper();
+    }
+}

+ 48 - 0
common/src/main/java/com/atmob/common/text/TextProguard.java

@@ -0,0 +1,48 @@
+package com.atmob.common.text;
+
+public class TextProguard {
+
+    private TextProguard() {
+
+    }
+
+    /**
+     * Run 'Translator.main()' with Coverage to get encode str, put it in program, and decode in runtime
+     *
+     * @param args nope
+     */
+    public static void main(String[] args) {
+        //for example
+        //coding:
+        System.out.println(encode("wtf")); //b79a939390dfa8908d939bd1
+    }
+
+    public static String decode(String origin) {
+        byte[] originBytes = origin.getBytes();
+        byte[] targetBytes = new byte[originBytes.length / 2];
+        for (int i = 0; i < originBytes.length; i += 2) {
+            String byteStr = new String(originBytes, i, 2);
+            targetBytes[i / 2] = (byte) ~Integer.parseInt(byteStr, 16);
+        }
+        return new String(targetBytes);
+    }
+
+    public static String encode(String origin) {
+        byte[] originBytes = origin.getBytes();
+        for (int i = 0; i < originBytes.length; i++) {
+            originBytes[i] = (byte) ~originBytes[i];
+        }
+        StringBuilder stringBuilder = new StringBuilder(originBytes.length << 1);
+        for (byte originByte : originBytes) {
+            int temp = originByte;
+            while (temp < 0) {
+                temp += 256;
+            }
+            if (temp < 16) {
+                stringBuilder.append("0");
+            }
+            stringBuilder.append(Integer.toString(temp, 16));
+        }
+        return stringBuilder.toString();
+    }
+}

+ 96 - 0
common/src/main/java/com/atmob/common/text/TextUtil.java

@@ -0,0 +1,96 @@
+package com.atmob.common.text;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.text.format.Formatter;
+import android.util.Patterns;
+
+import com.atmob.common.runtime.ContextUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.util.Locale;
+
+public class TextUtil {
+    /**
+     * 格式化浮点型数字, 超过位数的舍弃
+     * example:
+     * formatFloatWithout0End(1.234f, 2); return 1.23
+     * formatFloatWithout0End(1.235f, 2); return 1.23
+     * formatFloatWithout0End(1.23f, 4); return 1.23
+     * formatFloatWithout0End(1f, 4); return 1
+     *
+     * @param value    数值
+     * @param decimals 保留几位小数, 不足位数的不补0
+     * @return 格式化后的字符串
+     */
+    public static String formatFloatWithout0End(float value, int decimals) {
+        if (value == 0) {
+            return "0";
+        }
+        decimals = Math.max(0, decimals);
+        StringBuilder pattern = new StringBuilder("#.");
+        for (int i = 0; i < decimals; i++) {
+            pattern.append("#");
+        }
+        DecimalFormat decimalFormat = new DecimalFormat(pattern.toString());
+        return decimalFormat.format(value);
+    }
+
+    public static String formatSecond2Minute(long seconds) {
+        long m = seconds / 60;
+        long s = (seconds - m * 60) % 60;
+        return String.format(Locale.getDefault(), "%02d:%02d", m, s);
+    }
+
+    public static boolean isUrl(String str) {
+        if (TextUtils.isEmpty(str)) {
+            return false;
+        }
+        return Patterns.WEB_URL.matcher(str).matches();
+    }
+
+    public static void copy2Clipboard(CharSequence label, CharSequence text) {
+        ClipData clipData = ClipData.newPlainText(label, text);
+        copy2Clipboard(clipData);
+    }
+
+    public static void copy2Clipboard(ClipData clipData) {
+        Context context = ContextUtil.getContext();
+        ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+        clipboardManager.setPrimaryClip(clipData);
+    }
+
+    public static String urlEncode(String origin) {
+        try {
+            return URLEncoder.encode(origin, StandardCharsets.UTF_8.toString());
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        return origin;
+    }
+
+    public static String urlDecode(String origin) {
+        try {
+            return URLDecoder.decode(origin, StandardCharsets.UTF_8.toString());
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+        return origin;
+    }
+
+    public static String formatBytes(long bytes) {
+        Context context = ContextUtil.getContext();
+        return Formatter.formatFileSize(context, bytes);
+    }
+
+    public static String formatShortBytes(long bytes) {
+        Context context = ContextUtil.getContext();
+        return Formatter.formatShortFileSize(context, bytes);
+    }
+}

+ 58 - 0
common/src/main/java/com/atmob/common/thread/ThreadPoolUtil.java

@@ -0,0 +1,58 @@
+package com.atmob.common.thread;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class ThreadPoolUtil {
+    private static volatile ThreadPoolUtil INSTANCE;
+
+    private static final ExecutorService executorService;
+
+    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+    private static final int KEEP_ALIVE_SECONDS = 30;
+    private static final BlockingQueue<Runnable> sPoolWorkQueue =
+            new LinkedBlockingQueue<>(128);
+
+    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
+        private final AtomicInteger mCount = new AtomicInteger(1);
+
+        @Override
+        public Thread newThread(Runnable r) {
+            return new Thread(r, "NetWork Dispatcher #" + mCount.getAndIncrement());
+        }
+    };
+
+    static {
+        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
+                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
+                sPoolWorkQueue, sThreadFactory);
+        threadPoolExecutor.allowCoreThreadTimeOut(true);
+
+        executorService = threadPoolExecutor;
+    }
+
+    private ThreadPoolUtil() {
+    }
+
+    public static ThreadPoolUtil getInstance() {
+        if (INSTANCE == null) {
+            synchronized (ThreadPoolUtil.class) {
+                if (INSTANCE == null) {
+                    INSTANCE = new ThreadPoolUtil();
+                }
+            }
+        }
+        return INSTANCE;
+    }
+
+    public void execute(Runnable runnable) {
+        executorService.execute(runnable);
+    }
+}

+ 62 - 0
common/src/main/java/com/atmob/common/ui/SizeUtil.java

@@ -0,0 +1,62 @@
+package com.atmob.common.ui;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.WindowManager;
+
+import com.atmob.common.runtime.ContextUtil;
+
+public class SizeUtil {
+    private static final DisplayMetrics outMetrics = new DisplayMetrics();
+
+    private SizeUtil() {
+
+    }
+
+    public static float dp2px(float dip) {
+        Context context = ContextUtil.getContext();
+        Resources resources = context.getResources();
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, resources.getDisplayMetrics());
+    }
+
+    public static float sp2px(float sp) {
+        Context context = ContextUtil.getContext();
+        Resources resources = context.getResources();
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, resources.getDisplayMetrics());
+    }
+
+    public static float px2dp(float pixel) {
+        Context context = ContextUtil.getContext();
+        Resources resources = context.getResources();
+        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
+        return pixel / displayMetrics.density;
+    }
+
+    public static int getScreenWidth() {
+        Context context = ContextUtil.getContext();
+        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        windowManager.getDefaultDisplay().getRealMetrics(outMetrics);
+        return outMetrics.widthPixels;
+    }
+
+    public static int getScreenHeight() {
+        Context context = ContextUtil.getContext();
+        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        windowManager.getDefaultDisplay().getRealMetrics(outMetrics);
+        return outMetrics.heightPixels;
+    }
+
+    public static int getStatusBarHeight() {
+        Context context = ContextUtil.getContext();
+        int height = 0;
+        @SuppressLint({"InternalInsetResource", "DiscouragedApi"})
+        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
+        if (resourceId > 0) {
+            height = context.getResources().getDimensionPixelSize(resourceId);
+        }
+        return height;
+    }
+}

+ 22 - 0
gradle.properties

@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=false

二進制
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Tue Mar 14 16:30:40 HKT 2023
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME

+ 185 - 0
gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
network/.gitignore

@@ -0,0 +1 @@
+/build

+ 53 - 0
network/build.gradle

@@ -0,0 +1,53 @@
+plugins {
+    id 'com.android.library'
+}
+apply plugin: 'kotlin-android'
+apply from: 'publish.gradle'
+
+
+android {
+    compileSdk 32
+
+    defaultConfig {
+        minSdk 21
+        targetSdk 32
+
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+configurations.all {
+    resolutionStrategy {
+        // don't cache changing modules at all
+        cacheChangingModulesFor 10, 'seconds'
+    }
+}
+
+dependencies {
+    api fileTree(dir: "libs", include: ["*.jar"])
+
+    api("plus.net.okhttp3:okhttp:4.10.0-SNAPSHOT")
+    api("plus.net.retrofit2:retrofit:2.9.0-SNAPSHOT")
+    api("plus.net.retrofit2:adapters-rxjava3:2.9.0-SNAPSHOT")
+    api("plus.net.retrofit2:converters-gson:2.9.0-SNAPSHOT")
+
+//    implementation "extra.room:room-rxjava3:2.5.0-SNAPSHOT"
+
+
+    compileOnly 'com.google.code.gson:gson:+'
+    compileOnly 'androidx.annotation:annotation:+'
+    compileOnly 'com.google.protobuf:protobuf-javalite:+'
+    compileOnly project(":common")
+    compileOnly project(":rxjava")
+}

+ 4 - 0
network/config.gradle

@@ -0,0 +1,4 @@
+ext {
+    atmob_network_version_code = 2
+    atmob_network_version_name = "1.1.0-SNAPSHOT"
+}

+ 14 - 0
network/consumer-rules.pro

@@ -0,0 +1,14 @@
+# JSR 305 annotations are for embedding nullability information.
+-dontwarn javax.annotation.**
+
+# A resource is loaded with a relative path so the package of this class must be preserved.
+-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz
+
+# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
+-dontwarn org.codehaus.mojo.animal_sniffer.*
+
+# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
+-dontwarn atmob.okhttp3.internal.platform.**
+-dontwarn org.conscrypt.**
+-dontwarn org.bouncycastle.**
+-dontwarn org.openjsse.**

+ 21 - 0
network/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 59 - 0
network/publish.gradle

@@ -0,0 +1,59 @@
+apply plugin: 'maven-publish'
+apply from: 'config.gradle'
+
+task androidJavadocs(type: Javadoc) {
+    source = android.sourceSets.main.java.sourceFiles
+    ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar"
+    classpath += files(ext.androidJar)
+}
+
+task androidJavadocsJar(type: Jar) {
+    getArchiveClassifier().set("javadoc")
+    from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+    getArchiveClassifier().set("sources")
+    from android.sourceSets.main.java.srcDirs
+}
+
+
+String ver = "$ext.atmob_network_version_name"
+
+String publishUrl = !ver.endsWith("-SNAPSHOT") ? "$atmob_maven_url/repository/android-release/"
+        : "$atmob_maven_url/repository/android-snapshot/"
+
+
+String GROUP_ID = "extra.common"
+String ARTIFACT_ID = "network"
+
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            push(MavenPublication) {
+                from components.release
+
+                groupId = GROUP_ID
+                artifactId = ARTIFACT_ID
+                version = ver
+
+                artifact androidSourcesJar
+                artifact androidJavadocsJar
+            }
+
+        }
+        repositories {
+            maven {
+                name = "nexus"
+                allowInsecureProtocol true
+                credentials {
+                    username = "$atmob_maven_username"
+                    password = "$atmob_maven_password"
+                }
+                url = publishUrl
+            }
+        }
+    }
+}

+ 5 - 0
network/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.network">
+
+</manifest>

+ 193 - 0
network/src/main/java/com/atmob/network/okhttp/AtmobOkHttpClient.java

@@ -0,0 +1,193 @@
+package com.atmob.network.okhttp;
+
+import android.content.Context;
+import android.util.Pair;
+
+import com.atmob.common.runtime.ContextUtil;
+import com.atmob.network.okhttp.logging.HttpLoggingInterceptor;
+import com.atmob.network.okhttp.logging.Level;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import atmob.okhttp3.Cache;
+import atmob.okhttp3.OkHttpClient;
+
+public class AtmobOkHttpClient {
+
+    private static final String TAG = AtmobOkHttpClient.class.getSimpleName();
+
+    private static final long MaxHttpCacheSize = 50L * 1024L * 1024L; // 50 MiB;
+
+    private final static String KEYSTORE_TYPE = "BKS";
+    private final static String PROTOCOL_TYPE = "TLS";
+    private final static String CERTIFICATE_FORMAT = "X509";
+
+    private volatile static OkHttpClient INSTANCE;
+
+    private AtmobOkHttpClient() {
+
+    }
+
+    public static OkHttpClient defaultInstance() {
+        if (INSTANCE == null) {
+            synchronized (AtmobOkHttpClient.class) {
+                if (INSTANCE == null) {
+                    INSTANCE = newInstance(TAG, false);
+                }
+            }
+        }
+        return INSTANCE;
+    }
+
+    public static OkHttpClient newInstance(String logTAG, boolean loggable) {
+        Pair<X509TrustManager, SSLSocketFactory> pair = getSSLSocketFactory();
+        return new OkHttpClient.Builder()
+                .addInterceptor(newLoggingInterceptor(logTAG, loggable))
+                .hostnameVerifier((hostname, session) -> true)
+                .cache(getCacheConfig())
+                .sslSocketFactory(pair.second, pair.first)
+                .build();
+    }
+
+    private static Cache getCacheConfig() {
+        Context context = ContextUtil.getContext();
+        File cacheDir = context.getExternalCacheDir();
+        if (cacheDir == null) {
+            cacheDir = context.getCacheDir();
+        }
+        File file = new File(cacheDir, "atmob_net");
+        if (!file.isDirectory() || !file.exists()) {
+            file.mkdirs();
+        }
+        return new Cache(file, MaxHttpCacheSize);
+    }
+
+    public static HttpLoggingInterceptor newLoggingInterceptor(String TAG, boolean loggable) {
+        return new HttpLoggingInterceptor.Builder()
+                .loggable(loggable)
+                .tag(TAG)
+                .setLevel(Level.BODY)
+                .build();
+    }
+
+    public static Pair<X509TrustManager, SSLSocketFactory> getSSLSocketFactory() {
+        return getSSLSocketFactory(null, null, null);
+    }
+
+    private static Pair<X509TrustManager, SSLSocketFactory> getSSLSocketFactory(X509TrustManager trustManager, InputStream bksFile, String password, InputStream... certificates) {
+        try {
+            KeyManager[] keyManagers = prepareKeyManager(bksFile, password);
+            TrustManager[] trustManagers = prepareTrustManager(certificates);
+            X509TrustManager manager;
+            if (trustManager != null) {
+                //优先使用用户自定义的TrustManager
+                manager = trustManager;
+            } else if (trustManagers != null) {
+                //然后使用默认的TrustManager
+                manager = chooseTrustManager(trustManagers);
+            } else {
+                //否则使用不安全的TrustManager
+                manager = UnSafeTrustManager;
+            }
+            // 创建TLS类型的SSLContext对象, that uses our TrustManager
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            // 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书
+            // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书
+            sslContext.init(keyManagers, new TrustManager[]{manager}, null);
+            // 通过sslContext获取SSLSocketFactory对象
+            return new Pair<>(manager, sslContext.getSocketFactory());
+        } catch (NoSuchAlgorithmException | KeyManagementException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) {
+        try {
+            if (bksFile == null || password == null) return null;
+            KeyStore clientKeyStore = KeyStore.getInstance("BKS");
+            clientKeyStore.load(bksFile, password.toCharArray());
+            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(clientKeyStore, password.toCharArray());
+            return kmf.getKeyManagers();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    private static TrustManager[] prepareTrustManager(InputStream... certificates) {
+        if (certificates == null || certificates.length <= 0) return null;
+        try {
+            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+            // 创建一个默认类型的KeyStore,存储我们信任的证书
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            keyStore.load(null);
+            int index = 0;
+            for (InputStream certStream : certificates) {
+                String certificateAlias = Integer.toString(index++);
+                // 证书工厂根据证书文件的流生成证书 cert
+                Certificate cert = certificateFactory.generateCertificate(certStream);
+                // 将 cert 作为可信证书放入到keyStore中
+                keyStore.setCertificateEntry(certificateAlias, cert);
+                try {
+                    if (certStream != null) certStream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            //我们创建一个默认类型的TrustManagerFactory
+            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            //用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书
+            tmf.init(keyStore);
+            //通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书
+            return tmf.getTrustManagers();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) {
+        for (TrustManager trustManager : trustManagers) {
+            if (trustManager instanceof X509TrustManager) {
+                return (X509TrustManager) trustManager;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查,
+     * 这是一种有很大安全漏洞的办法
+     */
+    public static X509TrustManager UnSafeTrustManager = new X509TrustManager() {
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType) {
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType) {
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[]{};
+        }
+    };
+}

+ 246 - 0
network/src/main/java/com/atmob/network/okhttp/logging/HttpLoggingInterceptor.java

@@ -0,0 +1,246 @@
+package com.atmob.network.okhttp.logging;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import atmob.okhttp3.Headers;
+import atmob.okhttp3.Interceptor;
+import atmob.okhttp3.MediaType;
+import atmob.okhttp3.Request;
+import atmob.okhttp3.RequestBody;
+import atmob.okhttp3.Response;
+import atmob.okhttp3.ResponseBody;
+import atmob.okhttp3.internal.platform.Platform;
+
+
+/**
+ * @author ihsan on 09/02/2017.
+ */
+
+public class HttpLoggingInterceptor implements Interceptor {
+
+    private final boolean isDebug;
+    private final Builder builder;
+
+    private HttpLoggingInterceptor(Builder builder) {
+        this.builder = builder;
+        this.isDebug = builder.isDebug;
+    }
+
+    @NonNull
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        Request request = chain.request();
+        if (builder.getHeaders().size() > 0) {
+            Headers headers = request.headers();
+            Set<String> names = headers.names();
+            Iterator<String> iterator = names.iterator();
+            Request.Builder requestBuilder = request.newBuilder();
+            requestBuilder.headers(builder.getHeaders());
+            while (iterator.hasNext()) {
+                String name = iterator.next();
+                String value = headers.get(name);
+                if (value == null) {
+                    continue;
+                }
+                requestBuilder.addHeader(name, value);
+            }
+            request = requestBuilder.build();
+        }
+
+        if (!isDebug || builder.getLevel() == Level.NONE) {
+            return chain.proceed(request);
+        }
+        RequestBody requestBody = request.body();
+
+        MediaType rContentType = null;
+        if (requestBody != null) {
+            rContentType = requestBody.contentType();
+        }
+
+        String rSubtype = null;
+        if (rContentType != null) {
+            rSubtype = rContentType.subtype();
+        }
+
+        if (rSubtype != null && (rSubtype.contains("json")
+                || rSubtype.contains("xml")
+                || rSubtype.contains("plain")
+                || rSubtype.contains("html"))) {
+            Printer.printJsonRequest(builder, request);
+        } else {
+            Printer.printFileRequest(builder, request);
+        }
+
+        long st = System.nanoTime();
+        Response response = chain.proceed(request);
+
+        List<String> segmentList = request.url().encodedPathSegments();
+        long chainMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - st);
+        String header = response.headers().toString();
+        int code = response.code();
+        boolean isSuccessful = response.isSuccessful();
+        ResponseBody responseBody = response.body();
+        MediaType contentType = null;
+        if (responseBody != null) {
+            contentType = responseBody.contentType();
+        }
+
+        String subtype = null;
+        ResponseBody body;
+
+        if (contentType != null) {
+            subtype = contentType.subtype();
+        }
+
+        if (subtype != null && (subtype.contains("json")
+                || subtype.contains("xml")
+                || subtype.contains("plain")
+                || subtype.contains("html"))) {
+            String bodyString = responseBody.string();
+            String bodyJson = Printer.getJsonString(bodyString);
+            Printer.printJsonResponse(builder, chainMs, isSuccessful, code, header, bodyJson, segmentList);
+            body = ResponseBody.create(bodyString, contentType);
+        } else {
+            Printer.printFileResponse(builder, chainMs, isSuccessful, code, header, segmentList);
+            return response;
+        }
+        return response.newBuilder().body(body).build();
+    }
+
+    @SuppressWarnings("unused")
+    public static class Builder {
+
+        private static String TAG = "LoggingI";
+        private boolean isDebug;
+        private int type = Platform.INFO;
+        private String requestTag;
+        private String responseTag;
+        private Level level = Level.BASIC;
+        private final Headers.Builder builder;
+        private Logger logger;
+
+        public Builder() {
+            builder = new Headers.Builder();
+        }
+
+        int getType() {
+            return type;
+        }
+
+        Level getLevel() {
+            return level;
+        }
+
+        Headers getHeaders() {
+            return builder.build();
+        }
+
+        String getTag(boolean isRequest) {
+            if (isRequest) {
+                return TextUtils.isEmpty(requestTag) ? TAG : requestTag;
+            } else {
+                return TextUtils.isEmpty(responseTag) ? TAG : responseTag;
+            }
+        }
+
+        Logger getLogger() {
+            return logger;
+        }
+
+        /**
+         * @param name  Filed
+         * @param value Value
+         * @return Builder
+         * Add a field with the specified value
+         */
+        public Builder addHeader(String name, String value) {
+            builder.set(name, value);
+            return this;
+        }
+
+        /**
+         * @param level set log level
+         * @return Builder
+         * @see Level
+         */
+        public Builder setLevel(Level level) {
+            this.level = level;
+            return this;
+        }
+
+        /**
+         * Set request and response each log tag
+         *
+         * @param tag general log tag
+         * @return Builder
+         */
+        public Builder tag(String tag) {
+            TAG = tag;
+            return this;
+        }
+
+        /**
+         * Set request log tag
+         *
+         * @param tag request log tag
+         * @return Builder
+         */
+        public Builder request(String tag) {
+            this.requestTag = tag;
+            return this;
+        }
+
+        /**
+         * Set response log tag
+         *
+         * @param tag response log tag
+         * @return Builder
+         */
+        public Builder response(String tag) {
+            this.responseTag = tag;
+            return this;
+        }
+
+        /**
+         * @param isDebug set can sending log output
+         * @return Builder
+         */
+        public Builder loggable(boolean isDebug) {
+            this.isDebug = isDebug;
+            return this;
+        }
+
+        /**
+         * @param type set sending log output type
+         * @return Builder
+         * @see Platform
+         */
+        public Builder log(int type) {
+            this.type = type;
+            return this;
+        }
+
+        /**
+         * @param logger manuel logging interface
+         * @return Builder
+         * @see Logger
+         */
+        public Builder logger(Logger logger) {
+            this.logger = logger;
+            return this;
+        }
+
+        public HttpLoggingInterceptor build() {
+            return new HttpLoggingInterceptor(this);
+        }
+    }
+
+}

+ 25 - 0
network/src/main/java/com/atmob/network/okhttp/logging/I.java

@@ -0,0 +1,25 @@
+package com.atmob.network.okhttp.logging;
+
+import java.util.logging.Level;
+
+import atmob.okhttp3.internal.platform.Platform;
+
+
+/**
+ * @author ihsan on 10/02/2017.
+ */
+class I {
+
+    protected I() {
+        throw new UnsupportedOperationException();
+    }
+
+    static void log(int type, String tag, String msg) {
+        java.util.logging.Logger logger = java.util.logging.Logger.getLogger(tag);
+        if (type == Platform.INFO) {
+            logger.log(Level.INFO, msg);
+        } else {
+            logger.log(Level.WARNING, msg);
+        }
+    }
+}

+ 40 - 0
network/src/main/java/com/atmob/network/okhttp/logging/Level.java

@@ -0,0 +1,40 @@
+package com.atmob.network.okhttp.logging;
+
+/**
+ * @author ihsan on 21/02/2017.
+ */
+
+public enum Level {
+    /**
+     * No logs.
+     */
+    NONE,
+    /**
+     * <p>Example:
+     * <pre>{@code
+     *  - URL
+     *  - Method
+     *  - Headers
+     *  - Body
+     * }</pre>
+     */
+    BASIC,
+    /**
+     * <p>Example:
+     * <pre>{@code
+     *  - URL
+     *  - Method
+     *  - Headers
+     * }</pre>
+     */
+    HEADERS,
+    /**
+     * <p>Example:
+     * <pre>{@code
+     *  - URL
+     *  - Method
+     *  - Body
+     * }</pre>
+     */
+    BODY
+}

+ 13 - 0
network/src/main/java/com/atmob/network/okhttp/logging/Logger.java

@@ -0,0 +1,13 @@
+package com.atmob.network.okhttp.logging;
+
+import atmob.okhttp3.internal.platform.Platform;
+
+/**
+ * @author ihsan on 11/07/2017.
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public interface Logger {
+    void log(int level, String tag, String msg);
+
+    Logger DEFAULT = (level, tag, message) -> Platform.get().log(message, level, null);
+}

+ 234 - 0
network/src/main/java/com/atmob/network/okhttp/logging/Printer.java

@@ -0,0 +1,234 @@
+package com.atmob.network.okhttp.logging;
+
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.List;
+
+import atmob.okhttp3.FormBody;
+import atmob.okhttp3.Request;
+import atmob.okhttp3.RequestBody;
+import atmob.okio.Buffer;
+
+/**
+ * @author ihsan on 09/02/2017.
+ */
+
+class Printer {
+
+    private static final int JSON_INDENT = 3;
+
+    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+    private static final String DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR;
+
+    private static final String[] OMITTED_RESPONSE = {LINE_SEPARATOR, "Omitted response body"};
+    private static final String[] OMITTED_REQUEST = {LINE_SEPARATOR, "Omitted request body"};
+
+    private static final String N = "\n";
+    private static final String T = "\t";
+    private static final String REQUEST_UP_LINE = "┌────── Request ────────────────────────────────────────────────────────────────────────";
+    private static final String END_LINE = "└───────────────────────────────────────────────────────────────────────────────────────";
+    private static final String RESPONSE_UP_LINE = "┌────── Response ───────────────────────────────────────────────────────────────────────";
+    private static final String BODY_TAG = "Body:";
+    private static final String URL_TAG = "URL: ";
+    private static final String METHOD_TAG = "Method: @";
+    private static final String HEADERS_TAG = "Headers:";
+    private static final String STATUS_CODE_TAG = "Status Code: ";
+    private static final String RECEIVED_TAG = "Received in: ";
+    private static final String CORNER_UP = "┌ ";
+    private static final String CORNER_BOTTOM = "└ ";
+    private static final String CENTER_LINE = "├ ";
+    private static final String DEFAULT_LINE = "│ ";
+
+    protected Printer() {
+        throw new UnsupportedOperationException();
+    }
+
+    private static boolean isEmpty(String line) {
+        return TextUtils.isEmpty(line) || N.equals(line) || T.equals(line) || TextUtils.isEmpty(line.trim());
+    }
+
+    static void printJsonRequest(HttpLoggingInterceptor.Builder builder, Request request) {
+        String requestBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(request);
+        String tag = builder.getTag(true);
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, REQUEST_UP_LINE);
+        logLines(builder.getType(), tag, new String[]{URL_TAG + request.url()}, builder.getLogger(), false);
+        logLines(builder.getType(), tag, getRequest(request, builder.getLevel()), builder.getLogger(), true);
+        if (request.body() instanceof FormBody) {
+            StringBuilder formBody = new StringBuilder();
+            FormBody body = (FormBody) request.body();
+            if (body != null && body.size() != 0) {
+                for (int i = 0; i < body.size(); i++) {
+                    formBody.append(body.encodedName(i)).append("=").append(body.encodedValue(i)).append("&");
+                }
+                formBody.delete(formBody.length() - 1, formBody.length());
+                logLines(builder.getType(), tag, new String[]{formBody.toString()}, builder.getLogger(), true);
+            }
+        }
+        if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) {
+            logLines(builder.getType(), tag, requestBody.split(LINE_SEPARATOR), builder.getLogger(), true);
+        }
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, END_LINE);
+    }
+
+    static void printJsonResponse(HttpLoggingInterceptor.Builder builder, long chainMs, boolean isSuccessful,
+                                  int code, String headers, String bodyString, List<String> segments) {
+        String responseBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getJsonString(bodyString);
+        String tag = builder.getTag(false);
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, RESPONSE_UP_LINE);
+
+        logLines(builder.getType(), tag, getResponse(headers, chainMs, code, isSuccessful,
+                builder.getLevel(), segments), builder.getLogger(), true);
+        if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) {
+            logLines(builder.getType(), tag, responseBody.split(LINE_SEPARATOR), builder.getLogger(), true);
+        }
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, END_LINE);
+    }
+
+    static void printFileRequest(HttpLoggingInterceptor.Builder builder, Request request) {
+        String tag = builder.getTag(true);
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, REQUEST_UP_LINE);
+        logLines(builder.getType(), tag, new String[]{URL_TAG + request.url()}, builder.getLogger(), false);
+        logLines(builder.getType(), tag, getRequest(request, builder.getLevel()), builder.getLogger(), true);
+        if (request.body() instanceof FormBody) {
+            StringBuilder formBody = new StringBuilder();
+            FormBody body = (FormBody) request.body();
+            if (body != null && body.size() != 0) {
+                for (int i = 0; i < body.size(); i++) {
+                    formBody.append(body.encodedName(i)).append("=").append(body.encodedValue(i)).append("&");
+                }
+                formBody.delete(formBody.length() - 1, formBody.length());
+                logLines(builder.getType(), tag, new String[]{formBody.toString()}, builder.getLogger(), true);
+            }
+        }
+        if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) {
+            logLines(builder.getType(), tag, OMITTED_REQUEST, builder.getLogger(), true);
+        }
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, END_LINE);
+    }
+
+    static void printFileResponse(HttpLoggingInterceptor.Builder builder, long chainMs, boolean isSuccessful,
+                                  int code, String headers, List<String> segments) {
+        String tag = builder.getTag(false);
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, RESPONSE_UP_LINE);
+
+        logLines(builder.getType(), tag, getResponse(headers, chainMs, code, isSuccessful,
+                builder.getLevel(), segments), builder.getLogger(), true);
+        logLines(builder.getType(), tag, OMITTED_RESPONSE, builder.getLogger(), true);
+        if (builder.getLogger() == null)
+            I.log(builder.getType(), tag, END_LINE);
+    }
+
+    private static String[] getRequest(Request request, Level level) {
+        String message;
+        String header = request.headers().toString();
+        boolean loggableHeader = level == Level.HEADERS || level == Level.BASIC;
+        message = METHOD_TAG + request.method() + DOUBLE_SEPARATOR +
+                (isEmpty(header) ? "" : loggableHeader ? HEADERS_TAG + LINE_SEPARATOR + dotHeaders(header) : "");
+        return message.split(LINE_SEPARATOR);
+    }
+
+    private static String[] getResponse(String header, long tookMs, int code, boolean isSuccessful,
+                                        Level level, List<String> segments) {
+        String message;
+        boolean loggableHeader = level == Level.HEADERS || level == Level.BASIC;
+        String segmentString = slashSegments(segments);
+        message = ((!TextUtils.isEmpty(segmentString) ? segmentString + " - " : "") + "is success : "
+                + isSuccessful + " - " + RECEIVED_TAG + tookMs + "ms" + DOUBLE_SEPARATOR + STATUS_CODE_TAG +
+                code + DOUBLE_SEPARATOR + (isEmpty(header) ? "" : loggableHeader ? HEADERS_TAG + LINE_SEPARATOR +
+                dotHeaders(header) : ""));
+        return message.split(LINE_SEPARATOR);
+    }
+
+    private static String slashSegments(List<String> segments) {
+        StringBuilder segmentString = new StringBuilder();
+        for (String segment : segments) {
+            segmentString.append("/").append(segment);
+        }
+        return segmentString.toString();
+    }
+
+    private static String dotHeaders(String header) {
+        String[] headers = header.split(LINE_SEPARATOR);
+        StringBuilder builder = new StringBuilder();
+        String tag = "─ ";
+        if (headers.length > 1) {
+            for (int i = 0; i < headers.length; i++) {
+                if (i == 0) {
+                    tag = CORNER_UP;
+                } else if (i == headers.length - 1) {
+                    tag = CORNER_BOTTOM;
+                } else {
+                    tag = CENTER_LINE;
+                }
+                builder.append(tag).append(headers[i]).append("\n");
+            }
+        } else {
+            for (String item : headers) {
+                builder.append(tag).append(item).append("\n");
+            }
+        }
+        return builder.toString();
+    }
+
+    private static void logLines(int type, String tag, String[] lines, Logger logger, boolean withLineSize) {
+        for (String line : lines) {
+            int lineLength = line.length();
+            int MAX_LONG_SIZE = withLineSize ? 110 : lineLength;
+            for (int i = 0; i <= lineLength / MAX_LONG_SIZE; i++) {
+                int start = i * MAX_LONG_SIZE;
+                int end = (i + 1) * MAX_LONG_SIZE;
+                end = Math.min(end, line.length());
+                if (logger == null) {
+                    I.log(type, tag, DEFAULT_LINE + line.substring(start, end));
+                } else {
+                    logger.log(type, tag, line.substring(start, end));
+                }
+            }
+        }
+    }
+
+    private static String bodyToString(final Request request) {
+        try {
+            final Request copy = request.newBuilder().build();
+            final Buffer buffer = new Buffer();
+            RequestBody body = copy.body();
+            if (body == null)
+                return "";
+            body.writeTo(buffer);
+            return getJsonString(buffer.readUtf8());
+        } catch (final IOException e) {
+            return "{\"err\": \"" + e.getMessage() + "\"}";
+        }
+    }
+
+    static String getJsonString(String msg) {
+        String message;
+        try {
+            if (msg.startsWith("{")) {
+                JSONObject jsonObject = new JSONObject(msg);
+                message = jsonObject.toString(JSON_INDENT);
+            } else if (msg.startsWith("[")) {
+                JSONArray jsonArray = new JSONArray(msg);
+                message = jsonArray.toString(JSON_INDENT);
+            } else {
+                message = msg;
+            }
+        } catch (JSONException e) {
+            message = msg;
+        }
+        return message;
+    }
+
+}

+ 1 - 0
room-rx/.gitignore

@@ -0,0 +1 @@
+/build

+ 22 - 0
room-rx/build.gradle

@@ -0,0 +1,22 @@
+plugins {
+    id 'com.android.library'
+}
+apply from: 'publish.gradle'
+
+android {
+    compileSdkVersion 30
+    defaultConfig {
+        minSdkVersion 21
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    compileOnly("plus.reactivex.rxjava3:rxjava:3.1.6-SNAPSHOT")
+    compileOnly("plus.reactivex.rxjava3:rxandroid:3.0.2-SNAPSHOT")
+    api 'androidx.room:room-common:2.5.0'
+    api 'androidx.room:room-runtime:2.5.0'
+}

+ 2 - 0
room-rx/consumer-rules.pro

@@ -0,0 +1,2 @@
+-dontshrink
+-dontwarn java.util.concurrent.Flow*

+ 23 - 0
room-rx/proguard-rules.pro

@@ -0,0 +1,23 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-dontwarn java.util.concurrent.Flow*

+ 58 - 0
room-rx/publish.gradle

@@ -0,0 +1,58 @@
+apply plugin: 'maven-publish'
+
+task androidJavadocs(type: Javadoc) {
+    source = android.sourceSets.main.java.sourceFiles
+    ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar"
+    classpath += files(ext.androidJar)
+}
+
+task androidJavadocsJar(type: Jar) {
+    getArchiveClassifier().set("javadoc")
+    from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+    getArchiveClassifier().set("sources")
+    from android.sourceSets.main.java.srcDirs
+}
+
+
+String ver = "2.5.0-SNAPSHOT"
+
+String publishUrl = !ver.endsWith("-SNAPSHOT") ? "$atmob_maven_url/repository/android-release/"
+        : "$atmob_maven_url/repository/android-snapshot/"
+
+
+String GROUP_ID = "extra.room"
+String ARTIFACT_ID = "room-rxjava3"
+
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            push(MavenPublication) {
+                from components.release
+
+                groupId = GROUP_ID
+                artifactId = ARTIFACT_ID
+                version = ver
+
+                artifact androidSourcesJar
+                artifact androidJavadocsJar
+            }
+
+        }
+        repositories {
+            maven {
+                name = "nexus"
+                allowInsecureProtocol true
+                credentials {
+                    username = "$atmob_maven_username"
+                    password = "$atmob_maven_password"
+                }
+                url = publishUrl
+            }
+        }
+    }
+}

+ 5 - 0
room-rx/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="atmob.room.rxjava3">
+
+</manifest>

+ 39 - 0
room-rx/src/main/java/atmob/room/rxjava3/EmptyResultSetException.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package atmob.room.rxjava3;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Thrown by Room when the query in a Single&lt;T&gt; DAO method needs to return a result but the
+ * returned result from the database is empty.
+ * <p>
+ * Since a Single&lt;T&gt; must either emit a single non-null value or an error, this exception is
+ * thrown instead of emitting a null value when the query resulted empty. If the Single&lt;T&gt;
+ * contains a type argument of a collection (e.g. Single&lt;List&lt;Song&gt&gt;) then this
+ * exception is not thrown an an empty collection is emitted instead.
+ */
+public final class EmptyResultSetException extends RuntimeException {
+    /**
+     * Constructs a new EmptyResultSetException with the exception.
+     *
+     * @param message The SQL query which didn't return any results.
+     */
+    public EmptyResultSetException(@NonNull String message) {
+        super(message);
+    }
+}

+ 193 - 0
room-rx/src/main/java/atmob/room/rxjava3/RxRoom.java

@@ -0,0 +1,193 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package atmob.room.rxjava3;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.room.InvalidationTracker;
+import androidx.room.RoomDatabase;
+
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
+import atmob.reactivex.rxjava3.core.BackpressureStrategy;
+import atmob.reactivex.rxjava3.core.Flowable;
+import atmob.reactivex.rxjava3.core.Maybe;
+import atmob.reactivex.rxjava3.core.MaybeSource;
+import atmob.reactivex.rxjava3.core.Observable;
+import atmob.reactivex.rxjava3.core.Scheduler;
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import atmob.reactivex.rxjava3.functions.Function;
+import atmob.reactivex.rxjava3.schedulers.Schedulers;
+
+/**
+ * Helper class to add RxJava3 support to Room.
+ */
+public final class RxRoom {
+    /**
+     * Data dispatched by the publisher created by {@link #createFlowable(RoomDatabase, String...)}.
+     */
+    @NonNull
+    public static final Object NOTHING = new Object();
+
+    /**
+     * Creates a {@link Flowable} that emits at least once and also re-emits whenever one of the
+     * observed tables is updated.
+     * <p>
+     * You can easily chain a database operation to downstream of this {@link Flowable} to ensure
+     * that it re-runs when database is modified.
+     * <p>
+     * Since database invalidation is batched, multiple changes in the database may results in just
+     * 1 emission.
+     *
+     * @param database   The database instance
+     * @param tableNames The list of table names that should be observed
+     * @return A {@link Flowable} which emits {@link #NOTHING} when one of the observed tables
+     * is modified (also once when the invalidation tracker connection is established).
+     */
+    @NonNull
+    public static Flowable<Object> createFlowable(@NonNull final RoomDatabase database,
+            @NonNull final String... tableNames) {
+        return Flowable.create(emitter -> {
+            final InvalidationTracker.Observer observer = new InvalidationTracker.Observer(
+                    tableNames) {
+                @Override
+                public void onInvalidated(@NonNull Set<String> tables) {
+                    if (!emitter.isCancelled()) {
+                        emitter.onNext(NOTHING);
+                    }
+                }
+            };
+            if (!emitter.isCancelled()) {
+                database.getInvalidationTracker().addObserver(observer);
+                emitter.setDisposable(Disposable.fromAction(
+                        () -> database.getInvalidationTracker().removeObserver(observer)));
+            }
+
+            // emit once to avoid missing any data and also easy chaining
+            if (!emitter.isCancelled()) {
+                emitter.onNext(NOTHING);
+            }
+        }, BackpressureStrategy.LATEST);
+    }
+
+    /**
+     * Helper method used by generated code to bind a Callable such that it will be run in
+     * our disk io thread and will automatically block null values since RxJava3 does not like null.
+     *
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public static <T> Flowable<T> createFlowable(@NonNull final RoomDatabase database,
+            final boolean inTransaction, @NonNull final String[] tableNames,
+            @NonNull final Callable<T> callable) {
+        Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction));
+        final Maybe<T> maybe = Maybe.fromCallable(callable);
+        return createFlowable(database, tableNames)
+                .subscribeOn(scheduler)
+                .unsubscribeOn(scheduler)
+                .observeOn(scheduler)
+                .flatMapMaybe((Function<Object, MaybeSource<T>>) o -> maybe);
+    }
+
+    /**
+     * Creates a {@link Observable} that emits at least once and also re-emits whenever one of the
+     * observed tables is updated.
+     * <p>
+     * You can easily chain a database operation to downstream of this {@link Observable} to ensure
+     * that it re-runs when database is modified.
+     * <p>
+     * Since database invalidation is batched, multiple changes in the database may results in just
+     * 1 emission.
+     *
+     * @param database   The database instance
+     * @param tableNames The list of table names that should be observed
+     * @return A {@link Observable} which emits {@link #NOTHING} when one of the observed tables
+     * is modified (also once when the invalidation tracker connection is established).
+     */
+    @NonNull
+    public static Observable<Object> createObservable(@NonNull final RoomDatabase database,
+            @NonNull final String... tableNames) {
+        return Observable.create(emitter -> {
+            final InvalidationTracker.Observer observer = new InvalidationTracker.Observer(
+                    tableNames) {
+                @Override
+                public void onInvalidated(@NonNull Set<String> tables) {
+                    emitter.onNext(NOTHING);
+                }
+            };
+            database.getInvalidationTracker().addObserver(observer);
+            emitter.setDisposable(Disposable.fromAction(
+                    () -> database.getInvalidationTracker().removeObserver(observer)));
+
+            // emit once to avoid missing any data and also easy chaining
+            emitter.onNext(NOTHING);
+        });
+    }
+
+    /**
+     * Helper method used by generated code to bind a Callable such that it will be run in
+     * our disk io thread and will automatically block null values since RxJava3 does not like null.
+     *
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public static <T> Observable<T> createObservable(@NonNull final RoomDatabase database,
+            final boolean inTransaction, @NonNull final String[] tableNames,
+            @NonNull final Callable<T> callable) {
+        Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction));
+        final Maybe<T> maybe = Maybe.fromCallable(callable);
+        return createObservable(database, tableNames)
+                .subscribeOn(scheduler)
+                .unsubscribeOn(scheduler)
+                .observeOn(scheduler)
+                .flatMapMaybe(o -> maybe);
+    }
+
+    /**
+     * Helper method used by generated code to create a Single from a Callable that will ignore
+     * the EmptyResultSetException if the stream is already disposed.
+     *
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public static <T> Single<T> createSingle(@NonNull final Callable<T> callable) {
+        return Single.create(emitter -> {
+            try {
+                emitter.onSuccess(callable.call());
+            } catch (EmptyResultSetException e) {
+                emitter.tryOnError(e);
+            }
+        });
+    }
+
+    private static Executor getExecutor(@NonNull RoomDatabase database, boolean inTransaction) {
+        if (inTransaction) {
+            return database.getTransactionExecutor();
+        } else {
+            return database.getQueryExecutor();
+        }
+    }
+
+    private RxRoom() {
+    }
+}

+ 1 - 0
rxjava/.gitignore

@@ -0,0 +1 @@
+/build

+ 32 - 0
rxjava/build.gradle

@@ -0,0 +1,32 @@
+plugins {
+    id 'com.android.library'
+}
+apply from: 'publish.gradle'
+
+android {
+    compileSdk 32
+
+    defaultConfig {
+        minSdk 21
+        targetSdk 32
+
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    api("plus.reactivex.rxjava3:rxjava:3.1.6-SNAPSHOT")
+    api("plus.reactivex.rxjava3:rxandroid:3.0.2-SNAPSHOT")
+    compileOnly project(":common")
+}

+ 4 - 0
rxjava/config.gradle

@@ -0,0 +1,4 @@
+ext {
+    atmob_rxjava_version_code = 2
+    atmob_rxjava_version_name = "1.1.0-SNAPSHOT"
+}

+ 2 - 0
rxjava/consumer-rules.pro

@@ -0,0 +1,2 @@
+-dontshrink
+-dontwarn java.util.concurrent.Flow*

+ 23 - 0
rxjava/proguard-rules.pro

@@ -0,0 +1,23 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-dontwarn java.util.concurrent.Flow*

+ 59 - 0
rxjava/publish.gradle

@@ -0,0 +1,59 @@
+apply plugin: 'maven-publish'
+apply from: 'config.gradle'
+
+task androidJavadocs(type: Javadoc) {
+    source = android.sourceSets.main.java.sourceFiles
+    ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar"
+    classpath += files(ext.androidJar)
+}
+
+task androidJavadocsJar(type: Jar) {
+    getArchiveClassifier().set("javadoc")
+    from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+    getArchiveClassifier().set("sources")
+    from android.sourceSets.main.java.srcDirs
+}
+
+
+String ver = "$ext.atmob_rxjava_version_name"
+
+String publishUrl = !ver.endsWith("-SNAPSHOT") ? "$atmob_maven_url/repository/android-release/"
+        : "$atmob_maven_url/repository/android-snapshot/"
+
+
+String GROUP_ID = "extra.common"
+String ARTIFACT_ID = "rxjava"
+
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            push(MavenPublication) {
+                from components.release
+
+                groupId = GROUP_ID
+                artifactId = ARTIFACT_ID
+                version = ver
+
+                artifact androidSourcesJar
+                artifact androidJavadocsJar
+            }
+
+        }
+        repositories {
+            maven {
+                name = "nexus"
+                allowInsecureProtocol true
+                credentials {
+                    username = "$atmob_maven_username"
+                    password = "$atmob_maven_password"
+                }
+                url = publishUrl
+            }
+        }
+    }
+}

+ 5 - 0
rxjava/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.rxjava">
+
+</manifest>

+ 221 - 0
rxjava/src/main/java/atmob/rxjava/utils/RxJavaUtil.java

@@ -0,0 +1,221 @@
+package atmob.rxjava.utils;
+
+import android.view.View;
+
+import org.reactivestreams.Publisher;
+
+import atmob.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import atmob.reactivex.rxjava3.core.Completable;
+import atmob.reactivex.rxjava3.core.CompletableTransformer;
+import atmob.reactivex.rxjava3.core.Flowable;
+import atmob.reactivex.rxjava3.core.Observable;
+import atmob.reactivex.rxjava3.core.ObservableTransformer;
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.reactivex.rxjava3.core.SingleTransformer;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import atmob.reactivex.rxjava3.functions.Action;
+import atmob.reactivex.rxjava3.functions.Consumer;
+import atmob.reactivex.rxjava3.functions.Function;
+import atmob.reactivex.rxjava3.functions.Predicate;
+import atmob.reactivex.rxjava3.schedulers.Schedulers;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+public class RxJavaUtil {
+    private RxJavaUtil() {
+
+    }
+
+    public static class ObservableSchedule {
+        public static <T> ObservableTransformer<T, T> io2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static <T> ObservableTransformer<T, T> ioOnly() {
+            return upstream -> upstream.subscribeOn(Schedulers.io());
+        }
+
+        public static <T> ObservableTransformer<T, T> subThread2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static <T> ObservableTransformer<T, T> computation2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static <T> ObservableTransformer<T, T> subThreadOnly() {
+            return upstream -> upstream.subscribeOn(Schedulers.newThread());
+        }
+    }
+
+    public static class SingleSchedule {
+        public static <T> SingleTransformer<T, T> io2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static <T> SingleTransformer<T, T> ioOnly() {
+            return upstream -> upstream.subscribeOn(Schedulers.io());
+        }
+
+        public static <T> SingleTransformer<T, T> subThread2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static <T> SingleTransformer<T, T> computation2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static <T> SingleTransformer<T, T> subThreadOnly() {
+            return upstream -> upstream.subscribeOn(Schedulers.newThread());
+        }
+    }
+
+    public static class CompletableSchedule {
+        public static CompletableTransformer computation2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.computation()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static CompletableTransformer ioOnly() {
+            return upstream -> upstream.subscribeOn(Schedulers.io());
+        }
+
+        public static CompletableTransformer subThreadOnly() {
+            return upstream -> upstream.subscribeOn(Schedulers.newThread());
+        }
+
+        public static CompletableTransformer subThread2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread());
+        }
+
+        public static CompletableTransformer io2Main() {
+            return upstream -> upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
+        }
+    }
+
+    /**
+     * 将长按事件{@link View.OnLongClickListener}转换成{@link Observable}
+     *
+     * @param view 绑定长按时间的View
+     * @return 可观察数据流
+     */
+    public static ViewLongClickObservable longClick(View view) {
+        return ViewLongClickObservable.longClick(view);
+    }
+
+    /**
+     * 将点击事件{@link View.OnClickListener}转换成{@link Observable}
+     *
+     * @param view 绑定点击事件的View
+     * @return 可观察数据流
+     */
+    public static ViewClickObservable click(View view) {
+        return ViewClickObservable.onClick(view);
+    }
+
+    /**
+     * @param maxReyTimes   最大重试次数
+     * @param retryInterval 重试间隔时间
+     * @param timeUnit      重试间隔时间的单位
+     * @param predicate     重试的条件
+     * @return Function
+     */
+    public static Function<Flowable<Throwable>, Publisher<?>> retryWhen(
+            Predicate<? super Throwable> predicate,
+            int maxReyTimes,
+            long retryInterval,
+            TimeUnit timeUnit
+    ) {
+        return new Function<Flowable<Throwable>, Publisher<?>>() {
+
+            int currentRetryTimes;
+
+            @Override
+            public Publisher<?> apply(Flowable<Throwable> throwableFlowable) {
+                return throwableFlowable.flatMap((Function<Throwable, Publisher<?>>) throwable -> {
+                    if (predicate != null && !predicate.test(throwable)) {
+                        return Flowable.error(throwable);
+                    }
+                    if (++currentRetryTimes > maxReyTimes) {
+                        return Flowable.error(throwable);
+                    }
+                    return Flowable.timer(retryInterval, timeUnit);
+                });
+            }
+        };
+    }
+
+    /**
+     * 倒计时
+     *
+     * @param delay      倒计时时间
+     * @param timeUnit   时间单位
+     * @param onComplete 倒计时结束后回调
+     * @return 终止倒计时Disposable
+     */
+    public static Disposable timer(long delay, TimeUnit timeUnit, Action onComplete) {
+        if (onComplete == null) {
+            return Disposable.disposed();
+        }
+        return Completable.timer(delay, timeUnit)
+                .compose(CompletableSchedule.computation2Main())
+                .subscribe(onComplete, Throwable::printStackTrace);
+    }
+
+    /**
+     * 周期性
+     *
+     * @param period     时间间隔
+     * @param until      直到啥时候结束, 传 0 则不结束
+     * @param timeUnit   时间单位
+     * @param consumer   每一个周期回调
+     * @param onComplete 结束时回调
+     */
+    public static Disposable interval(long initialDelay, long period, long until, TimeUnit timeUnit, Consumer<Long> consumer, Action onComplete) {
+        return Observable.interval(initialDelay, period, timeUnit)
+                .takeUntil(aLong -> until != 0 && aLong * period >= until)
+                .compose(ObservableSchedule.computation2Main())
+                .subscribe(
+                        aLong -> {
+                            if (consumer != null) {
+                                consumer.accept(aLong);
+                            }
+                        },
+                        Throwable::printStackTrace,
+                        () -> {
+                            if (onComplete != null) {
+                                onComplete.run();
+                            }
+                        }
+                );
+    }
+
+    public static Disposable doInBackground(Runnable task, Action onComplete) {
+        if (task == null) {
+            return Disposable.disposed();
+        }
+        return Completable.fromRunnable(task)
+                .compose(CompletableSchedule.subThread2Main())
+                .subscribe(() -> {
+                    if (onComplete != null) {
+                        onComplete.run();
+                    }
+                }, Throwable::printStackTrace);
+
+    }
+
+    public static <T> Disposable doInBackground(Callable<T> callable, Consumer<T> onSuccess, Consumer<Throwable> onError) {
+        return Single.fromCallable(callable)
+                .compose(SingleSchedule.subThread2Main())
+                .subscribe(t -> {
+                    if (onSuccess != null) {
+                        onSuccess.accept(t);
+                    }
+                }, throwable -> {
+                    throwable.printStackTrace();
+                    if (onError != null) {
+                        onError.accept(throwable);
+                    }
+                });
+    }
+}

+ 65 - 0
rxjava/src/main/java/atmob/rxjava/utils/ViewClickObservable.java

@@ -0,0 +1,65 @@
+package atmob.rxjava.utils;
+
+import android.view.View;
+
+import com.atmob.common.runtime.ProcessUtil;
+
+import atmob.reactivex.rxjava3.android.MainThreadDisposable;
+import atmob.reactivex.rxjava3.annotations.NonNull;
+import atmob.reactivex.rxjava3.core.Observer;
+
+import atmob.reactivex.rxjava3.core.Observable;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+
+public class ViewClickObservable extends Observable<Object> {
+
+    private final View view;
+
+    public ViewClickObservable(View view) {
+        this.view = view;
+    }
+
+    @Override
+    protected void subscribeActual(@NonNull Observer<? super Object> observer) {
+        if (!ProcessUtil.isMainThread()) {
+            observer.onSubscribe(Disposable.empty());
+            observer.onError(new IllegalStateException(
+                    "Expected to be called on the main thread but was "
+                            + Thread.currentThread().getName()
+            ));
+            return;
+        }
+        Listener listener = new Listener(view, observer);
+        observer.onSubscribe(listener);
+        view.setOnClickListener(listener);
+    }
+
+    private static class Listener extends MainThreadDisposable implements View.OnClickListener {
+
+        private final View view;
+        private final Observer<? super Object> observer;
+        private final Object o;
+
+        public Listener(View view, Observer<? super Object> observer) {
+            this.o = new Object();
+            this.view = view;
+            this.observer = observer;
+        }
+
+        @Override
+        public void onClick(View v) {
+            if (!isDisposed()) {
+                observer.onNext(o);
+            }
+        }
+
+        @Override
+        protected void onDispose() {
+            this.view.setOnClickListener(null);
+        }
+    }
+
+    public static ViewClickObservable onClick(View view) {
+        return new ViewClickObservable(view);
+    }
+}

+ 79 - 0
rxjava/src/main/java/atmob/rxjava/utils/ViewLongClickObservable.java

@@ -0,0 +1,79 @@
+package atmob.rxjava.utils;
+
+import android.view.View;
+
+import com.atmob.common.runtime.ProcessUtil;
+
+import java.util.concurrent.Callable;
+
+import atmob.reactivex.rxjava3.android.MainThreadDisposable;
+import atmob.reactivex.rxjava3.annotations.NonNull;
+import atmob.reactivex.rxjava3.core.Observable;
+import atmob.reactivex.rxjava3.core.Observer;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+
+public class ViewLongClickObservable extends Observable<Object> {
+
+    private final View view;
+    private final Callable<Boolean> handled;
+
+    private ViewLongClickObservable(View view, Callable<Boolean> handled) {
+        this.view = view;
+        this.handled = handled;
+    }
+
+    @Override
+    protected void subscribeActual(@NonNull Observer<? super Object> observer) {
+        if (!ProcessUtil.isMainThread()) {
+            observer.onSubscribe(Disposable.empty());
+            observer.onError(new IllegalStateException(
+                    "Expected to be called on the main thread but was "
+                            + Thread.currentThread().getName()
+            ));
+            return;
+        }
+        Listener listener = new Listener(view, handled, observer);
+        observer.onSubscribe(listener);
+        view.setOnLongClickListener(listener);
+    }
+
+    private static class Listener extends MainThreadDisposable implements View.OnLongClickListener {
+
+        private final View view;
+        private final Callable<Boolean> handled;
+        private final Observer<? super Object> observer;
+        private final Object o;
+
+        public Listener(View view, Callable<Boolean> handled, Observer<? super Object> observer) {
+            this.o = new Object();
+            this.view = view;
+            this.handled = handled;
+            this.observer = observer;
+        }
+
+        @Override
+        public boolean onLongClick(View v) {
+            if (!isDisposed()) {
+                try {
+                    if (handled.call()) {
+                        observer.onNext(o);
+                        return true;
+                    }
+                } catch (Exception e) {
+                    observer.onError(e);
+                    dispose();
+                }
+            }
+            return false;
+        }
+
+        @Override
+        protected void onDispose() {
+            view.setOnClickListener(null);
+        }
+    }
+
+    public static ViewLongClickObservable longClick(@NonNull View view) {
+        return new ViewLongClickObservable(view, () -> true);
+    }
+}

+ 31 - 0
settings.gradle

@@ -0,0 +1,31 @@
+pluginManagement {
+    repositories {
+        gradlePluginPortal()
+        google()
+        mavenCentral()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+
+        maven {
+            allowInsecureProtocol = true
+            credentials {
+                username "$atmob_maven_username"
+                password "$atmob_maven_password"
+            }
+            url "$atmob_maven_url/repository/android-group/"
+        }
+    }
+}
+rootProject.name = "xm-common"
+include ':app'
+
+include ':network'
+include ':common'
+include ':rxjava'
+include ':user'
+include ':room-rx'

+ 1 - 0
user/.gitignore

@@ -0,0 +1 @@
+/build

+ 32 - 0
user/build.gradle

@@ -0,0 +1,32 @@
+plugins {
+    id 'com.android.library'
+}
+apply from: 'publish.gradle'
+
+android {
+    compileSdk 32
+
+    defaultConfig {
+        minSdk 21
+        targetSdk 32
+
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    compileOnly project(":common")
+    compileOnly 'com.google.code.gson:gson:+'
+    compileOnly 'androidx.annotation:annotation:+'
+}

+ 4 - 0
user/config.gradle

@@ -0,0 +1,4 @@
+ext {
+    atmob_user_version_code = 2
+    atmob_user_version_name = "1.0.1"
+}

+ 0 - 0
user/consumer-rules.pro


+ 22 - 0
user/proguard-rules.pro

@@ -0,0 +1,22 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+-dontshrink

+ 53 - 0
user/publish.gradle

@@ -0,0 +1,53 @@
+apply plugin: 'maven-publish'
+apply from: 'config.gradle'
+
+//打包main目录下代码和资源的 task
+task androidSourcesJar(type: Jar) {
+    classifier = 'sources'
+    from android.sourceSets.main.java.srcDirs
+}
+
+String GROUP_ID = "extra.common"
+String ARTIFACT_ID = "user"
+String versionName = "$ext.atmob_user_version_name"
+
+String publishUrl
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            release(MavenPublication) {
+                // Applies the component for the release build variant. 
+                from components.release
+
+                // You can then customize attributes of the publication as shown below.
+                groupId = GROUP_ID
+                artifactId = ARTIFACT_ID
+                version = versionName
+                publishUrl = "${atmob_maven_url}/repository/android-release/"
+            }
+            // Creates a Maven publication called “debug”.
+            debug(MavenPublication) {
+                // Applies the component for the debug build variant.
+                from components.debug
+
+                groupId = GROUP_ID
+                artifactId = ARTIFACT_ID
+                version = "$versionName-SNAPSHOT"
+                publishUrl = "${atmob_maven_url}/repository/android-snapshot/"
+            }
+        }
+        repositories {
+            maven {
+                name = "nexus"
+                allowInsecureProtocol true
+                credentials {
+                    username = "$atmob_maven_username"
+                    password = "$atmob_maven_password"
+                }
+                url = publishUrl
+            }
+        }
+    }
+}

+ 5 - 0
user/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.user">
+
+</manifest>

+ 126 - 0
user/src/main/java/com/atmob/user/AtmobUser.java

@@ -0,0 +1,126 @@
+package com.atmob.user;
+
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+
+import com.atmob.common.data.KVUtils;
+import com.atmob.common.text.TextProguard;
+import com.atmob.user.sm.SmAntiFraudHelper;
+import com.atmob.user.strategy.ComplianceStrategy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class AtmobUser {
+
+    private static final String PREFIX = TextProguard.encode("atmob_user");
+
+    private static final String KEY_POLICY_GRANTED = PREFIX + TextProguard.encode("policy_granted");
+
+    private volatile static boolean isPolicyGranted;
+
+    private static ComplianceStrategy strategy;
+
+    private static String atmobChannel;
+
+    private static int atmobAppId;
+
+    private static int atmobTgPlatformId;
+
+    static {
+        isPolicyGranted = KVUtils.getDefault().getBoolean(KEY_POLICY_GRANTED, false);
+    }
+
+    private AtmobUser() {
+
+    }
+
+    public static final int CHINA = 1;
+    public static final int GLOBAL = 2;
+
+    @IntDef({
+            CHINA,
+            GLOBAL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Strategy {
+
+    }
+
+    public static void init(@Strategy int complianceStrategy,
+                            String atmobChannel, int atmobAppId, int atmobTgPlatformId) {
+        AtmobUser.atmobChannel = atmobChannel;
+        AtmobUser.atmobAppId = atmobAppId;
+        AtmobUser.atmobTgPlatformId = atmobTgPlatformId;
+        AtmobUser.strategy = ComplianceStrategy.getStrategy(complianceStrategy);
+
+        if (isPolicyGranted) {
+            SmAntiFraudHelper.init();
+        }
+    }
+
+    public static boolean isPolicyGranted() {
+        return isPolicyGranted;
+    }
+
+    public synchronized static void recordPolicyGrant(boolean grant) {
+        isPolicyGranted = grant;
+        KVUtils.getDefault().putBoolean(KEY_POLICY_GRANTED, grant);
+        if (grant) {
+            SmAntiFraudHelper.init();
+        }
+    }
+
+    public static String getMacCode() {
+        return strategy.getMacCode();
+    }
+
+    public static String getWifiName() {
+        return strategy.getWifiName();
+    }
+
+    public static String getLocationAddress() {
+        return strategy.getLocationAddress();
+    }
+
+    public static String getIPAddress() {
+        return strategy.getIPAddress();
+    }
+
+    public static String getAndroidId() {
+        return strategy.getAndroidId();
+    }
+
+    public static String getOaId() {
+        return strategy.getOaId();
+    }
+
+    public static String getImei() {
+        return strategy.getImei();
+    }
+
+    public static String getImei(int slotIndex) {
+        return strategy.getImei(slotIndex);
+    }
+
+    public static String getAtmobChannel() {
+        return atmobChannel;
+    }
+
+    public static int getAtmobAppId() {
+        return atmobAppId;
+    }
+
+    public static int getAtmobTgPlatformId() {
+        return atmobTgPlatformId;
+    }
+
+    public static String getSmDeviceId() {
+        return strategy.getSmDeviceId();
+    }
+
+    public static String getAaid() {
+        return strategy.getAaid();
+    }
+}

+ 111 - 0
user/src/main/java/com/atmob/user/param/AtmobParams.java

@@ -0,0 +1,111 @@
+package com.atmob.user.param;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import com.atmob.common.runtime.ContextUtil;
+import com.atmob.user.AtmobUser;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * 通用请求参数
+ */
+public class AtmobParams {
+
+    /* exactly no compliance problem */
+    @SerializedName("packageName")
+    private final String packageName;
+    @SerializedName("channelName")
+    private final String channelName;
+    @SerializedName("appId")
+    private final int appId;
+    @SerializedName("tgPlatform")
+    private final int tgPlatform;
+    @SerializedName("appVersionName")
+    private String appVersionName = "";
+    @SerializedName("appVersionCode")
+    private String appVersionCode = "";
+    @SerializedName("osVersion")
+    private final String osVersion;
+    @SerializedName("brand")
+    private final String brand;
+    @SerializedName("model")
+    private final String model;
+    @SerializedName("androidId")
+    private final String androidId;
+    @SerializedName("aaid")
+    private final String aaid;
+    /* exactly no compliance problem */
+
+    /* may be has compliance problem */
+    @SerializedName("imei")
+    private final String imei;
+    @SerializedName("simImei0")
+    private final String simImei0;
+    @SerializedName("simImei1")
+    private final String simImei1;
+    @SerializedName("oaid")
+    private final String oaid;
+    @SerializedName("nIp")
+    private final String nIp;
+    @SerializedName("mac")
+    private final String mac;
+    @SerializedName("wifiName")
+    private final String wifiName;
+    @SerializedName("region")
+    private final String region;
+    @SerializedName("sm")
+    private final String sm;
+    /* may be has compliance problem */
+
+    public AtmobParams() {
+        Context context = ContextUtil.getContext();
+        packageName = context.getPackageName();
+        try {
+            appVersionName = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
+            String appVersionCodeTemp = appVersionName.replaceAll("\\.", "");
+            appVersionCode = appVersionCodeTemp.length() < 3 ? appVersionCodeTemp + "0" : appVersionCodeTemp;
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        channelName = AtmobUser.getAtmobChannel();
+        appId = AtmobUser.getAtmobAppId();
+        tgPlatform = AtmobUser.getAtmobTgPlatformId();
+        osVersion = Build.VERSION.RELEASE;
+        brand = Build.BRAND;
+        model = Build.MODEL;
+
+        androidId = AtmobUser.getAndroidId();
+        aaid = AtmobUser.getAaid();
+        imei = AtmobUser.getImei();
+        simImei0 = AtmobUser.getImei(0);
+        simImei1 = AtmobUser.getImei(1);
+        oaid = AtmobUser.getOaId();
+        nIp = AtmobUser.getIPAddress();
+        mac = AtmobUser.getMacCode();
+        wifiName = AtmobUser.getWifiName();
+        region = AtmobUser.getLocationAddress();
+        sm = AtmobUser.getSmDeviceId();
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "AtmobParams{" +
+                "packageName='" + packageName + '\'' +
+                ", channelName='" + channelName + '\'' +
+                ", appId=" + appId +
+                ", tgPlatform=" + tgPlatform +
+                ", appVersionName='" + appVersionName + '\'' +
+                ", appVersionCode='" + appVersionCode + '\'' +
+                ", androidId='" + androidId + '\'' +
+                ", aaid='" + aaid + '\'' +
+                ", imei='" + imei + '\'' +
+                ", oaid='" + oaid + '\'' +
+                ", region='" + region + '\'' +
+                '}';
+    }
+}

File diff suppressed because it is too large
+ 80 - 0
user/src/main/java/com/atmob/user/sm/SmAntiFraudHelper.java


+ 76 - 0
user/src/main/java/com/atmob/user/strategy/ChinaStrategy.java

@@ -0,0 +1,76 @@
+package com.atmob.user.strategy;
+
+import com.atmob.common.device.DeviceInfoUtil;
+import com.atmob.common.device.IPUtil;
+import com.atmob.common.device.MacUtils;
+import com.atmob.user.sm.SmAntiFraudHelper;
+
+final class ChinaStrategy implements ComplianceStrategy {
+
+    private static volatile ChinaStrategy INSTANCE;
+
+    private ChinaStrategy() {
+
+    }
+
+    public static ComplianceStrategy getInstance() {
+        if (INSTANCE == null) {
+            synchronized (ChinaStrategy.class) {
+                if (INSTANCE == null) {
+                    INSTANCE = new ChinaStrategy();
+                }
+            }
+        }
+        return INSTANCE;
+    }
+
+    @Override
+    public String getMacCode() {
+        return MacUtils.getMac();
+    }
+
+    @Override
+    public String getWifiName() {
+        return IPUtil.getWifiName();
+    }
+
+    @Override
+    public String getLocationAddress() {
+        return null;
+    }
+
+    @Override
+    public String getIPAddress() {
+        return IPUtil.getIpV4();
+    }
+
+    @Override
+    public String getAndroidId() {
+        return DeviceInfoUtil.getAndroidId();
+    }
+
+    @Override
+    public String getOaId() {
+        return DeviceInfoUtil.getOaid();
+    }
+
+    @Override
+    public String getImei() {
+        return DeviceInfoUtil.getImei();
+    }
+
+    @Override
+    public String getImei(int slotIndex) {
+        return DeviceInfoUtil.getSimImei(slotIndex);
+    }
+
+    @Override
+    public String getSmDeviceId() {
+        return SmAntiFraudHelper.getSmDeviceId();
+    }
+
+    @Override
+    public String getAaid() {
+        return null;
+    }
+}

+ 38 - 0
user/src/main/java/com/atmob/user/strategy/ComplianceStrategy.java

@@ -0,0 +1,38 @@
+package com.atmob.user.strategy;
+
+import static com.atmob.user.AtmobUser.CHINA;
+import static com.atmob.user.AtmobUser.GLOBAL;
+
+import com.atmob.user.AtmobUser;
+
+public interface ComplianceStrategy {
+    static ComplianceStrategy getStrategy(@AtmobUser.Strategy int strategy) {
+        if (strategy == CHINA) {
+            return ChinaStrategy.getInstance();
+        } else if (strategy == GLOBAL) {
+            return GlobalStrategy.getInstance();
+        } else {
+            throw new IllegalArgumentException("Unsupported strategy code(" + strategy + ").");
+        }
+    }
+
+    String getMacCode();
+
+    String getWifiName();
+
+    String getLocationAddress();
+
+    String getIPAddress();
+
+    String getAndroidId();
+
+    String getOaId();
+
+    String getImei();
+
+    String getImei(int slotIndex);
+
+    String getSmDeviceId();
+
+    String getAaid();
+}

+ 0 - 0
user/src/main/java/com/atmob/user/strategy/GlobalStrategy.java


Some files were not shown because too many files changed in this diff