詹子聪 5 rokov pred
rodič
commit
b4a59e5339
74 zmenil súbory, kde vykonal 4323 pridanie a 9 odobranie
  1. 7 0
      app/build.gradle
  2. 5 9
      app/src/main/res/layout/activity_main.xml
  3. 76 0
      common/build.gradle
  4. 21 0
      common/proguard-rules.pro
  5. 27 0
      common/src/androidTest/java/com/miekir/common/ExampleInstrumentedTest.java
  6. 22 0
      common/src/main/AndroidManifest.xml
  7. 31 0
      common/src/main/java/com/miekir/common/adapter/TabFragmentAdapter.java
  8. 59 0
      common/src/main/java/com/miekir/common/provider/CommonInstaller.java
  9. 30 0
      common/src/main/java/com/miekir/common/utils/ActivityTool.java
  10. 978 0
      common/src/main/java/com/miekir/common/utils/Base64.java
  11. 39 0
      common/src/main/java/com/miekir/common/utils/ContextManager.java
  12. 60 0
      common/src/main/java/com/miekir/common/utils/LogTool.java
  13. 16 0
      common/src/main/java/com/miekir/common/utils/SizeTool.java
  14. 72 0
      common/src/main/java/com/miekir/common/utils/ToastTool.java
  15. 76 0
      common/src/main/java/com/miekir/common/utils/ViewTool.java
  16. 141 0
      common/src/main/java/com/miekir/common/view/AlignTextView.java
  17. 237 0
      common/src/main/java/com/miekir/common/view/RoundedCornersTransformation.java
  18. 45 0
      common/src/main/java/com/miekir/common/widget/ScrollableViewPager.java
  19. 3 0
      common/src/main/res/values/strings.xml
  20. 5 0
      common/src/main/res/xml/network.xml
  21. 4 0
      common/src/main/res/xml/provider_paths.xml
  22. 17 0
      common/src/test/java/com/miekir/common/ExampleUnitTest.java
  23. 50 0
      mvp/build.gradle
  24. 21 0
      mvp/proguard-rules.pro
  25. 27 0
      mvp/src/androidTest/java/com/miekir/mvp/ExampleInstrumentedTest.java
  26. 11 0
      mvp/src/main/AndroidManifest.xml
  27. 19 0
      mvp/src/main/java/com/miekir/mvp/ProxyHandler.java
  28. 150 0
      mvp/src/main/java/com/miekir/mvp/base/BaseActivity.java
  29. 136 0
      mvp/src/main/java/com/miekir/mvp/base/BaseFragment.java
  30. 16 0
      mvp/src/main/java/com/miekir/mvp/base/NeedLoading.java
  31. 13 0
      mvp/src/main/java/com/miekir/mvp/constant/MvpRepository.java
  32. 36 0
      mvp/src/main/java/com/miekir/mvp/model/BaseModel.java
  33. 20 0
      mvp/src/main/java/com/miekir/mvp/model/IModel.java
  34. 17 0
      mvp/src/main/java/com/miekir/mvp/model/InjectModel.java
  35. 127 0
      mvp/src/main/java/com/miekir/mvp/presenter/BasePresenter.java
  36. 20 0
      mvp/src/main/java/com/miekir/mvp/presenter/InjectPresenter.java
  37. 60 0
      mvp/src/main/java/com/miekir/mvp/presenter/PresenterHelper.java
  38. 33 0
      mvp/src/main/java/com/miekir/mvp/view/BaseMvpActivity.java
  39. 76 0
      mvp/src/main/java/com/miekir/mvp/view/BaseMvpFragment.java
  40. 6 0
      mvp/src/main/java/com/miekir/mvp/view/IView.java
  41. 44 0
      mvp/src/main/java/com/miekir/mvp/view/ViewHandler.java
  42. 54 0
      mvp/src/main/java/com/miekir/mvp/view/ViewHelper.java
  43. 130 0
      mvp/src/main/java/com/miekir/mvp/widget/CircularRingView.java
  44. 97 0
      mvp/src/main/java/com/miekir/mvp/widget/LoadingView.java
  45. 12 0
      mvp/src/main/res/drawable/shape_dialog_bg.xml
  46. 34 0
      mvp/src/main/res/layout/dialog_loading_view.xml
  47. 3 0
      mvp/src/main/res/values/strings.xml
  48. 47 0
      mvp/src/main/res/values/styles.xml
  49. 17 0
      mvp/src/test/java/com/miekir/mvp/ExampleUnitTest.java
  50. 82 0
      network/build.gradle
  51. 21 0
      network/proguard-rules.pro
  52. 27 0
      network/src/androidTest/java/com/miekir/network/ExampleInstrumentedTest.java
  53. 6 0
      network/src/main/AndroidManifest.xml
  54. 184 0
      network/src/main/java/com/miekir/network/RetrofitHelper.java
  55. 9 0
      network/src/main/java/com/miekir/network/base/BaseBean.java
  56. 55 0
      network/src/main/java/com/miekir/network/base/BaseResponse.java
  57. 17 0
      network/src/main/java/com/miekir/network/constant/Code.java
  58. 16 0
      network/src/main/java/com/miekir/network/constant/RequestConst.java
  59. 46 0
      network/src/main/java/com/miekir/network/utils/ExceptionUtil.java
  60. 44 0
      network/src/main/java/com/miekir/network/utils/JsonTool.java
  61. 42 0
      network/src/main/java/com/miekir/network/widget/CustomProgressDialog.java
  62. 66 0
      network/src/main/java/com/miekir/network/widget/observe/MvpObserver.java
  63. 78 0
      network/src/main/java/com/miekir/network/widget/observe/NetMvpObserver.java
  64. 19 0
      network/src/main/java/com/miekir/network/widget/observe/cancelable/CancelableConsumer.java
  65. 70 0
      network/src/main/java/com/miekir/network/widget/observe/cancelable/CancelableObserver.java
  66. 83 0
      network/src/main/java/com/miekir/network/widget/observe/cancelable/NetCancelableObserver.java
  67. 67 0
      network/src/main/java/com/miekir/network/widget/observe/sticky/NetStickyObserver.java
  68. 63 0
      network/src/main/java/com/miekir/network/widget/observe/sticky/ProgressNetStickyObserver.java
  69. 8 0
      network/src/main/res/drawable/bg_dialog_net_loading.xml
  70. 31 0
      network/src/main/res/layout/dialog_net_loading.xml
  71. 3 0
      network/src/main/res/values/strings.xml
  72. 19 0
      network/src/main/res/values/styles.xml
  73. 17 0
      network/src/test/java/com/miekir/network/ExampleUnitTest.java
  74. 3 0
      settings.gradle

+ 7 - 0
app/build.gradle

@@ -67,4 +67,11 @@ dependencies {
     compile 'org.aspectj:aspectjrt:1.8.+'
     // 动态权限申请firefly1126/android_permission_aspectjx
     compile 'com.firefly1126.permissionaspect:permissionaspect:1.0.1'
+    implementation project(path: ':mvp')
+    implementation project(path: ':network')
+    implementation project(path: ':common')
+}
+
+aspectjx {
+    exclude  "android.support",'androidx','com.google','com.squareup.leakcanary','com.squareup.leakcanary.core','com.alipay','org.apache','com.tencent'
 }

+ 5 - 9
app/src/main/res/layout/activity_main.xml

@@ -1,18 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity">
 
-    <TextView
-        android:layout_width="wrap_content"
+    <EditText
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:text="Hello World!"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintLeft_toLeftOf="parent"
-        app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        android:inputType="number"/>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>

+ 76 - 0
common/build.gradle

@@ -0,0 +1,76 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+
+    defaultConfig {
+        minSdkVersion versions.minSdk
+        targetSdkVersion versions.targetSdk
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        debug {
+            minifyEnabled false
+            buildConfigField("boolean", "IS_DEBUG", 'true')
+        }
+
+        release {
+            minifyEnabled false
+            buildConfigField("boolean", "IS_DEBUG", 'false')
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+    //api "androidx.core:core:1.5.0-alpha05"
+    //api 'androidx.appcompat:appcompat:1.3.0-alpha02'
+    //api 'com.google.android.material:material:1.2.1'
+    //api 'androidx.vectordrawable:vectordrawable:1.1.0'
+    //api 'androidx.navigation:navigation-fragment:2.3.1'
+    //api 'androidx.navigation:navigation-ui:2.3.1'
+    //api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+
+    api 'androidx.appcompat:appcompat:1.3.0-alpha02'
+
+    // 防止Retrofit内存泄露
+    api 'com.trello.rxlifecycle3:rxlifecycle:3.1.0'
+    api 'com.trello.rxlifecycle3:rxlifecycle-android:3.1.0'
+    api 'com.trello.rxlifecycle3:rxlifecycle-components:3.1.0'
+
+    // 带行号的Log
+    api 'com.github.zhaokaiqiang.klog:library:1.6.0'
+
+    // 查看内存泄露
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
+
+    // 漂亮的TabLayout
+    api 'com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar'
+
+    // Glide加载图片
+    api 'com.github.bumptech.glide:glide:4.11.0'
+    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+
+    // guava
+    api("com.google.guava:guava:30.0-android")
+
+    // 适配器
+    //api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.50'
+
+    // 动态代理
+    //api group: 'cglib', name: 'cglib', version: '3.3.0'
+
+}

+ 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

+ 27 - 0
common/src/androidTest/java/com/miekir/common/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.miekir.common;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.itant.common.test", appContext.getPackageName());
+    }
+}

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

@@ -0,0 +1,22 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.miekir.common" >
+    <application
+        android:networkSecurityConfig="@xml/network"
+        android:requestLegacyExternalStorage="true">
+        <!--authorities需要是唯一的,随着包名变化而变化,否则一个手机只能有一个项目引入这个库-->
+        <provider
+            android:name=".provider.CommonInstaller"
+            android:authorities="${applicationId}.common"
+            android:exported="false"/>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.provider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/provider_paths" />
+        </provider>
+    </application>
+</manifest>

+ 31 - 0
common/src/main/java/com/miekir/common/adapter/TabFragmentAdapter.java

@@ -0,0 +1,31 @@
+package com.miekir.common.adapter;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+
+import java.util.List;
+
+/**
+ * Created by Jason on 2018/8/20.
+ * 在Fragment里使用ViewPager的适配器
+ */
+
+public class TabFragmentAdapter extends FragmentPagerAdapter {
+
+    private List<Fragment> tabFragments;
+    public TabFragmentAdapter(FragmentManager fm, List<Fragment> fragments) {
+        super(fm);
+        this.tabFragments = fragments;
+    }
+
+    @Override
+    public Fragment getItem(int position) {
+        return tabFragments == null ? null : tabFragments.get(position);
+    }
+
+    @Override
+    public int getCount() {
+        return tabFragments == null ? 0 : tabFragments.size();
+    }
+}

+ 59 - 0
common/src/main/java/com/miekir/common/provider/CommonInstaller.java

@@ -0,0 +1,59 @@
+package com.miekir.common.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.miekir.common.utils.ContextManager;
+import com.miekir.common.utils.SizeTool;
+
+/**
+ *
+ * @author Miekir
+ * @date 2020/7/5 0:17
+ * Description: 免手动初始化
+ */
+public class CommonInstaller extends ContentProvider {
+    @Override
+    public boolean onCreate() {
+        if (getContext() != null) {
+            ContextManager.getInstance().initContext(getContext().getApplicationContext());
+            SizeTool.SCREEN_WIDTH = getContext().getResources().getDisplayMetrics().widthPixels;
+            SizeTool.SCREEN_HEIGHT = getContext().getResources().getDisplayMetrics().heightPixels;
+        }
+
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+        return 0;
+    }
+}

+ 30 - 0
common/src/main/java/com/miekir/common/utils/ActivityTool.java

@@ -0,0 +1,30 @@
+package com.miekir.common.utils;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/6/21 21:32
+ * Description: 关于Activity的工具
+ */
+public class ActivityTool {
+    private ActivityTool() {}
+
+    public static void openUrl(Activity activity, String webUrl) {
+        Intent intent = new Intent();
+        intent.setAction("android.intent.action.VIEW");
+        intent.addCategory("android.intent.category.DEFAULT");
+        Uri uri = Uri.parse(webUrl);
+        intent.setData(uri);
+        try {
+            activity.startActivity(intent);
+        } catch (Exception e) {
+            ToastTool.showShort("找不到支付通道");
+            e.printStackTrace();
+        }
+    }
+}

+ 978 - 0
common/src/main/java/com/miekir/common/utils/Base64.java

@@ -0,0 +1,978 @@
+package com.miekir.common.utils;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class consists exclusively of static methods for obtaining
+ * encoders and decoders for the Base64 encoding scheme. The
+ * implementation of this class supports the following types of Base64
+ * as specified in
+ * <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a> and
+ * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
+ *
+ * <ul>
+ * <li><a name="basic"><b>Basic</b></a>
+ * <p> Uses "The Base64 Alphabet" as specified in Table 1 of
+ *     RFC 4648 and RFC 2045 for encoding and decoding operation.
+ *     The encoder does not add any line feed (line separator)
+ *     character. The decoder rejects data that contains characters
+ *     outside the base64 alphabet.</p></li>
+ *
+ * <li><a name="url"><b>URL and Filename safe</b></a>
+ * <p> Uses the "URL and Filename safe Base64 Alphabet" as specified
+ *     in Table 2 of RFC 4648 for encoding and decoding. The
+ *     encoder does not add any line feed (line separator) character.
+ *     The decoder rejects data that contains characters outside the
+ *     base64 alphabet.</p></li>
+ *
+ * <li><a name="mime"><b>MIME</b></a>
+ * <p> Uses the "The Base64 Alphabet" as specified in Table 1 of
+ *     RFC 2045 for encoding and decoding operation. The encoded output
+ *     must be represented in lines of no more than 76 characters each
+ *     and uses a carriage return {@code '\r'} followed immediately by
+ *     a linefeed {@code '\n'} as the line separator. No line separator
+ *     is added to the end of the encoded output. All line separators
+ *     or other characters not found in the base64 alphabet table are
+ *     ignored in decoding operation.</p></li>
+ * </ul>
+ *
+ * <p> Unless otherwise noted, passing a {@code null} argument to a
+ * method of this class will cause a {@link java.lang.NullPointerException
+ * NullPointerException} to be thrown.
+ *
+ * @author  Xueming Shen
+ * @since   1.8
+ */
+
+public class Base64 {
+
+    private Base64() {}
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#basic">Basic</a> type base64 encoding scheme.
+     *
+     * @return  A Base64 encoder.
+     */
+    public static Encoder getEncoder() {
+         return Encoder.RFC4648;
+    }
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#url">URL and Filename safe</a> type base64
+     * encoding scheme.
+     *
+     * @return  A Base64 encoder.
+     */
+    public static Encoder getUrlEncoder() {
+         return Encoder.RFC4648_URLSAFE;
+    }
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#mime">MIME</a> type base64 encoding scheme.
+     *
+     * @return  A Base64 encoder.
+     */
+    public static Encoder getMimeEncoder() {
+        return Encoder.RFC2045;
+    }
+
+    /**
+     * Returns a {@link Encoder} that encodes using the
+     * <a href="#mime">MIME</a> type base64 encoding scheme
+     * with specified line length and line separators.
+     *
+     * @param   lineLength
+     *          the length of each output line (rounded down to nearest multiple
+     *          of 4). If {@code lineLength <= 0} the output will not be separated
+     *          in lines
+     * @param   lineSeparator
+     *          the line separator for each output line
+     *
+     * @return  A Base64 encoder.
+     *
+     * @throws  IllegalArgumentException if {@code lineSeparator} includes any
+     *          character of "The Base64 Alphabet" as specified in Table 1 of
+     *          RFC 2045.
+     */
+    public static Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) {
+         Objects.requireNonNull(lineSeparator);
+         int[] base64 = Decoder.fromBase64;
+         for (byte b : lineSeparator) {
+             if (base64[b & 0xff] != -1)
+                 throw new IllegalArgumentException(
+                     "Illegal base64 line separator character 0x" + Integer.toString(b, 16));
+         }
+         if (lineLength <= 0) {
+             return Encoder.RFC4648;
+         }
+         return new Encoder(false, lineSeparator, lineLength >> 2 << 2, true);
+    }
+
+    /**
+     * Returns a {@link Decoder} that decodes using the
+     * <a href="#basic">Basic</a> type base64 encoding scheme.
+     *
+     * @return  A Base64 decoder.
+     */
+    public static Decoder getDecoder() {
+         return Decoder.RFC4648;
+    }
+
+    /**
+     * Returns a {@link Decoder} that decodes using the
+     * <a href="#url">URL and Filename safe</a> type base64
+     * encoding scheme.
+     *
+     * @return  A Base64 decoder.
+     */
+    public static Decoder getUrlDecoder() {
+         return Decoder.RFC4648_URLSAFE;
+    }
+
+    /**
+     * Returns a {@link Decoder} that decodes using the
+     * <a href="#mime">MIME</a> type base64 decoding scheme.
+     *
+     * @return  A Base64 decoder.
+     */
+    public static Decoder getMimeDecoder() {
+         return Decoder.RFC2045;
+    }
+
+    /**
+     * This class implements an encoder for encoding byte data using
+     * the Base64 encoding scheme as specified in RFC 4648 and RFC 2045.
+     *
+     * <p> Instances of {@link Encoder} class are safe for use by
+     * multiple concurrent threads.
+     *
+     * <p> Unless otherwise noted, passing a {@code null} argument to
+     * a method of this class will cause a
+     * {@link java.lang.NullPointerException NullPointerException} to
+     * be thrown.
+     *
+     * @see     Decoder
+     * @since   1.8
+     */
+    public static class Encoder {
+
+        private final byte[] newline;
+        private final int linemax;
+        private final boolean isURL;
+        private final boolean doPadding;
+
+        private Encoder(boolean isURL, byte[] newline, int linemax, boolean doPadding) {
+            this.isURL = isURL;
+            this.newline = newline;
+            this.linemax = linemax;
+            this.doPadding = doPadding;
+        }
+
+        /**
+         * This array is a lookup table that translates 6-bit positive integer
+         * index values into their "Base64 Alphabet" equivalents as specified
+         * in "Table 1: The Base64 Alphabet" of RFC 2045 (and RFC 4648).
+         */
+        private static final char[] toBase64 = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
+        };
+
+        /**
+         * It's the lookup table for "URL and Filename safe Base64" as specified
+         * in Table 2 of the RFC 4648, with the '+' and '/' changed to '-' and
+         * '_'. This table is used when BASE64_URL is specified.
+         */
+        private static final char[] toBase64URL = {
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'
+        };
+
+        private static final int MIMELINEMAX = 76;
+        private static final byte[] CRLF = new byte[] {'\r', '\n'};
+
+        static final Encoder RFC4648 = new Encoder(false, null, -1, true);
+        static final Encoder RFC4648_URLSAFE = new Encoder(true, null, -1, true);
+        static final Encoder RFC2045 = new Encoder(false, CRLF, MIMELINEMAX, true);
+
+        private final int outLength(int srclen) {
+            int len = 0;
+            if (doPadding) {
+                len = 4 * ((srclen + 2) / 3);
+            } else {
+                int n = srclen % 3;
+                len = 4 * (srclen / 3) + (n == 0 ? 0 : n + 1);
+            }
+            if (linemax > 0)                                  // line separators
+                len += (len - 1) / linemax * newline.length;
+            return len;
+        }
+
+        /**
+         * Encodes all bytes from the specified byte array into a newly-allocated
+         * byte array using the {@link Base64} encoding scheme. The returned byte
+         * array is of the length of the resulting bytes.
+         *
+         * @param   src
+         *          the byte array to encode
+         * @return  A newly-allocated byte array containing the resulting
+         *          encoded bytes.
+         */
+        public byte[] encode(byte[] src) {
+            int len = outLength(src.length);          // dst array size
+            byte[] dst = new byte[len];
+            int ret = encode0(src, 0, src.length, dst);
+            if (ret != dst.length)
+                 return Arrays.copyOf(dst, ret);
+            return dst;
+        }
+
+        /**
+         * Encodes all bytes from the specified byte array using the
+         * {@link Base64} encoding scheme, writing the resulting bytes to the
+         * given output byte array, starting at offset 0.
+         *
+         * <p> It is the responsibility of the invoker of this method to make
+         * sure the output byte array {@code dst} has enough space for encoding
+         * all bytes from the input byte array. No bytes will be written to the
+         * output byte array if the output byte array is not big enough.
+         *
+         * @param   src
+         *          the byte array to encode
+         * @param   dst
+         *          the output byte array
+         * @return  The number of bytes written to the output byte array
+         *
+         * @throws  IllegalArgumentException if {@code dst} does not have enough
+         *          space for encoding all input bytes.
+         */
+        public int encode(byte[] src, byte[] dst) {
+            int len = outLength(src.length);         // dst array size
+            if (dst.length < len)
+                throw new IllegalArgumentException(
+                    "Output byte array is too small for encoding all input bytes");
+            return encode0(src, 0, src.length, dst);
+        }
+
+        /**
+         * Encodes the specified byte array into a String using the {@link Base64}
+         * encoding scheme.
+         *
+         * <p> This method first encodes all input bytes into a base64 encoded
+         * byte array and then constructs a new String by using the encoded byte
+         * array and the {@link java.nio.charset.StandardCharsets#ISO_8859_1
+         * ISO-8859-1} charset.
+         *
+         * <p> In other words, an invocation of this method has exactly the same
+         * effect as invoking
+         * {@code new String(encode(src), StandardCharsets.ISO_8859_1)}.
+         *
+         * @param   src
+         *          the byte array to encode
+         * @return  A String containing the resulting Base64 encoded characters
+         */
+        @SuppressWarnings("deprecation")
+        public String encodeToString(byte[] src) {
+            byte[] encoded = encode(src);
+            return new String(encoded, 0, 0, encoded.length);
+        }
+
+        /**
+         * Encodes all remaining bytes from the specified byte buffer into
+         * a newly-allocated ByteBuffer using the {@link Base64} encoding
+         * scheme.
+         *
+         * Upon return, the source buffer's position will be updated to
+         * its limit; its limit will not have been changed. The returned
+         * output buffer's position will be zero and its limit will be the
+         * number of resulting encoded bytes.
+         *
+         * @param   buffer
+         *          the source ByteBuffer to encode
+         * @return  A newly-allocated byte buffer containing the encoded bytes.
+         */
+        public ByteBuffer encode(ByteBuffer buffer) {
+            int len = outLength(buffer.remaining());
+            byte[] dst = new byte[len];
+            int ret = 0;
+            if (buffer.hasArray()) {
+                ret = encode0(buffer.array(),
+                              buffer.arrayOffset() + buffer.position(),
+                              buffer.arrayOffset() + buffer.limit(),
+                              dst);
+                buffer.position(buffer.limit());
+            } else {
+                byte[] src = new byte[buffer.remaining()];
+                buffer.get(src);
+                ret = encode0(src, 0, src.length, dst);
+            }
+            if (ret != dst.length)
+                 dst = Arrays.copyOf(dst, ret);
+            return ByteBuffer.wrap(dst);
+        }
+
+        /**
+         * Wraps an output stream for encoding byte data using the {@link Base64}
+         * encoding scheme.
+         *
+         * <p> It is recommended to promptly close the returned output stream after
+         * use, during which it will flush all possible leftover bytes to the underlying
+         * output stream. Closing the returned output stream will close the underlying
+         * output stream.
+         *
+         * @param   os
+         *          the output stream.
+         * @return  the output stream for encoding the byte data into the
+         *          specified Base64 encoded format
+         */
+        public OutputStream wrap(OutputStream os) {
+            Objects.requireNonNull(os);
+            return new EncOutputStream(os, isURL ? toBase64URL : toBase64,
+                                       newline, linemax, doPadding);
+        }
+
+        /**
+         * Returns an encoder instance that encodes equivalently to this one,
+         * but without adding any padding character at the end of the encoded
+         * byte data.
+         *
+         * <p> The encoding scheme of this encoder instance is unaffected by
+         * this invocation. The returned encoder instance should be used for
+         * non-padding encoding operation.
+         *
+         * @return an equivalent encoder that encodes without adding any
+         *         padding character at the end
+         */
+        public Encoder withoutPadding() {
+            if (!doPadding)
+                return this;
+            return new Encoder(isURL, newline, linemax, false);
+        }
+
+        private int encode0(byte[] src, int off, int end, byte[] dst) {
+            char[] base64 = isURL ? toBase64URL : toBase64;
+            int sp = off;
+            int slen = (end - off) / 3 * 3;
+            int sl = off + slen;
+            if (linemax > 0 && slen  > linemax / 4 * 3)
+                slen = linemax / 4 * 3;
+            int dp = 0;
+            while (sp < sl) {
+                int sl0 = Math.min(sp + slen, sl);
+                for (int sp0 = sp, dp0 = dp ; sp0 < sl0; ) {
+                    int bits = (src[sp0++] & 0xff) << 16 |
+                               (src[sp0++] & 0xff) <<  8 |
+                               (src[sp0++] & 0xff);
+                    dst[dp0++] = (byte)base64[(bits >>> 18) & 0x3f];
+                    dst[dp0++] = (byte)base64[(bits >>> 12) & 0x3f];
+                    dst[dp0++] = (byte)base64[(bits >>> 6)  & 0x3f];
+                    dst[dp0++] = (byte)base64[bits & 0x3f];
+                }
+                int dlen = (sl0 - sp) / 3 * 4;
+                dp += dlen;
+                sp = sl0;
+                if (dlen == linemax && sp < end) {
+                    for (byte b : newline){
+                        dst[dp++] = b;
+                    }
+                }
+            }
+            if (sp < end) {               // 1 or 2 leftover bytes
+                int b0 = src[sp++] & 0xff;
+                dst[dp++] = (byte)base64[b0 >> 2];
+                if (sp == end) {
+                    dst[dp++] = (byte)base64[(b0 << 4) & 0x3f];
+                    if (doPadding) {
+                        dst[dp++] = '=';
+                        dst[dp++] = '=';
+                    }
+                } else {
+                    int b1 = src[sp++] & 0xff;
+                    dst[dp++] = (byte)base64[(b0 << 4) & 0x3f | (b1 >> 4)];
+                    dst[dp++] = (byte)base64[(b1 << 2) & 0x3f];
+                    if (doPadding) {
+                        dst[dp++] = '=';
+                    }
+                }
+            }
+            return dp;
+        }
+    }
+
+    /**
+     * This class implements a decoder for decoding byte data using the
+     * Base64 encoding scheme as specified in RFC 4648 and RFC 2045.
+     *
+     * <p> The Base64 padding character {@code '='} is accepted and
+     * interpreted as the end of the encoded byte data, but is not
+     * required. So if the final unit of the encoded byte data only has
+     * two or three Base64 characters (without the corresponding padding
+     * character(s) padded), they are decoded as if followed by padding
+     * character(s). If there is a padding character present in the
+     * final unit, the correct number of padding character(s) must be
+     * present, otherwise {@code IllegalArgumentException} (
+     * {@code IOException} when reading from a Base64 stream) is thrown
+     * during decoding.
+     *
+     * <p> Instances of {@link Decoder} class are safe for use by
+     * multiple concurrent threads.
+     *
+     * <p> Unless otherwise noted, passing a {@code null} argument to
+     * a method of this class will cause a
+     * {@link java.lang.NullPointerException NullPointerException} to
+     * be thrown.
+     *
+     * @see     Encoder
+     * @since   1.8
+     */
+    public static class Decoder {
+
+        private final boolean isURL;
+        private final boolean isMIME;
+
+        private Decoder(boolean isURL, boolean isMIME) {
+            this.isURL = isURL;
+            this.isMIME = isMIME;
+        }
+
+        /**
+         * Lookup table for decoding unicode characters drawn from the
+         * "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into
+         * their 6-bit positive integer equivalents.  Characters that
+         * are not in the Base64 alphabet but fall within the bounds of
+         * the array are encoded to -1.
+         *
+         */
+        private static final int[] fromBase64 = new int[256];
+        static {
+            Arrays.fill(fromBase64, -1);
+            for (int i = 0; i < Encoder.toBase64.length; i++)
+                fromBase64[Encoder.toBase64[i]] = i;
+            fromBase64['='] = -2;
+        }
+
+        /**
+         * Lookup table for decoding "URL and Filename safe Base64 Alphabet"
+         * as specified in Table2 of the RFC 4648.
+         */
+        private static final int[] fromBase64URL = new int[256];
+
+        static {
+            Arrays.fill(fromBase64URL, -1);
+            for (int i = 0; i < Encoder.toBase64URL.length; i++)
+                fromBase64URL[Encoder.toBase64URL[i]] = i;
+            fromBase64URL['='] = -2;
+        }
+
+        static final Decoder RFC4648         = new Decoder(false, false);
+        static final Decoder RFC4648_URLSAFE = new Decoder(true, false);
+        static final Decoder RFC2045         = new Decoder(false, true);
+
+        /**
+         * Decodes all bytes from the input byte array using the {@link Base64}
+         * encoding scheme, writing the results into a newly-allocated output
+         * byte array. The returned byte array is of the length of the resulting
+         * bytes.
+         *
+         * @param   src
+         *          the byte array to decode
+         *
+         * @return  A newly-allocated byte array containing the decoded bytes.
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme
+         */
+        public byte[] decode(byte[] src) {
+            byte[] dst = new byte[outLength(src, 0, src.length)];
+            int ret = decode0(src, 0, src.length, dst);
+            if (ret != dst.length) {
+                dst = Arrays.copyOf(dst, ret);
+            }
+            return dst;
+        }
+
+        /**
+         * Decodes a Base64 encoded String into a newly-allocated byte array
+         * using the {@link Base64} encoding scheme.
+         *
+         * <p> An invocation of this method has exactly the same effect as invoking
+         * {@code decode(src.getBytes(StandardCharsets.ISO_8859_1))}
+         *
+         * @param   src
+         *          the string to decode
+         *
+         * @return  A newly-allocated byte array containing the decoded bytes.
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme
+         */
+        public byte[] decode(String src) {
+            return decode(src.getBytes(StandardCharsets.ISO_8859_1));
+        }
+
+        /**
+         * Decodes all bytes from the input byte array using the {@link Base64}
+         * encoding scheme, writing the results into the given output byte array,
+         * starting at offset 0.
+         *
+         * <p> It is the responsibility of the invoker of this method to make
+         * sure the output byte array {@code dst} has enough space for decoding
+         * all bytes from the input byte array. No bytes will be be written to
+         * the output byte array if the output byte array is not big enough.
+         *
+         * <p> If the input byte array is not in valid Base64 encoding scheme
+         * then some bytes may have been written to the output byte array before
+         * IllegalargumentException is thrown.
+         *
+         * @param   src
+         *          the byte array to decode
+         * @param   dst
+         *          the output byte array
+         *
+         * @return  The number of bytes written to the output byte array
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme, or {@code dst}
+         *          does not have enough space for decoding all input bytes.
+         */
+        public int decode(byte[] src, byte[] dst) {
+            int len = outLength(src, 0, src.length);
+            if (dst.length < len)
+                throw new IllegalArgumentException(
+                    "Output byte array is too small for decoding all input bytes");
+            return decode0(src, 0, src.length, dst);
+        }
+
+        /**
+         * Decodes all bytes from the input byte buffer using the {@link Base64}
+         * encoding scheme, writing the results into a newly-allocated ByteBuffer.
+         *
+         * <p> Upon return, the source buffer's position will be updated to
+         * its limit; its limit will not have been changed. The returned
+         * output buffer's position will be zero and its limit will be the
+         * number of resulting decoded bytes
+         *
+         * <p> {@code IllegalArgumentException} is thrown if the input buffer
+         * is not in valid Base64 encoding scheme. The position of the input
+         * buffer will not be advanced in this case.
+         *
+         * @param   buffer
+         *          the ByteBuffer to decode
+         *
+         * @return  A newly-allocated byte buffer containing the decoded bytes
+         *
+         * @throws  IllegalArgumentException
+         *          if {@code src} is not in valid Base64 scheme.
+         */
+        public ByteBuffer decode(ByteBuffer buffer) {
+            int pos0 = buffer.position();
+            try {
+                byte[] src;
+                int sp, sl;
+                if (buffer.hasArray()) {
+                    src = buffer.array();
+                    sp = buffer.arrayOffset() + buffer.position();
+                    sl = buffer.arrayOffset() + buffer.limit();
+                    buffer.position(buffer.limit());
+                } else {
+                    src = new byte[buffer.remaining()];
+                    buffer.get(src);
+                    sp = 0;
+                    sl = src.length;
+                }
+                byte[] dst = new byte[outLength(src, sp, sl)];
+                return ByteBuffer.wrap(dst, 0, decode0(src, sp, sl, dst));
+            } catch (IllegalArgumentException iae) {
+                buffer.position(pos0);
+                throw iae;
+            }
+        }
+
+        /**
+         * Returns an input stream for decoding {@link Base64} encoded byte stream.
+         *
+         * <p> The {@code read}  methods of the returned {@code InputStream} will
+         * throw {@code IOException} when reading bytes that cannot be decoded.
+         *
+         * <p> Closing the returned input stream will close the underlying
+         * input stream.
+         *
+         * @param   is
+         *          the input stream
+         *
+         * @return  the input stream for decoding the specified Base64 encoded
+         *          byte stream
+         */
+        public InputStream wrap(InputStream is) {
+            Objects.requireNonNull(is);
+            return new DecInputStream(is, isURL ? fromBase64URL : fromBase64, isMIME);
+        }
+
+        private int outLength(byte[] src, int sp, int sl) {
+            int[] base64 = isURL ? fromBase64URL : fromBase64;
+            int paddings = 0;
+            int len = sl - sp;
+            if (len == 0)
+                return 0;
+            if (len < 2) {
+                if (isMIME && base64[0] == -1)
+                    return 0;
+                throw new IllegalArgumentException(
+                    "Input byte[] should at least have 2 bytes for base64 bytes");
+            }
+            if (isMIME) {
+                // scan all bytes to fill out all non-alphabet. a performance
+                // trade-off of pre-scan or Arrays.copyOf
+                int n = 0;
+                while (sp < sl) {
+                    int b = src[sp++] & 0xff;
+                    if (b == '=') {
+                        len -= (sl - sp + 1);
+                        break;
+                    }
+                    if ((b = base64[b]) == -1)
+                        n++;
+                }
+                len -= n;
+            } else {
+                if (src[sl - 1] == '=') {
+                    paddings++;
+                    if (src[sl - 2] == '=')
+                        paddings++;
+                }
+            }
+            if (paddings == 0 && (len & 0x3) !=  0)
+                paddings = 4 - (len & 0x3);
+            return 3 * ((len + 3) / 4) - paddings;
+        }
+
+        private int decode0(byte[] src, int sp, int sl, byte[] dst) {
+            int[] base64 = isURL ? fromBase64URL : fromBase64;
+            int dp = 0;
+            int bits = 0;
+            int shiftto = 18;       // pos of first byte of 4-byte atom
+            while (sp < sl) {
+                int b = src[sp++] & 0xff;
+                if ((b = base64[b]) < 0) {
+                    if (b == -2) {         // padding byte '='
+                        // =     shiftto==18 unnecessary padding
+                        // x=    shiftto==12 a dangling single x
+                        // x     to be handled together with non-padding case
+                        // xx=   shiftto==6&&sp==sl missing last =
+                        // xx=y  shiftto==6 last is not =
+                        if (shiftto == 6 && (sp == sl || src[sp++] != '=') ||
+                            shiftto == 18) {
+                            throw new IllegalArgumentException(
+                                "Input byte array has wrong 4-byte ending unit");
+                        }
+                        break;
+                    }
+                    if (isMIME)    // skip if for rfc2045
+                        continue;
+                    else
+                        throw new IllegalArgumentException(
+                            "Illegal base64 character " +
+                            Integer.toString(src[sp - 1], 16));
+                }
+                bits |= (b << shiftto);
+                shiftto -= 6;
+                if (shiftto < 0) {
+                    dst[dp++] = (byte)(bits >> 16);
+                    dst[dp++] = (byte)(bits >>  8);
+                    dst[dp++] = (byte)(bits);
+                    shiftto = 18;
+                    bits = 0;
+                }
+            }
+            // reached end of byte array or hit padding '=' characters.
+            if (shiftto == 6) {
+                dst[dp++] = (byte)(bits >> 16);
+            } else if (shiftto == 0) {
+                dst[dp++] = (byte)(bits >> 16);
+                dst[dp++] = (byte)(bits >>  8);
+            } else if (shiftto == 12) {
+                // dangling single "x", incorrectly encoded.
+                throw new IllegalArgumentException(
+                    "Last unit does not have enough valid bits");
+            }
+            // anything left is invalid, if is not MIME.
+            // if MIME, ignore all non-base64 character
+            while (sp < sl) {
+                if (isMIME && base64[src[sp++]] < 0)
+                    continue;
+                throw new IllegalArgumentException(
+                    "Input byte array has incorrect ending byte at " + sp);
+            }
+            return dp;
+        }
+    }
+
+    /*
+     * An output stream for encoding bytes into the Base64.
+     */
+    private static class EncOutputStream extends FilterOutputStream {
+
+        private int leftover = 0;
+        private int b0, b1, b2;
+        private boolean closed = false;
+
+        private final char[] base64;    // byte->base64 mapping
+        private final byte[] newline;   // line separator, if needed
+        private final int linemax;
+        private final boolean doPadding;// whether or not to pad
+        private int linepos = 0;
+
+        EncOutputStream(OutputStream os, char[] base64,
+                        byte[] newline, int linemax, boolean doPadding) {
+            super(os);
+            this.base64 = base64;
+            this.newline = newline;
+            this.linemax = linemax;
+            this.doPadding = doPadding;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            byte[] buf = new byte[1];
+            buf[0] = (byte)(b & 0xff);
+            write(buf, 0, 1);
+        }
+
+        private void checkNewline() throws IOException {
+            if (linepos == linemax) {
+                out.write(newline);
+                linepos = 0;
+            }
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            if (closed)
+                throw new IOException("Stream is closed");
+            // Android-changed: Upstream fix to avoid overflow.
+            // This upstream fix is from beyond OpenJDK8u121-b13. http://b/62368386
+            // if (off < 0 || len < 0 || off + len > b.length)
+            if (off < 0 || len < 0 || len > b.length - off)
+                throw new ArrayIndexOutOfBoundsException();
+            if (len == 0)
+                return;
+            if (leftover != 0) {
+                if (leftover == 1) {
+                    b1 = b[off++] & 0xff;
+                    len--;
+                    if (len == 0) {
+                        leftover++;
+                        return;
+                    }
+                }
+                b2 = b[off++] & 0xff;
+                len--;
+                checkNewline();
+                out.write(base64[b0 >> 2]);
+                out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]);
+                out.write(base64[(b1 << 2) & 0x3f | (b2 >> 6)]);
+                out.write(base64[b2 & 0x3f]);
+                linepos += 4;
+            }
+            int nBits24 = len / 3;
+            leftover = len - (nBits24 * 3);
+            while (nBits24-- > 0) {
+                checkNewline();
+                int bits = (b[off++] & 0xff) << 16 |
+                           (b[off++] & 0xff) <<  8 |
+                           (b[off++] & 0xff);
+                out.write(base64[(bits >>> 18) & 0x3f]);
+                out.write(base64[(bits >>> 12) & 0x3f]);
+                out.write(base64[(bits >>> 6)  & 0x3f]);
+                out.write(base64[bits & 0x3f]);
+                linepos += 4;
+           }
+            if (leftover == 1) {
+                b0 = b[off++] & 0xff;
+            } else if (leftover == 2) {
+                b0 = b[off++] & 0xff;
+                b1 = b[off++] & 0xff;
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (!closed) {
+                closed = true;
+                if (leftover == 1) {
+                    checkNewline();
+                    out.write(base64[b0 >> 2]);
+                    out.write(base64[(b0 << 4) & 0x3f]);
+                    if (doPadding) {
+                        out.write('=');
+                        out.write('=');
+                    }
+                } else if (leftover == 2) {
+                    checkNewline();
+                    out.write(base64[b0 >> 2]);
+                    out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]);
+                    out.write(base64[(b1 << 2) & 0x3f]);
+                    if (doPadding) {
+                       out.write('=');
+                    }
+                }
+                leftover = 0;
+                out.close();
+            }
+        }
+    }
+
+    /*
+     * An input stream for decoding Base64 bytes
+     */
+    private static class DecInputStream extends InputStream {
+
+        private final InputStream is;
+        private final boolean isMIME;
+        private final int[] base64;      // base64 -> byte mapping
+        private int bits = 0;            // 24-bit buffer for decoding
+        private int nextin = 18;         // next available "off" in "bits" for input;
+                                         // -> 18, 12, 6, 0
+        private int nextout = -8;        // next available "off" in "bits" for output;
+                                         // -> 8, 0, -8 (no byte for output)
+        private boolean eof = false;
+        private boolean closed = false;
+
+        DecInputStream(InputStream is, int[] base64, boolean isMIME) {
+            this.is = is;
+            this.base64 = base64;
+            this.isMIME = isMIME;
+        }
+
+        private byte[] sbBuf = new byte[1];
+
+        @Override
+        public int read() throws IOException {
+            return read(sbBuf, 0, 1) == -1 ? -1 : sbBuf[0] & 0xff;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            if (closed)
+                throw new IOException("Stream is closed");
+            if (eof && nextout < 0)    // eof and no leftover
+                return -1;
+            if (off < 0 || len < 0 || len > b.length - off)
+                throw new IndexOutOfBoundsException();
+            int oldOff = off;
+            if (nextout >= 0) {       // leftover output byte(s) in bits buf
+                do {
+                    if (len == 0)
+                        return off - oldOff;
+                    b[off++] = (byte)(bits >> nextout);
+                    len--;
+                    nextout -= 8;
+                } while (nextout >= 0);
+                bits = 0;
+            }
+            while (len > 0) {
+                int v = is.read();
+                if (v == -1) {
+                    eof = true;
+                    if (nextin != 18) {
+                        if (nextin == 12)
+                            throw new IOException("Base64 stream has one un-decoded dangling byte.");
+                        // treat ending xx/xxx without padding character legal.
+                        // same logic as v == '=' below
+                        b[off++] = (byte)(bits >> (16));
+                        len--;
+                        if (nextin == 0) {           // only one padding byte
+                            if (len == 0) {          // no enough output space
+                                bits >>= 8;          // shift to lowest byte
+                                nextout = 0;
+                            } else {
+                                b[off++] = (byte) (bits >>  8);
+                            }
+                        }
+                    }
+                    if (off == oldOff)
+                        return -1;
+                    else
+                        return off - oldOff;
+                }
+                if (v == '=') {                  // padding byte(s)
+                    // =     shiftto==18 unnecessary padding
+                    // x=    shiftto==12 dangling x, invalid unit
+                    // xx=   shiftto==6 && missing last '='
+                    // xx=y  or last is not '='
+                    if (nextin == 18 || nextin == 12 ||
+                        nextin == 6 && is.read() != '=') {
+                        throw new IOException("Illegal base64 ending sequence:" + nextin);
+                    }
+                    b[off++] = (byte)(bits >> (16));
+                    len--;
+                    if (nextin == 0) {           // only one padding byte
+                        if (len == 0) {          // no enough output space
+                            bits >>= 8;          // shift to lowest byte
+                            nextout = 0;
+                        } else {
+                            b[off++] = (byte) (bits >>  8);
+                        }
+                    }
+                    eof = true;
+                    break;
+                }
+                if ((v = base64[v]) == -1) {
+                    if (isMIME)                 // skip if for rfc2045
+                        continue;
+                    else
+                        throw new IOException("Illegal base64 character " +
+                            Integer.toString(v, 16));
+                }
+                bits |= (v << nextin);
+                if (nextin == 0) {
+                    nextin = 18;    // clear for next
+                    nextout = 16;
+                    while (nextout >= 0) {
+                        b[off++] = (byte)(bits >> nextout);
+                        len--;
+                        nextout -= 8;
+                        if (len == 0 && nextout >= 0) {  // don't clean "bits"
+                            return off - oldOff;
+                        }
+                    }
+                    bits = 0;
+                } else {
+                    nextin -= 6;
+                }
+            }
+            return off - oldOff;
+        }
+
+        @Override
+        public int available() throws IOException {
+            if (closed)
+                throw new IOException("Stream is closed");
+            return is.available();   // TBD:
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (!closed) {
+                closed = true;
+                is.close();
+            }
+        }
+    }
+}

+ 39 - 0
common/src/main/java/com/miekir/common/utils/ContextManager.java

@@ -0,0 +1,39 @@
+package com.miekir.common.utils;
+
+import android.content.Context;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/9/12 17:43
+ * Description:
+ */
+public class ContextManager {
+    private ContextManager(){}
+
+    private static WeakReference<Context> contextWeakReference = new WeakReference<>(null);
+    private static ContextManager instance;
+    public void initContext(Context context) {
+        contextWeakReference = new WeakReference<>(context);
+    }
+
+    public static ContextManager getInstance() {
+        if (instance == null) {
+            init();
+        }
+        return instance;
+    }
+
+    private static synchronized void init() {
+        if (instance == null) {
+            instance = new ContextManager();
+        }
+    }
+
+    public Context getContext() {
+        return contextWeakReference.get();
+    }
+}

+ 60 - 0
common/src/main/java/com/miekir/common/utils/LogTool.java

@@ -0,0 +1,60 @@
+package com.miekir.common.utils;
+
+import android.util.Log;
+
+import com.miekir.common.BuildConfig;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/19 9:19
+ * Description:
+ */
+public class LogTool {
+    private static final String TAG = "LogTool";
+    private static final int RETURN_DEFAULT = -1;
+    private LogTool() {}
+
+    public static int i(String tag, String message) {
+        if (BuildConfig.IS_DEBUG) {
+            return Log.i(tag, message);
+        }
+        return RETURN_DEFAULT;
+    }
+
+    public static int i(String message) {
+        if (BuildConfig.IS_DEBUG) {
+            return Log.i(TAG, message);
+        }
+        return RETURN_DEFAULT;
+    }
+
+    public static int d(String tag, String message) {
+        if (BuildConfig.IS_DEBUG) {
+            return Log.d(tag, message);
+        }
+        return RETURN_DEFAULT;
+    }
+
+    public static int d(String message) {
+        if (BuildConfig.IS_DEBUG) {
+            return Log.d(TAG, message);
+        }
+        return RETURN_DEFAULT;
+    }
+
+    public static int e(String tag, String message) {
+        if (BuildConfig.IS_DEBUG) {
+            return Log.e(tag, message);
+        }
+        return RETURN_DEFAULT;
+    }
+
+    public static int e(String message) {
+        if (BuildConfig.IS_DEBUG) {
+            return Log.e(TAG, message);
+        }
+        return RETURN_DEFAULT;
+    }
+}

+ 16 - 0
common/src/main/java/com/miekir/common/utils/SizeTool.java

@@ -0,0 +1,16 @@
+package com.miekir.common.utils;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/7/5 11:35
+ * Description: 尺寸工具
+ */
+public class SizeTool {
+    private SizeTool() {}
+
+    public static int SCREEN_WIDTH;
+    public static int SCREEN_HEIGHT;
+
+}

+ 72 - 0
common/src/main/java/com/miekir/common/utils/ToastTool.java

@@ -0,0 +1,72 @@
+package com.miekir.common.utils;
+
+import android.content.Context;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.Toast;
+
+/**
+ * @author Miekir
+ * @date 2020/7/5 0:08
+ * Description: Toast工具
+ */
+public class ToastTool {
+    private static final long PERIOD_SHORT = 1500L;
+    private static final long PERIOD_LONG = 2500L;
+
+    private ToastTool() { }
+
+    private static long mLastShortToastMillis;
+    private static long mLastLongToastMillis;
+
+    private static int mVerticalMargin = 0;
+
+    /**
+     * @param text 要弹出的语句
+     */
+    public static void showShort(String text) {
+        Context context = ContextManager.getInstance().getContext();
+        if (context == null) {
+            return;
+        }
+
+        initMargin(context);
+
+        if (System.currentTimeMillis() - mLastShortToastMillis > PERIOD_SHORT) {
+            mLastShortToastMillis = System.currentTimeMillis();
+            Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
+            toast.setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL, 0, mVerticalMargin);
+            toast.show();
+        }
+    }
+
+    /**
+     * @param text 要弹出的语句
+     */
+    public static void showLong(String text) {
+        Context context = ContextManager.getInstance().getContext();
+        if (context == null) {
+            return;
+        }
+
+        initMargin(context);
+
+        if (System.currentTimeMillis() - mLastLongToastMillis > PERIOD_LONG) {
+            mLastLongToastMillis = System.currentTimeMillis();
+            Toast toast = Toast.makeText(context, text, Toast.LENGTH_LONG);
+            toast.setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL, 0, mVerticalMargin);
+            toast.show();
+        }
+    }
+
+    private static void initMargin(Context context) {
+        if (context == null) {
+            return;
+        }
+        if (mVerticalMargin == 0) {
+            mVerticalMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    72,
+                    context.getResources().getDisplayMetrics());
+        }
+    }
+}

+ 76 - 0
common/src/main/java/com/miekir/common/utils/ViewTool.java

@@ -0,0 +1,76 @@
+package com.miekir.common.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/7/5 8:21
+ * Description: View相关的工具
+ */
+public class ViewTool {
+    private ViewTool() {}
+
+    /**
+     * 批量设置监听
+     * @param activity 控件所在Activity
+     * @param viewIdArray 需要监听的控件ID列表
+     * @param listener 监听回调
+     */
+    public static void setOnClickListener(Activity activity, View.OnClickListener listener, int[] viewIdArray) {
+        if (activity == null || viewIdArray == null || listener == null || viewIdArray.length == 0) {
+            return;
+        }
+
+        for (int viewId : viewIdArray) {
+            activity.findViewById(viewId).setOnClickListener(listener);
+        }
+    }
+
+    /**
+     * 获取焦点
+     * @param activity
+     * @param view
+     */
+    public static void requestInputFocus(Activity activity, final View view) {
+        // 进入页面弹出软键盘
+        view.requestFocus();
+        activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+        // 或使用以下方法(view.post不行)
+        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN , 0, 0, 0));
+                view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP , 0, 0, 0));
+                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+            }
+        });
+    }
+
+    /**
+     * 隐藏输入法
+     */
+    public static void hideInputMethod(Activity activity, View rootView) {
+        if (rootView != null) {
+            rootView.requestFocus();
+        }
+
+        final InputMethodManager imm = (InputMethodManager) activity.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm == null) {
+            return;
+        }
+
+        final View currentFocusView = activity.getCurrentFocus();
+        if (currentFocusView != null) {
+            imm.hideSoftInputFromWindow(currentFocusView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+        }
+    }
+}

+ 141 - 0
common/src/main/java/com/miekir/common/view/AlignTextView.java

@@ -0,0 +1,141 @@
+package com.miekir.common.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+
+import androidx.appcompat.widget.AppCompatTextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 两端对齐的textview,可以设置最后一行靠左,靠右,居中对齐
+ *
+ * @author yuyh.
+ * @date 16/4/10.
+ */
+public class AlignTextView extends AppCompatTextView {
+    private float textHeight; // 单行文字高度
+    private int width; // textview宽度
+    private List<String> lines = new ArrayList<String>(); // 分割后的行
+    private List<Integer> tailLines = new ArrayList<Integer>(); // 尾行
+    private Align align = Align.ALIGN_LEFT; // 默认最后一行左对齐
+
+    // 尾行对齐方式,针对段落最后一行
+    public enum Align {
+        ALIGN_LEFT,
+        ALIGN_CENTER,
+        ALIGN_RIGHT,
+    }
+
+    public AlignTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        TextPaint paint = getPaint();
+        paint.setColor(getCurrentTextColor());
+        paint.drawableState = getDrawableState();
+        width = getMeasuredWidth();
+        String text = getText().toString();
+
+        Paint.FontMetrics fm = paint.getFontMetrics();
+        // 计算行高
+        Layout layout = getLayout();
+
+        // layout.getLayout()在4.4.3出现NullPointerException
+        if (layout == null) {
+            return;
+        }
+
+        textHeight = fm.descent - fm.ascent;
+        textHeight = textHeight * layout.getSpacingMultiplier() + layout.getSpacingAdd();
+
+        float firstHeight = getTextSize();
+
+        int gravity = getGravity();
+        if ((gravity & 0x1000) == 0) { // 是否垂直居中
+            firstHeight = firstHeight + (textHeight - firstHeight) / 2;
+        }
+
+        int paddingLeft = getPaddingLeft();
+        int paddingRight = getPaddingRight();
+        width = width - paddingLeft - paddingRight;
+
+        lines.clear();
+        tailLines.clear();
+
+        // 文本含有换行符时,分割单独处理
+        String[] items = text.split("\\n");
+        for (String item : items) {
+            calc(paint, item);
+        }
+
+        for (int i = 0; i < lines.size(); i++) {
+            float drawY = i * textHeight + firstHeight;
+            String line = lines.get(i);
+            // 绘画起始x坐标
+            float drawSpacingX = paddingLeft;
+            float gap = (width - paint.measureText(line));
+            float interval = gap / (line.length() - 1);
+
+            // 绘制最后一行
+            if (tailLines.contains(i)) {
+                interval = 0;
+                if (align == Align.ALIGN_CENTER)
+                    drawSpacingX += gap / 2;
+                else if (align == Align.ALIGN_RIGHT)
+                    drawSpacingX += gap;
+            }
+
+            for (int j = 0; j < line.length(); j++) {
+                float drawX = paint.measureText(line.substring(0, j))
+                        + interval * j;
+                canvas.drawText(line.substring(j, j + 1), drawX + drawSpacingX,
+                        drawY, paint);
+            }
+        }
+    }
+
+    /**
+     * 设置尾行对齐方式
+     *
+     * @param align
+     */
+    public void setAlign(Align align) {
+        this.align = align;
+        invalidate();
+    }
+
+    /**
+     * 计算每行应显示的文本数
+     *
+     * @param text
+     * @return
+     */
+    private void calc(Paint paint, String text) {
+        if (text.length() == 0) {
+            lines.add("\n");
+            return;
+        }
+        StringBuffer sb = new StringBuffer("");
+        int startPosition = 0; // 起始位置
+        for (int i = 0; i < text.length(); i++) {
+            if (paint.measureText(text.substring(startPosition, i + 1)) > width) {
+                startPosition = i;
+                lines.add(sb.toString());
+                sb = new StringBuffer();
+            }
+            sb.append(text.charAt(i));
+        }
+        if (sb.length() > 0)
+            lines.add(sb.toString());
+
+        tailLines.add(lines.size() - 1);
+    }
+}

+ 237 - 0
common/src/main/java/com/miekir/common/view/RoundedCornersTransformation.java

@@ -0,0 +1,237 @@
+package com.miekir.common.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+import java.security.MessageDigest;
+
+public class RoundedCornersTransformation extends BitmapTransformation {
+
+  private static final int VERSION = 1;
+  private static final String ID = "jp.wasabeef.glide.transformations.RoundedCornersTransformation." + VERSION;
+
+  public enum CornerType {
+    ALL,
+    TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT,
+    TOP, BOTTOM, LEFT, RIGHT,
+    OTHER_TOP_LEFT, OTHER_TOP_RIGHT, OTHER_BOTTOM_LEFT, OTHER_BOTTOM_RIGHT,
+    DIAGONAL_FROM_TOP_LEFT, DIAGONAL_FROM_TOP_RIGHT
+  }
+
+  private int radius;
+  private int diameter;
+  private int margin;
+  private CornerType cornerType;
+
+  public RoundedCornersTransformation(int radius, int margin) {
+    this(radius, margin, CornerType.ALL);
+  }
+
+  public RoundedCornersTransformation(int radius, int margin, CornerType cornerType) {
+    this.radius = radius;
+    this.diameter = this.radius * 2;
+    this.margin = margin;
+    this.cornerType = cornerType;
+  }
+
+  @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
+    int width = toTransform.getWidth();
+    int height = toTransform.getHeight();
+
+    Bitmap bitmap = pool.get(width, height, Bitmap.Config.ARGB_8888);
+    bitmap.setHasAlpha(true);
+
+    Canvas canvas = new Canvas(bitmap);
+    Paint paint = new Paint();
+    paint.setAntiAlias(true);
+    paint.setShader(new BitmapShader(toTransform, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
+    drawRoundRect(canvas, paint, width, height);
+    return bitmap;
+  }
+
+  private void drawRoundRect(Canvas canvas, Paint paint, float width, float height) {
+    float right = width - margin;
+    float bottom = height - margin;
+
+    switch (cornerType) {
+      case ALL:
+        canvas.drawRoundRect(new RectF(margin, margin, right, bottom), radius, radius, paint);
+        break;
+      case TOP_LEFT:
+        drawTopLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case TOP_RIGHT:
+        drawTopRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case BOTTOM_LEFT:
+        drawBottomLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case BOTTOM_RIGHT:
+        drawBottomRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case TOP:
+        drawTopRoundRect(canvas, paint, right, bottom);
+        break;
+      case BOTTOM:
+        drawBottomRoundRect(canvas, paint, right, bottom);
+        break;
+      case LEFT:
+        drawLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case RIGHT:
+        drawRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_TOP_LEFT:
+        drawOtherTopLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_TOP_RIGHT:
+        drawOtherTopRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_BOTTOM_LEFT:
+        drawOtherBottomLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case OTHER_BOTTOM_RIGHT:
+        drawOtherBottomRightRoundRect(canvas, paint, right, bottom);
+        break;
+      case DIAGONAL_FROM_TOP_LEFT:
+        drawDiagonalFromTopLeftRoundRect(canvas, paint, right, bottom);
+        break;
+      case DIAGONAL_FROM_TOP_RIGHT:
+        drawDiagonalFromTopRightRoundRect(canvas, paint, right, bottom);
+        break;
+      default:
+        canvas.drawRoundRect(new RectF(margin, margin, right, bottom), radius, radius, paint);
+        break;
+    }
+  }
+
+  private void drawTopLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin + radius, margin + radius, bottom), paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom), paint);
+  }
+
+  private void drawTopRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom), paint);
+    canvas.drawRect(new RectF(right - radius, margin + radius, right, bottom), paint);
+  }
+
+  private void drawBottomLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, margin + diameter, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, margin + diameter, bottom - radius), paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom), paint);
+  }
+
+  private void drawBottomRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, bottom - diameter, right, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom), paint);
+    canvas.drawRect(new RectF(right - radius, margin, right, bottom - radius), paint);
+  }
+
+  private void drawTopRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, right, margin + diameter), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin, margin + radius, right, bottom), paint);
+  }
+
+  private void drawBottomRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, right, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin, margin, right, bottom - radius), paint);
+  }
+
+  private void drawLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom), paint);
+  }
+
+  private void drawRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, bottom), radius, radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom), paint);
+  }
+
+  private void drawOtherTopLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, right, bottom), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, bottom), radius, radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom - radius), paint);
+  }
+
+  private void drawOtherTopRightRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, bottom), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, right, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin + radius, margin, right, bottom - radius), paint);
+  }
+
+  private void drawOtherBottomLeftRoundRect(Canvas canvas, Paint paint, float right, float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, right, margin + diameter), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, bottom), radius, radius, paint);
+    canvas.drawRect(new RectF(margin, margin + radius, right - radius, bottom), paint);
+  }
+
+  private void drawOtherBottomRightRoundRect(Canvas canvas, Paint paint, float right,
+      float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, right, margin + diameter), radius, radius,
+        paint);
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, bottom), radius, radius,
+        paint);
+    canvas.drawRect(new RectF(margin + radius, margin + radius, right, bottom), paint);
+  }
+
+  private void drawDiagonalFromTopLeftRoundRect(Canvas canvas, Paint paint, float right,
+      float bottom) {
+    canvas.drawRoundRect(new RectF(margin, margin, margin + diameter, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRoundRect(new RectF(right - diameter, bottom - diameter, right, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin + radius, right - diameter, bottom), paint);
+    canvas.drawRect(new RectF(margin + diameter, margin, right, bottom - radius), paint);
+  }
+
+  private void drawDiagonalFromTopRightRoundRect(Canvas canvas, Paint paint, float right,
+      float bottom) {
+    canvas.drawRoundRect(new RectF(right - diameter, margin, right, margin + diameter), radius,
+        radius, paint);
+    canvas.drawRoundRect(new RectF(margin, bottom - diameter, margin + diameter, bottom), radius,
+        radius, paint);
+    canvas.drawRect(new RectF(margin, margin, right - radius, bottom - radius), paint);
+    canvas.drawRect(new RectF(margin + radius, margin + radius, right, bottom), paint);
+  }
+
+  @Override public String toString() {
+    return "RoundedTransformation(radius=" + radius + ", margin=" + margin + ", diameter="
+        + diameter + ", cornerType=" + cornerType.name() + ")";
+  }
+
+  @Override public boolean equals(Object o) {
+    return o instanceof RoundedCornersTransformation &&
+        ((RoundedCornersTransformation) o).radius == radius &&
+        ((RoundedCornersTransformation) o).diameter == diameter &&
+        ((RoundedCornersTransformation) o).margin == margin &&
+        ((RoundedCornersTransformation) o).cornerType == cornerType;
+  }
+
+  @Override public int hashCode() {
+    return ID.hashCode() + radius * 10000 + diameter * 1000 + margin * 100 + cornerType.ordinal() * 10;
+  }
+
+  @Override public void updateDiskCacheKey(MessageDigest messageDigest) {
+    messageDigest.update((ID + radius + diameter + margin + cornerType).getBytes(CHARSET));
+  }
+}

+ 45 - 0
common/src/main/java/com/miekir/common/widget/ScrollableViewPager.java

@@ -0,0 +1,45 @@
+package com.miekir.common.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.viewpager.widget.ViewPager;
+
+public class ScrollableViewPager extends ViewPager {
+
+    private boolean scrollable = false;
+
+
+    public ScrollableViewPager(Context context) {
+        super(context);
+    }
+
+    public ScrollableViewPager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!scrollable) {
+            return false;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!scrollable) {
+            return false;
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    public boolean isCanScrollble() {
+        return scrollable;
+    }
+
+    public void setCanScrollble(boolean scrollble) {
+        this.scrollable = scrollble;
+    }
+}  

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

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

+ 5 - 0
common/src/main/res/xml/network.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <!--解决9.0版本上无法访问http服务器的问题-->
+    <base-config cleartextTrafficPermitted="true" />
+</network-security-config>

+ 4 - 0
common/src/main/res/xml/provider_paths.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <external-path name="external_files" path="."/>
+</paths>

+ 17 - 0
common/src/test/java/com/miekir/common/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.miekir.common;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 50 - 0
mvp/build.gradle

@@ -0,0 +1,50 @@
+apply plugin: 'com.android.library'
+
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+
+    defaultConfig {
+        minSdkVersion versions.minSdk
+        targetSdkVersion versions.targetSdk
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        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 {
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation project(path: ':common')
+
+    def lifecycle_version = "2.2.0"
+    //implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+
+    //def activity_version = "1.1.0"
+    def activity_version = "1.2.0-beta01"
+    implementation "androidx.activity:activity:$activity_version"
+
+    // 屏幕适配JessYanCoding/AndroidAutoSize
+    api 'me.jessyan:autosize:1.2.1'
+}

+ 21 - 0
mvp/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

+ 27 - 0
mvp/src/androidTest/java/com/miekir/mvp/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.miekir.mvp;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.miekir.mvp.test", appContext.getPackageName());
+    }
+}

+ 11 - 0
mvp/src/main/AndroidManifest.xml

@@ -0,0 +1,11 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.miekir.mvp" >
+    <application>
+        <meta-data
+            android:name="design_width_in_dp"
+            android:value="360"/>
+        <meta-data
+            android:name="design_height_in_dp"
+            android:value="640"/>
+    </application>
+</manifest>

+ 19 - 0
mvp/src/main/java/com/miekir/mvp/ProxyHandler.java

@@ -0,0 +1,19 @@
+package com.miekir.mvp;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+
+public class ProxyHandler implements InvocationHandler {
+    private Object object;
+
+    public ProxyHandler(Object object){
+        this.object = object;
+    }
+    @Override
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+        //System.out.println("Before invoke "  + method.getName());
+        method.invoke(object, args);
+        //System.out.println("After invoke " + method.getName());
+        return null;
+    }
+}

+ 150 - 0
mvp/src/main/java/com/miekir/mvp/base/BaseActivity.java

@@ -0,0 +1,150 @@
+package com.miekir.mvp.base;
+
+import android.os.Bundle;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.miekir.common.utils.ViewTool;
+import com.miekir.mvp.widget.LoadingView;
+
+/**
+ * 适配器模式,这个类会适配子类的功能,帮子类实现具体的弹出加载框、弹出提示等基本操作
+ */
+public abstract class BaseActivity extends AppCompatActivity {
+    private LoadingView mLoadingDialog;
+    private View rootView;
+
+    /**
+     * 获取布局文件Id
+     * @return 布局Id
+     */
+    public abstract int getLayoutId();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // 状态栏深色模式,改变状态栏文字颜色
+        //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        //    getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+        //}
+        super.onCreate(savedInstanceState);
+        rootView = LayoutInflater.from(this).inflate(getLayoutId(), null);
+        setContentView(rootView);
+
+        View.OnTouchListener rootTouchListener = new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                // 点击空白处隐藏输入法(要配合Activity根布局增加以下标志使用
+                // android:focusable="true"
+                // android:focusableInTouchMode="true")
+                ViewTool.hideInputMethod(BaseActivity.this, rootView);
+                return false;
+            }
+        };
+        rootView.setOnTouchListener(rootTouchListener);
+
+        //进入页面隐藏输入框
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+        mLoadingDialog = new LoadingView(this);
+        mLoadingDialog.setCancelListener(new LoadingView.OnLoadingCancelListener() {
+            @Override
+            public void onLoadingCancel() {
+                mLoadingTime = 0;
+                onProgressTaskCancel();
+            }
+        });
+    }
+
+    @Override
+    protected void onPause() {
+        // 必须要在onPause隐藏键盘,在onDestroy就太晚了
+        ViewTool.hideInputMethod(this, rootView);
+        super.onPause();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+    }
+
+    /**
+     * 可见任务被取消
+     */
+    public abstract void onProgressTaskCancel();
+
+    /**
+     * 视图初始化
+     * @param savedInstanceState
+     */
+    public abstract void initViews(Bundle savedInstanceState);
+
+    private boolean isMainThread() {
+        return Looper.myLooper() == Looper.getMainLooper();
+    }
+
+    /**
+     * 加载框相关
+     */
+    public void showLoading() {
+        showLoading(true);
+    }
+
+    public void showLoading(final boolean cancelable) {
+        showLoading("", cancelable);
+    }
+
+    public void showLoading(final String msg) {
+        showLoading(msg, true);
+    }
+
+    private volatile int mLoadingTime = 0;
+    private synchronized void onLoadTimeChange(boolean isShow) {
+        if (isShow) {
+            mLoadingTime++;
+        } else {
+            mLoadingTime--;
+        }
+
+        if (mLoadingTime <= 0) {
+            mLoadingTime = 0;
+            if (mLoadingDialog != null) {
+                if (isMainThread()) {
+                    mLoadingDialog.close();
+                } else {
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            mLoadingDialog.close();
+                        }
+                    });
+                }
+            }
+        }
+    }
+    public void showLoading(final String msg, final boolean cancelable) {
+        onLoadTimeChange(true);
+        if (mLoadingDialog == null) {
+            return;
+        }
+
+        if (isMainThread()) {
+            mLoadingDialog.show(msg, cancelable);
+        } else {
+            runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mLoadingDialog.show(msg, cancelable);
+                }
+            });
+        }
+    }
+
+    public void hideLoading() {
+        onLoadTimeChange(false);
+    }
+}

+ 136 - 0
mvp/src/main/java/com/miekir/mvp/base/BaseFragment.java

@@ -0,0 +1,136 @@
+package com.miekir.mvp.base;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.miekir.mvp.widget.LoadingView;
+
+
+public abstract class BaseFragment extends Fragment {
+    public FragmentActivity activity;
+    private LoadingView mLoadingDialog;
+    protected View rootView;
+
+    /**
+     * 设置布局layout
+     * @return 布局文件id
+     */
+    public abstract @LayoutRes int getLayoutId();
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        rootView = inflater.inflate(getLayoutId(), container, false);
+        activity = getActivity();
+        return rootView;
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mLoadingDialog = new LoadingView(getActivity());
+        mLoadingDialog.setCancelListener(new LoadingView.OnLoadingCancelListener() {
+            @Override
+            public void onLoadingCancel() {
+                mLoadingTime = 0;
+                onProgressTaskCancel();
+            }
+        });
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        hideLoading();
+        activity = null;
+    }
+
+    /**
+     * 可见任务被取消
+     */
+    public abstract void onProgressTaskCancel();
+
+    private boolean isMainThread() {
+        return Looper.myLooper() == Looper.getMainLooper();
+    }
+
+    /**
+     * 加载框相关
+     */
+    public void showLoading() {
+        showLoading(true);
+    }
+
+    public void showLoading(final boolean cancelable) {
+        showLoading("", cancelable);
+    }
+
+    public void showLoading(final String msg) {
+        showLoading(msg, true);
+    }
+
+    private volatile int mLoadingTime = 0;
+    private synchronized void onLoadTimeChange(boolean isShow) {
+        if (isShow) {
+            mLoadingTime++;
+        } else {
+            mLoadingTime--;
+        }
+
+        if (mLoadingTime <= 0) {
+            mLoadingTime = 0;
+            if (mLoadingDialog != null) {
+                if (isMainThread()) {
+                    mLoadingDialog.close();
+                } else {
+                    Activity activity = getActivity();
+                    if (activity != null) {
+                        activity.runOnUiThread(new Runnable() {
+                            @Override
+                            public void run() {
+                                mLoadingDialog.close();
+                            }
+                        });
+                    }
+                }
+            }
+        }
+    }
+    public void showLoading(final String msg, final boolean cancelable) {
+        onLoadTimeChange(true);
+        if (mLoadingDialog == null) {
+            return;
+        }
+
+        if (isMainThread()) {
+            mLoadingDialog.show(msg, cancelable);
+        } else {
+            Activity activity = getActivity();
+            if (activity != null) {
+                activity.runOnUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        mLoadingDialog.show(msg, cancelable);
+                    }
+                });
+            }
+        }
+    }
+
+    public void hideLoading() {
+        onLoadTimeChange(false);
+    }
+
+
+}

+ 16 - 0
mvp/src/main/java/com/miekir/mvp/base/NeedLoading.java

@@ -0,0 +1,16 @@
+package com.miekir.mvp.base;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @description 方法回调标记
+ * @date 2019/8/19
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface NeedLoading {
+    //String message() default "";
+}

+ 13 - 0
mvp/src/main/java/com/miekir/mvp/constant/MvpRepository.java

@@ -0,0 +1,13 @@
+package com.miekir.mvp.constant;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/20 21:25
+ * Description:
+ */
+public interface MvpRepository {
+    int REPO_NET = 0;
+    int REPO_FILE = 1;
+}

+ 36 - 0
mvp/src/main/java/com/miekir/mvp/model/BaseModel.java

@@ -0,0 +1,36 @@
+package com.miekir.mvp.model;
+
+import com.miekir.common.utils.LogTool;
+import com.miekir.mvp.presenter.BasePresenter;
+
+import io.reactivex.disposables.CompositeDisposable;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/20 15:08
+ * Description:
+ */
+public abstract class BaseModel {
+    protected BasePresenter<?> mPresenter;
+
+    public BaseModel(BasePresenter<?> presenter) {
+        mPresenter = presenter;
+    }
+
+    protected CompositeDisposable mProgressDisposableList = new CompositeDisposable();
+    protected CompositeDisposable mBackDisposableList = new CompositeDisposable();
+
+    public void onProgressTaskCancel() {
+        mProgressDisposableList.dispose();
+        mProgressDisposableList = new CompositeDisposable();
+        LogTool.d(getClass().getName(), "progress model cancel");
+    }
+
+    public void onBackTaskCancel() {
+        mBackDisposableList.dispose();
+        mPresenter = null;
+        LogTool.d(getClass().getName(), "back model cancel");
+    }
+}

+ 20 - 0
mvp/src/main/java/com/miekir/mvp/model/IModel.java

@@ -0,0 +1,20 @@
+package com.miekir.mvp.model;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/20 15:08
+ * Description:
+ */
+public interface IModel {
+    /**
+     * 可见任务被取消
+     */
+    void onProgressTaskCancel();
+
+    /**
+     * 不可见任务被取消
+     */
+    void onBackTaskCancel();
+}

+ 17 - 0
mvp/src/main/java/com/miekir/mvp/model/InjectModel.java

@@ -0,0 +1,17 @@
+package com.miekir.mvp.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author zhan
+ * @date 2019/8/19
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface InjectModel {
+    boolean withProgress() default true;
+}

+ 127 - 0
mvp/src/main/java/com/miekir/mvp/presenter/BasePresenter.java

@@ -0,0 +1,127 @@
+package com.miekir.mvp.presenter;
+
+import androidx.lifecycle.ViewModel;
+
+import com.miekir.mvp.model.BaseModel;
+import com.miekir.mvp.view.IView;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+
+import io.reactivex.disposables.CompositeDisposable;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/10/7 22:46
+ * Description:
+ *
+ * View (strong)-> Presenter (strong)-> Model (strong)-> Repository
+ * View <-(weak) Presenter <-(weak) Model <-(weak) Repository
+
+ */
+public abstract class BasePresenter<V extends IView> extends ViewModel {
+    private WeakReference<V> mViewReference;
+    /**
+     * 有加载框的可取消任务,完成之后,会自动移除
+     */
+    protected CompositeDisposable mProgressDisposableList = new CompositeDisposable();
+    /**
+     * 后台没有加载框的可取消任务,完成之后,会自动移除
+     */
+    protected CompositeDisposable mBackDisposableList = new CompositeDisposable();
+    /**
+     * 有加载框的可取消任务, for model
+     */
+    private List<BaseModel> mProgressModelList = new CopyOnWriteArrayList<>();
+    /**
+     * 后台没有加载框的可取消任务,for model
+     */
+    private List<BaseModel> mBackModelList = new CopyOnWriteArrayList<>();
+
+    public BasePresenter() {
+        PresenterHelper.initModelList(mProgressModelList, mBackModelList, this);
+        onInit();
+    }
+
+    /**
+     * 安全地执行方法
+     * @param consumer 要执行的操作
+     */
+    public void post(Consumer<? super V> consumer) {
+        Optional.ofNullable(getView()).ifPresent(consumer);
+    }
+
+    protected V getView() {
+        return mViewReference == null ? null : mViewReference.get();
+    }
+
+    public void attachView(V view) {
+        mViewReference = new WeakReference<V>(view);
+    }
+
+    @Override
+    protected void onCleared() {
+        super.onCleared();
+        // 界面被销毁时,可见和不可见的任务都要取消
+        onBackTaskCancel();
+        onProgressTaskCancel();
+        if (mViewReference != null) {
+            mViewReference.clear();
+            mViewReference = null;
+        }
+    }
+
+    /**
+     * 没有加载框的任务被取消
+     */
+    private void onBackTaskCancel() {
+        mBackDisposableList.dispose();
+        for (BaseModel model : mBackModelList) {
+            model.onProgressTaskCancel();
+        }
+        mBackModelList.clear();
+    }
+
+    /**
+     * 界面还存在,可见任务被取消,响应加载框的取消
+     */
+    public void onProgressTaskCancel() {
+        mProgressDisposableList.dispose();
+        mProgressDisposableList = new CompositeDisposable();
+
+        for (BaseModel model : mProgressModelList) {
+            model.onProgressTaskCancel();
+        }
+        mProgressModelList.clear();
+    }
+
+    /**
+     * 显示加载框
+     */
+    public void showProgress() {
+        IView view = getView();
+        if (view != null) {
+            view.showLoading();
+        }
+    }
+
+    /**
+     * 加载框消失
+     */
+    public void hideProgress() {
+        IView view = getView();
+        if (view != null) {
+            view.hideLoading();
+        }
+    }
+
+    /**
+     * 初始化
+     */
+    public abstract void onInit();
+}

+ 20 - 0
mvp/src/main/java/com/miekir/mvp/presenter/InjectPresenter.java

@@ -0,0 +1,20 @@
+package com.miekir.mvp.presenter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author zhan
+ * @date 2019/8/19
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface InjectPresenter {
+    /**
+     * @return 运行类型,默认在Activity活动生命周期的时候才触发回调,如果是persist则在后台也能触发回调
+     */
+    boolean isPersist() default false;
+}

+ 60 - 0
mvp/src/main/java/com/miekir/mvp/presenter/PresenterHelper.java

@@ -0,0 +1,60 @@
+package com.miekir.mvp.presenter;
+
+import com.miekir.mvp.model.BaseModel;
+import com.miekir.mvp.model.InjectModel;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/14 13:32
+ * Description:
+ */
+public class PresenterHelper {
+    private PresenterHelper() {}
+
+    /**
+     * 初始化添加注解的变量
+     */
+    public static  void initModelList(List<BaseModel> progressModelList, List<BaseModel> backModelList, BasePresenter<?> presenter) {
+        progressModelList.clear();
+        // 这里可以获取到子类的成员变量
+        Field[] fields = presenter.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            // 获取变量上面的注解类型
+            InjectModel modelAnnotation = field.getAnnotation(InjectModel.class);
+            if (modelAnnotation == null) {
+                continue;
+            }
+
+            try {
+                field.setAccessible(true);
+                // 父类引用指向子类对象
+                Class<BaseModel> type = (Class<BaseModel>) field.getType();
+
+                // 不带参数实例化BaseModel model = type.newInstance();
+                // 带参数的构造方法实例化
+                Class<?>[] parameterTypes = {BasePresenter.class};
+                Constructor<BaseModel> modelConstructor = type.getConstructor(parameterTypes);
+                BaseModel model = modelConstructor.newInstance(presenter);
+                field.set(presenter, model);
+
+                if (modelAnnotation.withProgress()) {
+                    progressModelList.add(model);
+                } else {
+                    backModelList.add(model);
+                }
+            } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
+                e.printStackTrace();
+            } catch (ClassCastException e) {
+                e.printStackTrace();
+                throw new RuntimeException(InjectPresenter.class.getName() + "注解修饰的类必须继承自:" + BasePresenter.class.getName());
+            }
+        }
+    }
+}

+ 33 - 0
mvp/src/main/java/com/miekir/mvp/view/BaseMvpActivity.java

@@ -0,0 +1,33 @@
+package com.miekir.mvp.view;
+
+
+import android.os.Bundle;
+
+import com.miekir.mvp.base.BaseActivity;
+import com.miekir.mvp.presenter.BasePresenter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 基于MVP思想的Activity
+ * @author zhan
+ */
+public abstract class BaseMvpActivity extends BaseActivity implements IView {
+    private List<BasePresenter<?>> mInjectPresenters = new ArrayList<>();
+
+    @Override
+    public void onProgressTaskCancel() {
+        for (BasePresenter<?> presenter : mInjectPresenters) {
+            presenter.onProgressTaskCancel();
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        ViewHelper.initVariables(mInjectPresenters, this, this);
+        initViews(savedInstanceState);
+    }
+}

+ 76 - 0
mvp/src/main/java/com/miekir/mvp/view/BaseMvpFragment.java

@@ -0,0 +1,76 @@
+package com.miekir.mvp.view;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.miekir.mvp.base.BaseFragment;
+import com.miekir.mvp.presenter.BasePresenter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class BaseMvpFragment extends BaseFragment implements IView {
+    /**
+     * 是否被创建了
+     */
+    protected boolean isViewCreated;
+    /**
+     * 当前是否可见
+     */
+    protected boolean isUIVisible;
+
+    private List<BasePresenter<?>> mInjectPresenters = new ArrayList<>();
+
+    @Override
+    public void onProgressTaskCancel() {
+        for (BasePresenter<?> presenter : mInjectPresenters) {
+            presenter.onProgressTaskCancel();
+        }
+    }
+
+    @Override
+    public  void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        ViewHelper.initVariables(mInjectPresenters, this, this);
+
+        isViewCreated = true;
+        onViewInit();
+        loadData();
+    }
+
+
+
+    /**
+     * View初始化
+     */
+    protected abstract void onViewInit();
+
+    @Override
+    public void setUserVisibleHint(boolean isVisibleToUser) {
+        super.setUserVisibleHint(isVisibleToUser);
+        isUIVisible = isVisibleToUser;
+        if (isVisibleToUser) {
+            loadData();
+        }
+    }
+
+    /**
+     * 懒加载,当Fragment可见的时候,再去加载数据
+     * 应用初始化会先调用完所有的setUserVisibleHint再调用onViewCreated,然后切换的时候,就只调用setUserVisibleHint了
+     */
+    private void loadData() {
+        if (isViewCreated && isUIVisible) {
+            isViewCreated = false;
+            isUIVisible = false;
+            onLazyLoad();
+        }
+    }
+
+    /**
+     * 懒加载,初始化加载数据
+     */
+    protected abstract void onLazyLoad();
+
+}

+ 6 - 0
mvp/src/main/java/com/miekir/mvp/view/IView.java

@@ -0,0 +1,6 @@
+package com.miekir.mvp.view;
+
+public interface IView {
+    void showLoading();
+    void hideLoading();
+}

+ 44 - 0
mvp/src/main/java/com/miekir/mvp/view/ViewHandler.java

@@ -0,0 +1,44 @@
+package com.miekir.mvp.view;
+
+import com.miekir.common.utils.LogTool;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * @author Miekir
+ */
+public class ViewHandler implements InvocationHandler {
+    private Object mView;
+    private Class<?> mClazz;
+
+    public ViewHandler(Class<?> clazz) {
+        mClazz = clazz;
+    }
+
+    public void setView(Object view) {
+        mView = view;
+    }
+
+    /**
+     * @return 获取代理对象
+     */
+    public Object getProxyObject() {
+        return Proxy.newProxyInstance(mClazz.getClassLoader(), mClazz.getInterfaces(), this);
+    }
+
+    @Override
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+        Object invoke = null;
+
+        if (mView != null) {
+            LogTool.d("proxy_handler", "view invoked!!!!");
+            invoke = method.invoke(mView, args);
+        } else {
+            LogTool.d("proxy_handler", "view is null!!!!");
+        }
+
+        return invoke;
+    }
+}

+ 54 - 0
mvp/src/main/java/com/miekir/mvp/view/ViewHelper.java

@@ -0,0 +1,54 @@
+package com.miekir.mvp.view;
+
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
+
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.mvp.presenter.InjectPresenter;
+
+import java.lang.reflect.Field;
+import java.util.List;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/14 13:32
+ * Description:
+ */
+public class ViewHelper {
+    private ViewHelper() {}
+
+    /**
+     * 初始化添加注解的变量
+     * final Class<? extends IView> getClass,可以代表接口的实现类
+     */
+    public static  void initVariables(List<BasePresenter<?>> mInjectPresenters, final ViewModelStoreOwner owner, final IView iView) {
+        mInjectPresenters.clear();
+        // 这里可以获取到子类的成员变量
+        Field[] fields = iView.getClass().getDeclaredFields();
+        for (Field field : fields) {
+            // 获取变量上面的注解类型
+            InjectPresenter presenterAnnotation = field.getAnnotation(InjectPresenter.class);
+            if (presenterAnnotation == null) {
+                continue;
+            }
+
+            try {
+                field.setAccessible(true);
+                // 父类引用指向子类对象
+                Class<? extends BasePresenter<?>> type = (Class<? extends BasePresenter<?>>) field.getType();
+                //BasePresenter<V> presenter = type.newInstance();
+                BasePresenter presenter = new ViewModelProvider(owner).get(type);
+                field.set(iView, presenter);
+                presenter.attachView(iView);
+                mInjectPresenters.add(presenter);
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            } catch (ClassCastException e) {
+                e.printStackTrace();
+                throw new RuntimeException(InjectPresenter.class.getName() + "注解修饰的类必须继承自:" + BasePresenter.class.getName());
+            }
+        }
+    }
+}

+ 130 - 0
mvp/src/main/java/com/miekir/mvp/widget/CircularRingView.java

@@ -0,0 +1,130 @@
+package com.miekir.mvp.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * 自定义加载动画
+ * @author zc
+ */
+public class CircularRingView extends View {
+    private float mWidth = 0f;
+    private float mPadding = 0f;
+    private float startAngle = 0f;
+    private Paint mPaint;
+
+    public CircularRingView(Context context) {
+        this(context, null);
+    }
+
+    public CircularRingView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CircularRingView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initPaint();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        if (getMeasuredWidth() > getHeight()){
+            mWidth = getMeasuredHeight();
+        }else{
+            mWidth = getMeasuredWidth();
+        }
+        mPadding = 5;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        mPaint.setColor(Color.argb(100, 255, 255, 255));
+        canvas.drawCircle(mWidth / 2, mWidth / 2, mWidth / 2 - mPadding, mPaint);
+        mPaint.setColor(Color.WHITE);
+        RectF rectF = new RectF(mPadding, mPadding, mWidth - mPadding, mWidth - mPadding);
+        //第四个参数是否显示半径
+        canvas.drawArc(rectF, startAngle, 100, false, mPaint);
+
+    }
+
+    private void initPaint() {
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setColor(Color.WHITE);
+        mPaint.setStrokeWidth(8);
+    }
+
+    public void startAnim() {
+        stopAnim();
+        startViewAnim(0f, 1f, 1000);
+    }
+
+    public void stopAnim() {
+        if (valueAnimator != null) {
+            clearAnimation();
+            valueAnimator.setRepeatCount(1);
+            valueAnimator.cancel();
+            valueAnimator.end();
+            valueAnimator.removeAllUpdateListeners();
+            valueAnimator.removeAllListeners();
+        }
+    }
+
+    private ValueAnimator valueAnimator;
+    private ValueAnimator startViewAnim(float startF, final float endF, long time) {
+        if (valueAnimator != null && valueAnimator.isRunning()) {
+            return valueAnimator;
+        }
+
+        valueAnimator = ValueAnimator.ofFloat(startF, endF);
+        valueAnimator.setDuration(time);
+        valueAnimator.setInterpolator(new LinearInterpolator());
+        //无限循环
+        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
+        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
+        if (mUpdateListener == null) {
+            mUpdateListener = new UpdateListener(this);
+        }
+        valueAnimator.addUpdateListener(mUpdateListener);
+        valueAnimator.start();
+        return valueAnimator;
+    }
+
+    private UpdateListener mUpdateListener;
+    private static class UpdateListener implements ValueAnimator.AnimatorUpdateListener {
+        private WeakReference<CircularRingView> mView;
+        public UpdateListener(CircularRingView view) {
+            mView = new WeakReference<>(view);
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            if (mView == null) {
+                return;
+            }
+            CircularRingView view = mView.get();
+            if (view == null) {
+                return;
+            }
+
+            float value = (float) view.valueAnimator.getAnimatedValue();
+            view.startAngle = 360 * value;
+            view.invalidate();
+        }
+    }
+}
+

+ 97 - 0
mvp/src/main/java/com/miekir/mvp/widget/LoadingView.java

@@ -0,0 +1,97 @@
+package com.miekir.mvp.widget;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.miekir.mvp.R;
+
+
+public class LoadingView {
+    CircularRingView mLoadingView;
+    private TextView loadingText;
+    Dialog mLoadingDialog;
+    Context mContext;
+    private String msg = "加载中...";
+
+    public LoadingView(Context context) {
+        mContext = context;
+        // 首先得到整个View
+        View view = LayoutInflater.from(context).inflate(
+                R.layout.dialog_loading_view, null);
+        // 获取整个布局
+        LinearLayout dialogLayout = (LinearLayout) view.findViewById(R.id.dialog_view);
+        // 页面中的LoadingView
+        mLoadingView = (CircularRingView) view.findViewById(R.id.lv_circularring);
+        // 页面中显示文本
+        loadingText = (TextView) view.findViewById(R.id.loading_text);
+        // 显示文本
+        loadingText.setText(msg);
+        // 创建自定义样式的Dialog
+        mLoadingDialog = new Dialog(context, R.style.loading_dialog);
+        // 设置默认可以取消
+        mLoadingDialog.setCancelable(true);
+        mLoadingDialog.setCanceledOnTouchOutside(false);
+        mLoadingDialog.setContentView(dialogLayout, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
+        mLoadingDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+            @Override
+            public void onCancel(DialogInterface dialog) {
+                close();
+                if (mCancelListener != null) {
+                    mCancelListener.onLoadingCancel();
+                }
+            }
+        });
+    }
+
+
+    public void show() {
+        show(true);
+    }
+
+    public void show(boolean cancelable) {
+        show("", cancelable);
+    }
+
+    public void show(String message) {
+        show(message, true);
+    }
+
+    public void show(String message, boolean cancelable) {
+        if (mLoadingDialog != null) {
+            mLoadingDialog.setCancelable(cancelable);
+            if (!TextUtils.isEmpty(message)) {
+                loadingText.setText(message);
+            }
+
+            if (!mLoadingDialog.isShowing()) {
+                mLoadingDialog.show();
+                mLoadingView.startAnim();
+            }
+        }
+    }
+
+    public void close() {
+        if (mLoadingDialog != null && mLoadingDialog.isShowing()) {
+            mLoadingDialog.dismiss();
+        }
+        mLoadingView.stopAnim();
+    }
+
+    private OnLoadingCancelListener mCancelListener;
+    public void setCancelListener(OnLoadingCancelListener cancelListener) {
+        this.mCancelListener = cancelListener;
+    }
+
+    public interface OnLoadingCancelListener {
+        /**
+         * dismiss不会触发回调,只有按返回cancel才会回调
+         */
+        void onLoadingCancel();
+    }
+}

+ 12 - 0
mvp/src/main/res/drawable/shape_dialog_bg.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 内部颜色 -->
+    <solid android:color="#A61B1B1B" />
+
+    <!-- 圆角的幅度 -->
+    <corners
+        android:bottomLeftRadius="3dp"
+        android:bottomRightRadius="3dp"
+        android:topLeftRadius="3dp"
+        android:topRightRadius="3dp" />
+</shape>

+ 34 - 0
mvp/src/main/res/layout/dialog_loading_view.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/dialog_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:background="@drawable/shape_dialog_bg"
+    android:padding="16dp"
+    android:orientation="vertical">
+
+    <FrameLayout
+        android:layout_width="55dp"
+        android:layout_height="35dp">
+        <com.miekir.mvp.widget.CircularRingView
+            android:id="@+id/lv_circularring"
+            android:layout_width="35dp"
+            android:layout_height="35dp"
+            android:layout_gravity="center"/>
+    </FrameLayout>
+
+
+    <TextView
+        android:id="@+id/loading_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:ellipsize="end"
+        android:textColor="#ffffff"
+        android:layout_marginTop="6dp"
+        android:textSize="11sp"/>
+
+</LinearLayout>
+

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

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

+ 47 - 0
mvp/src/main/res/values/styles.xml

@@ -0,0 +1,47 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <!--<style name="AppTheme" parent="Theme.AppCompat.Light">
+        <item name="windowNoTitle">true</item>
+        <item name="windowActionBar">false</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>-->
+
+    <style name="DialogActivity" parent="android:Theme.Dialog">
+        <!-- 下面这两个是最重要的 -->
+        <item name="android:windowIsFloating">true</item> <!-- 是否浮现在activity之上 (内容会居中显示并靠左右有距离)-->
+        <item name="android:windowNoTitle">true</item> <!-- 无标题 -->
+        <!-- 设置透明下面两个一定要同时设置 -->
+        <item name="android:windowBackground">@android:color/transparent</item> <!-- 背景透明 -->
+        <item name="android:windowIsTranslucent">true</item> <!-- 半透明 -->
+        <item name="android:backgroundDimEnabled">true</item> <!-- 模糊 -->
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowCloseOnTouchOutside">false</item>
+    </style>
+
+    <!--BaseDialog样式-->
+    <style name="BaseDialog" parent="@android:style/Theme.Dialog">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowSoftInputMode">adjustPan</item>
+        <item name="android:windowIsFloating">true</item>
+    </style>
+
+    <style name="common_FullScreenDialogStyle">
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
+    <style name="loading_dialog" parent="android:style/Theme.Dialog">
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:backgroundDimEnabled">false</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>
+
+
+</resources>

+ 17 - 0
mvp/src/test/java/com/miekir/mvp/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.miekir.mvp;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 82 - 0
network/build.gradle

@@ -0,0 +1,82 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion versions.compileSdk
+    buildToolsVersion versions.buildTools
+
+    defaultConfig {
+        minSdkVersion versions.minSdk
+        targetSdkVersion versions.targetSdk
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            buildConfigField("boolean", "IS_DEBUG_MODE", "false")
+        }
+
+        debug {
+            minifyEnabled false
+            buildConfigField("boolean", "IS_DEBUG_MODE", "true")
+        }
+    }
+
+    // 开发与发布的URL
+    flavorDimensions "url"
+    productFlavors {
+        dev {
+            // 开发环境宿舍服务器
+            buildConfigField("String", "BASE_URL", '"http://192.168.0.190:8080/"')
+            buildConfigField("String", "HOST_STATIC_RESOURCE_IMAGE", '"http://192.168.0.190/"')
+        }
+
+        rel {
+            // 正式上线
+            buildConfigField("String", "BASE_URL", '"http://app.icsmabc.cyou:8856/"')
+            buildConfigField("String", "HOST_STATIC_RESOURCE_IMAGE", '"http://icsmabc.cyou/eden/images/"')
+        }
+
+        company {
+            // 开发2
+            buildConfigField("String", "BASE_URL", '"http://10.16.0.153:8080/"')
+            buildConfigField("String", "HOST_STATIC_RESOURCE_IMAGE", '"http://10.16.0.153/"')
+        }
+    }
+}
+
+dependencies {
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+    implementation project(path: ':mvp')
+    implementation project(path: ':common')
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    // 使用 retrofit + rx + gson 实现网络请求与解析start---->
+    // 导入 retrofit
+    api 'com.squareup.retrofit2:retrofit:2.7.1'
+    // 导入 rxjava,此处一定要注意使用RxJava2的版本
+    api "io.reactivex.rxjava2:rxjava:2.2.17"
+    // 导入 rxandroid
+    api 'io.reactivex.rxjava2:rxandroid:2.1.1'
+
+    // 衔接 retrofit 和 rxjava
+    api 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
+    // 衔接 retrofit 和 gson
+    api 'com.squareup.retrofit2:converter-gson:2.2.0'
+    // 使用 retrofit + rx + gson 实现网络请求与解析end<-----
+
+    // 打印网络请求日志框架
+    //implementation 'com.orhanobut:logger:2.2.0'
+    api "com.squareup.okhttp3:logging-interceptor:3.6.0"
+
+    // 调试过程中可以在通知栏查看详细的网络请求
+    debugImplementation 'com.readystatesoftware.chuck:library:1.1.0'
+    releaseImplementation 'com.readystatesoftware.chuck:library-no-op:1.1.0'
+}

+ 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

+ 27 - 0
network/src/androidTest/java/com/miekir/network/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.miekir.network;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.itant.network.test", appContext.getPackageName());
+    }
+}

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

@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.miekir.network">
+    <application>
+        <!--authorities需要是唯一的,随着包名变化而变化,否则一个手机只能有一个项目引入这个库-->
+    </application>
+</manifest>

+ 184 - 0
network/src/main/java/com/miekir/network/RetrofitHelper.java

@@ -0,0 +1,184 @@
+package com.miekir.network;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.miekir.common.utils.ContextManager;
+import com.readystatesoftware.chuck.ChuckInterceptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.logging.HttpLoggingInterceptor;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/1/19 10:58
+ * Description: 请求类,适用于请求链接和配置随时变动
+ * 如果注解里不以/开头,baseURL才可以带路由,如@POST("eden/api/register"),
+ * baseURL为http://www.baidu.com:9999/route/
+ * 所以一般我们在ApiService里不要以/开头
+ */
+public class RetrofitHelper {
+    private static volatile RetrofitHelper mInstance;
+
+    private Context mContext;
+    private Retrofit mRetrofit;
+    private Retrofit mOtherRetrofit;
+
+    private Interceptor mHeaderInterceptor = new Interceptor() {
+        @Override
+        public Response intercept(Chain chain) throws IOException {
+            String token = "";
+            String email = "";
+//            if (EdenManager.getInstance().isLogin()) {
+//                token = EdenManager.getInstance().getBeiUser().token;
+//                email = EdenManager.getInstance().getBeiUser().email;
+//            }
+
+            Request.Builder builder = chain.request().newBuilder();
+            if (!TextUtils.isEmpty(token)) {
+                builder.addHeader("token", token)
+                        .addHeader("email", email);
+            }
+
+            return chain.proceed(builder.build());
+        }
+    };
+
+    private static SSLSocketFactory getSSLContext (Context context){
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            //privatekey.crt证书文件,将它放在Assets目录下
+            InputStream is =  context.getAssets().open("lomsxj.crt");
+
+            Certificate ca = cf.generateCertificate(is);
+            Log.i("getSSLContext ", "ca=" + ((X509Certificate) ca).getSubjectDN());
+
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            keyStore.load(null,null);
+            keyStore.setCertificateEntry("ca",ca);
+
+            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(keyStore);
+
+            SSLContext ssContext = SSLContext.getInstance("TLSv1", "AndroidOpenSSL");
+            ssContext.init(null,tmf.getTrustManagers(),null);
+
+            return ssContext.getSocketFactory();
+
+        } catch (CertificateException | KeyManagementException | NoSuchProviderException | NoSuchAlgorithmException | KeyStoreException | IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    private RetrofitHelper() {
+        mContext = ContextManager.getInstance().getContext();
+
+        // 默认请求的超时等配置
+        OkHttpClient httpClient = getDefaultOkHttpClient();
+
+        // 默认请求的地址、转换规则等配置,Base URL以不能包含路径,只能是http://加上IP或域名
+        mRetrofit = new Retrofit.Builder()
+                .baseUrl(BuildConfig.BASE_URL)
+                .client(httpClient)
+                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+                .addConverterFactory(GsonConverterFactory.create())
+                .build();
+    }
+
+    public static RetrofitHelper getInstance() {
+        if (mInstance == null) {
+            init();
+        }
+        return mInstance;
+    }
+
+    private static synchronized void init() {
+        if (mInstance == null) {
+            mInstance = new RetrofitHelper();
+        }
+    }
+
+    public OkHttpClient getDefaultOkHttpClient(boolean needLogin) {
+        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
+            @Override
+            public void log(String message) {
+                if (BuildConfig.IS_DEBUG_MODE) {
+                    Log.i("Retrofit:", message);
+                }
+            }
+        });
+        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+
+        OkHttpClient.Builder builder = new OkHttpClient.Builder();
+        if (needLogin) {
+            builder.addInterceptor(mHeaderInterceptor);
+        }
+        return  builder
+                .addInterceptor(interceptor)
+                //.sslSocketFactory(getSSLContext(context))
+                .addInterceptor(new ChuckInterceptor(mContext))
+                .retryOnConnectionFailure(true)
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .writeTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(30, TimeUnit.SECONDS)
+                .build();
+    }
+
+    /**
+     *
+     * @return 默认的请求设置
+     */
+    public OkHttpClient getDefaultOkHttpClient() {
+        return getDefaultOkHttpClient(true);
+    }
+
+    /**
+     *
+     * @return 请求接口
+     */
+    public <T> T getRequestApi(Class<T> apiClass) {
+        return mRetrofit.create(apiClass);
+    }
+
+    /**
+     *
+     * @return 请求接口
+     */
+    public <T> T getRequestApi(Class<T> apiClass, String baseUrl) {
+        OkHttpClient httpClient = getDefaultOkHttpClient(false);
+        mOtherRetrofit = new Retrofit.Builder()
+                .baseUrl(baseUrl)
+                .client(httpClient)
+                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+                .addConverterFactory(GsonConverterFactory.create())
+                .build();
+        return mOtherRetrofit.create(apiClass);
+    }
+}

+ 9 - 0
network/src/main/java/com/miekir/network/base/BaseBean.java

@@ -0,0 +1,9 @@
+package com.miekir.network.base;
+
+/**
+ * 公共返回内容
+ * @author Miekir
+ */
+
+public class BaseBean {
+}

+ 55 - 0
network/src/main/java/com/miekir/network/base/BaseResponse.java

@@ -0,0 +1,55 @@
+package com.miekir.network.base;
+
+/**
+ * 统一返回封装
+ * @author Miekir
+ */
+public class BaseResponse<T> {
+    private int code;
+    private String message;
+    private String extra;
+    private T content;
+
+    /**
+     *
+     * @return 返回状态代码
+     */
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    /**
+     * @return 返回数据
+     */
+    public T getContent() {
+        return content;
+    }
+
+    public void setContent(T content) {
+        this.content = content;
+    }
+
+    /**
+     *
+     * @return 返回信息
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public String getExtra() {
+        return extra;
+    }
+
+    public void setExtra(String extra) {
+        this.extra = extra;
+    }
+}

+ 17 - 0
network/src/main/java/com/miekir/network/constant/Code.java

@@ -0,0 +1,17 @@
+package com.miekir.network.constant;
+
+/**
+ * 请求码
+ * @author Miekir
+ */
+
+public class Code {
+
+    public static final int FAILURE = -1;
+    public static final int SUCCESS = 0;
+
+    /**
+     * 登录token超时
+     */
+    public static final int TOKEN_TIMEOUT = 10005;
+}

+ 16 - 0
network/src/main/java/com/miekir/network/constant/RequestConst.java

@@ -0,0 +1,16 @@
+package com.miekir.network.constant;
+
+/**
+ *
+ *
+ * @author Miekir
+ * @date 2020/1/19 11:27
+ * Description: 请求相关的常量
+ */
+public interface RequestConst {
+    /**
+     * 请求域名
+     * RetrofitHelper.getInstance().setRequestHost()
+     */
+    String BASE_URL = "http://192.168.0.190:8080";
+}

+ 46 - 0
network/src/main/java/com/miekir/network/utils/ExceptionUtil.java

@@ -0,0 +1,46 @@
+package com.miekir.network.utils;
+
+import android.net.ParseException;
+
+import org.json.JSONException;
+
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+import retrofit2.HttpException;
+
+/**
+ * 异常信息
+ * @author Miekir
+ */
+
+public class ExceptionUtil {
+    public static String exceptionHandler(Throwable e) {
+        String errorMsg = "未知错误";
+        if (e instanceof UnknownHostException) {
+            errorMsg = "网络不可用";
+        } else if (e instanceof SocketTimeoutException) {
+            errorMsg = "请求网络超时";
+        } else if (e instanceof HttpException) {
+            HttpException httpException = (HttpException) e;
+            errorMsg = convertStatusCode(httpException);
+        } else if (e instanceof ParseException || e instanceof JSONException) {
+            errorMsg = "数据解析错误";
+        }
+        return errorMsg;
+    }
+
+    public static String convertStatusCode(HttpException httpException) {
+        String msg;
+        if (httpException.code() >= 500 && httpException.code() < 600) {
+            msg = "服务器处理请求出错";
+        } else if (httpException.code() >= 400 && httpException.code() < 500) {
+            msg = "服务器无法处理请求";
+        } else if (httpException.code() >= 300 && httpException.code() < 400) {
+            msg = "请求被重定向到其他页面";
+        } else {
+            msg = httpException.message();
+        }
+        return msg;
+    }
+}

+ 44 - 0
network/src/main/java/com/miekir/network/utils/JsonTool.java

@@ -0,0 +1,44 @@
+package com.miekir.network.utils;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 网络工具类
+ * @author Miekir
+ */
+public class JsonTool {
+
+    /**
+     * @param params  请求参数
+     * @return 请求体
+     */
+    public static JsonObject getJsonFromKeyValueList(Map<String, Object> params) {
+        JsonObject contentObject = new JsonObject();
+        if (params == null || params.size() == 0) {
+            return contentObject;
+        }
+
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            if (entry.getValue() instanceof String) {
+                contentObject.addProperty(entry.getKey(), String.valueOf(entry.getValue()));
+            } else if (entry.getValue() instanceof Number) {
+                contentObject.addProperty(entry.getKey(), (Number) entry.getValue());
+            } else if (entry.getValue() instanceof Boolean) {
+                contentObject.addProperty(entry.getKey(), (Boolean) entry.getValue());
+            } else if (entry.getValue() instanceof List<?>) {
+                List<?> list = (List<?>) entry.getValue();
+                JsonArray jsonArray = new Gson().toJsonTree(list, new TypeToken<List<?>>() {
+                }.getType()).getAsJsonArray();
+                contentObject.add(entry.getKey(), jsonArray);
+            }
+        }
+
+        return contentObject;
+    }
+}

+ 42 - 0
network/src/main/java/com/miekir/network/widget/CustomProgressDialog.java

@@ -0,0 +1,42 @@
+package com.miekir.network.widget;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import com.miekir.network.R;
+
+/**
+ * 网络请求进度条
+ * @author Miekir
+ */
+
+public class CustomProgressDialog extends Dialog {
+    private static final String TAG = "CustomProgressDialog";
+
+    public CustomProgressDialog(Context context, String strMessage) {
+        this(context, R.style.NetworkCustomProgressDialog, strMessage);
+    }
+
+    private CustomProgressDialog(Context context, int theme, String strMessage) {
+        super(context, theme);
+        this.setContentView(R.layout.dialog_net_loading);
+        this.getWindow().getAttributes().gravity = Gravity.CENTER;
+        TextView tvMsg = (TextView) this.findViewById(R.id.network_progress_text);
+        if (tvMsg != null) {
+            tvMsg.setText(strMessage);
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+    }
+
+    public void setMessage(String msg) {
+        TextView tvMsg = (TextView) this.findViewById(R.id.network_progress_text);
+        if (tvMsg != null) {
+            tvMsg.setText(msg);
+        }
+    }
+}

+ 66 - 0
network/src/main/java/com/miekir/network/widget/observe/MvpObserver.java

@@ -0,0 +1,66 @@
+package com.miekir.network.widget.observe;
+
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.network.constant.Code;
+import com.miekir.network.utils.ExceptionUtil;
+
+import io.reactivex.observers.DisposableObserver;
+
+/**
+ * 进度条观察者
+ * 需要进度条时,调用onSubscribe同时显示进度条,
+ * 流程走到onComplete/onError时隐藏进度条。
+ * @author Miekir
+ * 用于可见,可手动取消的网络任务
+ */
+public abstract class MvpObserver<T> extends DisposableObserver<T> {
+    private boolean mWithLoading;
+    private BasePresenter<?> mPresenter;
+
+    public MvpObserver(final BasePresenter<?> presenter) {
+        this(presenter, false);
+    }
+
+    public MvpObserver(final BasePresenter<?> presenter, final boolean withLoading) {
+        mWithLoading = withLoading;
+        mPresenter = presenter;
+        if (mPresenter != null && withLoading) {
+            presenter.showProgress();
+        }
+    }
+
+    @Override
+    public void onNext(T t) {
+        if (mPresenter != null && mWithLoading) {
+            mPresenter.hideProgress();
+        }
+        onSuccess(Code.SUCCESS, t);
+    }
+
+    @Override
+    public void onComplete() {}
+
+    @Override
+    public void onError(Throwable e) {
+        if (mWithLoading) {
+            mPresenter.hideProgress();
+        }
+        e.printStackTrace();
+        onFailure(Code.FAILURE, e, ExceptionUtil.exceptionHandler(e));
+    }
+
+    /**
+     * 成功回调
+     * @param code 返回码
+     * @param t 返回的实体
+     */
+    public abstract void onSuccess(int code, T t);
+
+    /**
+     * 失败回调
+     * @param code 返回码
+     * @param e 异常
+     * @param errMsg 异常信息
+     */
+    public abstract void onFailure(int code, Throwable e, String errMsg);
+}

+ 78 - 0
network/src/main/java/com/miekir/network/widget/observe/NetMvpObserver.java

@@ -0,0 +1,78 @@
+package com.miekir.network.widget.observe;
+
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.network.base.BaseResponse;
+import com.miekir.network.constant.Code;
+import com.miekir.network.utils.ExceptionUtil;
+
+import io.reactivex.annotations.NonNull;
+import io.reactivex.observers.DisposableObserver;
+
+/**
+ * 返回结果处理
+ * @author Miekir
+ * 没加载框,可手动取消的网络任务
+ */
+public abstract class NetMvpObserver<T> extends DisposableObserver<BaseResponse<T>> {
+
+    private boolean mWithLoading;
+    private BasePresenter<?> mPresenter;
+
+    public NetMvpObserver(final BasePresenter<?> presenter) {
+        this(presenter, false);
+    }
+
+    public NetMvpObserver(final BasePresenter<?> presenter, final boolean withLoading) {
+        mWithLoading = withLoading;
+        mPresenter = presenter;
+        if (withLoading) {
+            presenter.showProgress();
+        }
+    }
+
+    @Override
+    public void onNext(@NonNull BaseResponse<T> response) {
+        if (mPresenter != null && mWithLoading) {
+            mPresenter.hideProgress();
+        }
+        try {
+            if (response.getCode() == Code.SUCCESS) {
+                onSuccess(response.getCode(), response.getContent());
+            } else if (response.getCode() == Code.TOKEN_TIMEOUT) {
+                // 重新登录
+            } else {
+                onFailure(response.getCode(), new Exception(response.getMessage()), response.getMessage());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            onFailure(Code.FAILURE, new Exception("null"), null);
+        }
+    }
+
+    @Override
+    public void onComplete() {}
+
+    @Override
+    public void onError(@NonNull Throwable e) {
+        if (mPresenter != null && mWithLoading) {
+            mPresenter.hideProgress();
+        }
+        e.printStackTrace();
+        onFailure(Code.FAILURE, e, ExceptionUtil.exceptionHandler(e));
+    }
+
+    /**
+     * 成功
+     * @param code
+     * @param result
+     */
+    public abstract void onSuccess(int code, T result);
+
+    /**
+     * 失败
+     * @param code
+     * @param e
+     * @param errMsg
+     */
+    public abstract void onFailure(int code, Throwable e, String errMsg);
+}

+ 19 - 0
network/src/main/java/com/miekir/network/widget/observe/cancelable/CancelableConsumer.java

@@ -0,0 +1,19 @@
+package com.miekir.network.widget.observe.cancelable;
+
+import io.reactivex.functions.Consumer;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/21 17:40
+ * Description:
+ */
+public abstract class CancelableConsumer<T> implements Consumer<T> {
+    @Override
+    public void accept(T t) throws Exception {
+        onResult(t);
+    }
+
+    public abstract void onResult(T t);
+}

+ 70 - 0
network/src/main/java/com/miekir/network/widget/observe/cancelable/CancelableObserver.java

@@ -0,0 +1,70 @@
+package com.miekir.network.widget.observe.cancelable;
+
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.network.constant.Code;
+import com.miekir.network.utils.ExceptionUtil;
+
+import io.reactivex.internal.functions.Functions;
+import io.reactivex.internal.observers.LambdaObserver;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/21 17:25
+ * Description:
+ * 可手动取消的任务(可见可不见)
+ */
+public abstract class CancelableObserver<T> {
+    private CancelableConsumer<? super T> onNext;
+    private CancelableConsumer<? super Throwable> onError;
+
+    public CancelableObserver(final BasePresenter<?> presenter) {
+        this(presenter, false);
+    }
+
+    public CancelableObserver(final BasePresenter<?> presenter, final boolean withLoading) {
+        if (withLoading) {
+            presenter.showProgress();
+        }
+
+        onNext = new CancelableConsumer<T>() {
+            @Override
+            public void onResult(T t) {
+                if (withLoading) {
+                    presenter.hideProgress();
+                }
+                onSuccess(Code.SUCCESS, t);
+            }
+        };
+
+        onError = new CancelableConsumer<Throwable>() {
+            @Override
+            public void onResult(Throwable throwable) {
+                if (withLoading) {
+                    presenter.hideProgress();
+                }
+                onFailure(Code.FAILURE, throwable, ExceptionUtil.exceptionHandler(throwable));
+            }
+        };
+    }
+
+    /**
+     * 成功回调
+     * @param code 返回码
+     * @param t 返回的实体
+     */
+    public abstract void onSuccess(int code, T t);
+
+    /**
+     * 失败回调
+     * @param code 返回码
+     * @param e 异常
+     * @param errMsg 异常信息
+     */
+    public abstract void onFailure(int code, Throwable e, String errMsg);
+
+    public LambdaObserver<T> getDisposal() {
+        return new LambdaObserver<T>(onNext, onError, Functions.EMPTY_ACTION, Functions.emptyConsumer());
+    }
+}

+ 83 - 0
network/src/main/java/com/miekir/network/widget/observe/cancelable/NetCancelableObserver.java

@@ -0,0 +1,83 @@
+package com.miekir.network.widget.observe.cancelable;
+
+import com.miekir.mvp.presenter.BasePresenter;
+import com.miekir.network.base.BaseResponse;
+import com.miekir.network.constant.Code;
+import com.miekir.network.utils.ExceptionUtil;
+
+import io.reactivex.internal.functions.Functions;
+import io.reactivex.internal.observers.LambdaObserver;
+
+/**
+ * Copyright (C), 2019-2020, Miekir
+ *
+ * @author Miekir
+ * @date 2020/11/21 17:25
+ * Description:
+ * 可手动取消的网络任务(可见可不见)
+ */
+public abstract class NetCancelableObserver<T> {
+    private CancelableConsumer<? super BaseResponse<T>> onNext;
+    private CancelableConsumer<? super Throwable> onError;
+
+    public NetCancelableObserver(final BasePresenter<?> presenter) {
+        this(presenter, false);
+    }
+
+    public NetCancelableObserver(final BasePresenter<?> presenter, final boolean withLoading) {
+        if (withLoading) {
+            presenter.showProgress();
+        }
+
+        onNext = new CancelableConsumer<BaseResponse<T>>() {
+            @Override
+            public void onResult(BaseResponse<T> response) {
+                if (withLoading) {
+                    presenter.hideProgress();
+                }
+
+                try {
+                    if (response.getCode() == Code.SUCCESS) {
+                        onSuccess(response.getCode(), response.getContent());
+                    } else if (response.getCode() == Code.TOKEN_TIMEOUT) {
+                        // 重新登录
+                    } else {
+                        onFailure(response.getCode(), new Exception(response.getMessage()), response.getMessage());
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    onFailure(Code.FAILURE, new Exception("null"), null);
+                }
+            }
+        };
+
+        onError = new CancelableConsumer<Throwable>() {
+            @Override
+            public void onResult(Throwable throwable) {
+                if (withLoading) {
+                    presenter.hideProgress();
+                }
+                onFailure(Code.FAILURE, throwable, ExceptionUtil.exceptionHandler(throwable));
+            }
+        };
+    }
+
+    /**
+     * 成功回调
+     * @param code 返回码
+     * @param t 返回的实体
+     */
+    public abstract void onSuccess(int code, T t);
+
+    /**
+     * 失败回调
+     * @param code 返回码
+     * @param e 异常
+     * @param errMsg 异常信息
+     */
+    public abstract void onFailure(int code, Throwable e, String errMsg);
+
+    public LambdaObserver<BaseResponse<T>> getDisposal() {
+        return new LambdaObserver<BaseResponse<T>>(onNext, onError, Functions.EMPTY_ACTION, Functions.emptyConsumer());
+    }
+}

+ 67 - 0
network/src/main/java/com/miekir/network/widget/observe/sticky/NetStickyObserver.java

@@ -0,0 +1,67 @@
+package com.miekir.network.widget.observe.sticky;
+
+import com.miekir.network.base.BaseResponse;
+import com.miekir.network.constant.Code;
+import com.miekir.network.utils.ExceptionUtil;
+
+import io.reactivex.Observer;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * 返回结果处理
+ * @author Miekir
+ * 没加载框,不可手动取消的网络任务,直到Activity销毁
+ * @deprecated 使用cancelable的观察者来更好控制任务和资源回收
+ * {@link com.miekir.network.widget.observe.MvpObserver}
+ */
+@Deprecated
+public abstract class NetStickyObserver<T> implements Observer<BaseResponse<T>> {
+
+    @Override
+    public void onNext(@NonNull BaseResponse<T> response) {
+        try {
+            if (response.getCode() == Code.SUCCESS) {
+                onSuccess(response.getCode(), response.getContent());
+            } else if (response.getCode() == Code.TOKEN_TIMEOUT) {
+                // 重新登录
+            } else {
+                onFailure(response.getCode(), new Exception(response.getMessage()), response.getMessage());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            onFailure(Code.FAILURE, new Exception("null"), null);
+        }
+    }
+
+    @Override
+    public void onError(@NonNull Throwable e) {
+        e.printStackTrace();
+        onFailure(Code.FAILURE, e, ExceptionUtil.exceptionHandler(e));
+    }
+
+    @Override
+    public void onComplete() {
+
+    }
+
+    @Override
+    public void onSubscribe(@NonNull Disposable d) {
+
+    }
+
+    /**
+     * 成功
+     * @param code
+     * @param result
+     */
+    public abstract void onSuccess(int code, T result);
+
+    /**
+     * 失败
+     * @param code
+     * @param e
+     * @param errMsg
+     */
+    public abstract void onFailure(int code, Throwable e, String errMsg);
+}

+ 63 - 0
network/src/main/java/com/miekir/network/widget/observe/sticky/ProgressNetStickyObserver.java

@@ -0,0 +1,63 @@
+package com.miekir.network.widget.observe.sticky;
+
+import android.content.Context;
+
+import com.miekir.network.widget.CustomProgressDialog;
+
+import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * 进度条观察者
+ * 需要进度条时,调用onSubscribe同时显示进度条,
+ * 流程走到onComplete/onError时隐藏进度条。
+ * @author Miekir
+ * 用于可见,但不可手动取消的网络任务
+ * @deprecated 使用cancelable的观察者来更好控制任务和资源回收
+ * {@link com.miekir.network.widget.observe.MvpObserver}
+ */
+@Deprecated
+public abstract class ProgressNetStickyObserver<T> extends NetStickyObserver<T> {
+    private CustomProgressDialog mProgressDialog;
+    private Context mContext;
+    private String mText;
+
+    public ProgressNetStickyObserver(Context context) {
+        this(context, "");
+    }
+
+    public ProgressNetStickyObserver(Context context, String text) {
+        this.mContext = context;
+        this.mText = text;
+    }
+
+    @Override
+    public void onSubscribe(@NonNull Disposable d) {
+        super.onSubscribe(d);
+        if (d.isDisposed()) {
+            return;
+        }
+        if (mProgressDialog == null) {
+            mProgressDialog = new CustomProgressDialog(mContext, mText);
+            mProgressDialog.setCancelable(false);
+            mProgressDialog.setCanceledOnTouchOutside(false);
+        }
+        mProgressDialog.show();
+    }
+
+    @Override
+    public void onComplete() {
+        super.onComplete();
+        if (mProgressDialog != null) {
+            mProgressDialog.dismiss();
+        }
+    }
+
+    @Override
+    public void onError(@NonNull Throwable e) {
+        super.onError(e);
+        if (mProgressDialog != null) {
+            mProgressDialog.dismiss();
+        }
+    }
+}

+ 8 - 0
network/src/main/res/drawable/bg_dialog_net_loading.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <corners android:radius="10dp" />
+    <solid android:color="#ffffff" />
+
+</shape>

+ 31 - 0
network/src/main/res/layout/dialog_net_loading.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/bg_dialog_net_loading"
+    android:gravity="center"
+    android:minWidth="110dp"
+    android:orientation="vertical"
+    android:paddingBottom="5dp"
+    android:paddingLeft="10dp"
+    android:paddingRight="10dp"
+    android:paddingTop="10dp">
+
+    <!-- 暂时去掉
+        android:indeterminateDrawable="@drawable/loading_progress_pic_list_bg"该代码 -->
+    <ProgressBar
+        android:id="@+id/network_progress_bar"
+        style="?android:attr/progressBarStyleLarge"
+        android:layout_width="50dp"
+        android:layout_height="50dp"
+        android:layout_gravity="center" />
+
+    <TextView
+        android:id="@+id/network_progress_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="5dp"
+        android:textColor="#999999"
+        android:textSize="15sp"
+        android:visibility="gone"/>
+</LinearLayout>

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

@@ -0,0 +1,3 @@
+<resources>
+    <string name="network_loading">Loading…</string>
+</resources>

+ 19 - 0
network/src/main/res/values/styles.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="NetworkMyDialogStyle" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+        <item name="android:backgroundDimEnabled">true</item>
+    </style>
+
+    <!--进度条提示框-->
+    <style name="NetworkCustomProgressDialog" parent="@style/NetworkMyDialogStyle">
+        <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
+    </style>
+</resources>

+ 17 - 0
network/src/test/java/com/miekir/network/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.miekir.network;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 3 - 0
settings.gradle

@@ -1,2 +1,5 @@
+include ':network'
+include ':mvp'
+include ':common'
 include ':app'
 rootProject.name = "YangMao"