Przeglądaj źródła

v1.0.1开发:迁移融合的浮标实现

#Suyghur 3 lat temu
rodzic
commit
8b945b186c
100 zmienionych plików z 5449 dodań i 40 usunięć
  1. 1 0
      build.gradle
  2. 8 5
      demo/src/main/java/com/yyxx/support/demo/DemoActivity.kt
  3. 5 5
      demo/src/main/java/com/yyxx/support/demo/DemoApplication.kt
  4. 1 2
      demo/src/main/java/com/yyxx/support/demo/FloatView.kt
  5. 44 8
      demo/src/main/java/com/yyxx/support/demo/FloatViewService.kt
  6. 2 0
      demo/src/main/java/com/yyxx/support/demo/FloatViewServiceManager.kt
  7. BIN
      demo/src/main/res/drawable-xhdpi/float_icon.png
  8. 8 0
      demo/src/main/res/drawable/yyxx_float_menu_bg.xml
  9. 1 1
      gradle.properties
  10. 8 1
      library_support/build.gradle
  11. 1 1
      library_support/buildJar.gradle
  12. 6 1
      library_support/proguard-rules.pro
  13. 17 2
      library_support/src/main/java/cn/yyxx/support/AppUtils.java
  14. 89 0
      library_support/src/main/java/cn/yyxx/support/ReflectUtils.java
  15. 8 0
      library_support/src/main/java/cn/yyxx/support/ResUtils.java
  16. 165 0
      library_support/src/main/java/cn/yyxx/support/StrUtils.java
  17. 10 4
      library_support/src/main/java/cn/yyxx/support/device/DeviceInfoUtils.java
  18. 29 0
      library_support/src/main/java/cn/yyxx/support/encryption/HexUtils.java
  19. 140 0
      library_support/src/main/java/cn/yyxx/support/gaid/GAIDUtils.java
  20. 143 0
      library_support/src/main/java/cn/yyxx/support/gaid/GooglePlayServicesClient.java
  21. 9 0
      library_support/src/main/java/cn/yyxx/support/gaid/OnDeviceIdsRead.java
  22. 488 0
      library_support/src/main/java/cn/yyxx/support/multidex/MultiDex.java
  23. 323 0
      library_support/src/main/java/cn/yyxx/support/multidex/MultiDexExtractor.java
  24. 95 0
      library_support/src/main/java/cn/yyxx/support/multidex/ZipUtil.java
  25. 19 0
      library_support/src/main/java/cn/yyxx/support/scheduler/FutureScheduler.java
  26. 25 0
      library_support/src/main/java/cn/yyxx/support/scheduler/RunnableWrapper.java
  27. 67 0
      library_support/src/main/java/cn/yyxx/support/scheduler/SingleThreadFutureScheduler.java
  28. 39 0
      library_support/src/main/java/cn/yyxx/support/scheduler/ThreadFactoryWrapper.java
  29. 11 10
      library_support/src/main/java/cn/yyxx/support/ui/DragViewLayout.java
  30. 394 0
      library_support/src/main/java/cn/yyxx/support/ui/floating/DotImageView.java
  31. 73 0
      library_support/src/main/java/cn/yyxx/support/ui/floating/FloatItem.java
  32. 1036 0
      library_support/src/main/java/cn/yyxx/support/ui/floating/FloatLogoMenu.java
  33. 413 0
      library_support/src/main/java/cn/yyxx/support/ui/floating/FloatMenuView.java
  34. 1 0
      library_volley/.gitignore
  35. 47 0
      library_volley/build.gradle
  36. 17 0
      library_volley/buildJar.gradle
  37. 21 0
      library_volley/proguard-rules.pro
  38. 5 0
      library_volley/src/main/AndroidManifest.xml
  39. 0 0
      library_volley/src/main/java/android/support/annotation/GuardedBy.java
  40. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/AuthFailureError.java
  41. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/Cache.java
  42. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/CacheDispatcher.java
  43. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/ClientError.java
  44. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/DefaultRetryPolicy.java
  45. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/ExecutorDelivery.java
  46. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/Header.java
  47. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/Network.java
  48. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/NetworkDispatcher.java
  49. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/NetworkError.java
  50. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/NetworkResponse.java
  51. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/NoConnectionError.java
  52. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/ParseError.java
  53. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/Request.java
  54. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/RequestQueue.java
  55. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/Response.java
  56. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/ResponseDelivery.java
  57. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/RetryPolicy.java
  58. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/ServerError.java
  59. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/TimeoutError.java
  60. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/VolleyError.java
  61. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/VolleyLog.java
  62. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/AdaptedHttpStack.java
  63. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/AndroidAuthenticator.java
  64. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/Authenticator.java
  65. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/BaseHttpStack.java
  66. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/BasicNetwork.java
  67. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ByteArrayPool.java
  68. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ClearCacheRequest.java
  69. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/DiskBasedCache.java
  70. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpClientStack.java
  71. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpHeaderParser.java
  72. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpResponse.java
  73. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpStack.java
  74. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HurlStack.java
  75. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageLoader.java
  76. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageRequest.java
  77. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonArrayRequest.java
  78. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonObjectRequest.java
  79. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonRequest.java
  80. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/NetworkImageView.java
  81. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/NoCache.java
  82. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/PoolingByteArrayOutputStream.java
  83. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/RequestFuture.java
  84. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/StringRequest.java
  85. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/Threads.java
  86. 0 0
      library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/Volley.java
  87. 1 0
      library_volleyx/.gitignore
  88. 48 0
      library_volleyx/build.gradle
  89. 17 0
      library_volleyx/buildJar.gradle
  90. 21 0
      library_volleyx/proguard-rules.pro
  91. 5 0
      library_volleyx/src/main/AndroidManifest.xml
  92. 89 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/AsyncCache.java
  93. 156 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/AsyncNetwork.java
  94. 676 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/AsyncRequestQueue.java
  95. 56 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/AuthFailureError.java
  96. 114 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/Cache.java
  97. 220 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/CacheDispatcher.java
  98. 34 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/ClientError.java
  99. 121 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/DefaultRetryPolicy.java
  100. 122 0
      library_volleyx/src/main/java/cn/yyxx/support/volley/source/ExecutorDelivery.java

+ 1 - 0
build.gradle

@@ -1,6 +1,7 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 buildscript {
 
+    ext.USE_ANDROIDX_VOLLEY=true
     // 混淆开关
     ext.MINIFY_ENABLE = true
     // ndk版本

+ 8 - 5
demo/src/main/java/com/yyxx/support/demo/DemoActivity.kt

@@ -112,13 +112,16 @@ class DemoActivity : Activity(), View.OnClickListener {
                 }
                 2 -> requestImg()
                 3 -> FloatViewServiceManager.getInstance().attach()
+
                 4 -> FloatViewServiceManager.getInstance().detach()
-                5 -> {
-                    MMKV.defaultMMKV()!!.encode("test", "yyxx support")
-                    MMKV.defaultMMKV()!!.encode("test1", "yyxx support1")
-                    MMKV.defaultMMKV()!!.encode("test2", "yyxx support2")
-                    MMKV.defaultMMKV()!!.encode("test3", "yyxx support3")
 
+                5 -> {
+//                    MMKV.defaultMMKV()!!.encode("test", "yyxx support")
+//                    MMKV.defaultMMKV()!!.encode("test1", "yyxx support1")
+//                    MMKV.defaultMMKV()!!.encode("test2", "yyxx support2")
+//                    MMKV.defaultMMKV()!!.encode("test3", "yyxx support3")
+//                    val text =
+//                        "eFeiSQvEaVfyAmbsKfYpHjK/g3VFQ2lzHaLMv7f2yKXCoka0wGE6zp/4y6REvnpjspBn81Gya+yi3Q3MV3h3csxF0QA2ebKy+ytV3Lmwb5RUx/F5ps01wZ83QkVa2WpxzDG1zBaT6NxnfDXO2oL0J+6d4/E82fbEt0kwvO0KyfU="
                 }
                 6 -> {
 //                    sb.append("MMKV decode : ").append(MMKV.defaultMMKV()!!.decodeString("test"))

+ 5 - 5
demo/src/main/java/com/yyxx/support/demo/DemoApplication.kt

@@ -14,15 +14,15 @@ class DemoApplication : Application() {
 
     override fun attachBaseContext(base: Context?) {
         super.attachBaseContext(base)
-        MsaDeviceIdsHandler.initMsaDeviceIds(this) { code, msg, _ ->
-            LogUtils.i("initMsaDeviceIds code : $code , msg : $msg")
-        }
+//        MsaDeviceIdsHandler.initMsaDeviceIds(this) { code, msg, _ ->
+//            LogUtils.i("initMsaDeviceIds code : $code , msg : $msg")
+//        }
 
     }
 
     override fun onCreate() {
         super.onCreate()
-        val dir = MMKV.initialize(this)
-        LogUtils.i("mmkv dir : $dir")
+//        val dir = MMKV.initialize(this)
+//        LogUtils.i("mmkv dir : $dir")
     }
 }

+ 1 - 2
demo/src/main/java/com/yyxx/support/demo/FloatView.kt

@@ -1,6 +1,5 @@
 package com.yyxx.support.demo
 
-import android.app.Activity
 import android.content.Context
 import android.view.ViewGroup
 import android.widget.ImageView
@@ -19,7 +18,7 @@ class FloatView(context: Context) : DragViewLayout(context) {
     init {
         isClickable = true
         imageView = ImageView(context)
-        imageView.setBackgroundResource(ResUtils.getResId(context, "icon", "drawable"))
+        imageView.setBackgroundResource(ResUtils.getResId(context, "float_icon", "drawable"))
         imageView.setOnClickListener {
             LogUtils.d("点击了DemoFloatView")
         }

+ 44 - 8
demo/src/main/java/com/yyxx/support/demo/FloatViewService.kt

@@ -2,11 +2,15 @@ package com.yyxx.support.demo
 
 import android.app.Activity
 import android.app.Service
-import android.content.Context
 import android.content.Intent
+import android.graphics.BitmapFactory
 import android.os.Binder
 import android.os.IBinder
+import cn.yyxx.support.ResUtils
 import cn.yyxx.support.hawkeye.LogUtils
+import cn.yyxx.support.ui.floating.FloatItem
+import cn.yyxx.support.ui.floating.FloatLogoMenu
+import cn.yyxx.support.ui.floating.FloatMenuView
 
 /**
  * @author #Suyghur.
@@ -15,26 +19,58 @@ import cn.yyxx.support.hawkeye.LogUtils
 class FloatViewService : Service() {
 
     private lateinit var activity: Activity
-    private var floatView: FloatView? = null
+    private var floatView: FloatLogoMenu? = null
 
     override fun onCreate() {
         super.onCreate()
     }
 
 
-    fun initFloatView(activity:Activity) {
+    fun initFloatView(activity: Activity) {
         this.activity = activity
         LogUtils.d("init")
     }
 
     fun show() {
         if (floatView == null) {
-            floatView = FloatView(activity)
-        }
-        if (floatView == null) {
-            LogUtils.d("aaaa")
+            val features = mutableListOf(
+                FloatItem(
+                    "aaaa", "#1DB1AD",
+                    "#000000", BitmapFactory.decodeResource(activity.resources, ResUtils.getResId(activity, "float_icon", "drawable"))
+                ),
+                FloatItem(
+                    "aaaa", "#1DB1AD",
+                    "#000000", BitmapFactory.decodeResource(activity.resources, ResUtils.getResId(activity, "float_icon", "drawable"))
+                ),
+                FloatItem(
+                    "aaaa", "#1DB1AD",
+                    "#000000", BitmapFactory.decodeResource(activity.resources, ResUtils.getResId(activity, "float_icon", "drawable"))
+                ),
+                FloatItem(
+                    "aaaa", "#1DB1AD",
+                    "#000000", BitmapFactory.decodeResource(activity.resources, ResUtils.getResId(activity, "float_icon", "drawable"))
+                )
+            )
+            floatView = FloatLogoMenu.Builder()
+                .withActivity(activity)
+                .logo(BitmapFactory.decodeResource(activity.resources, ResUtils.getResId(activity, "float_icon", "drawable")))
+                .drawCicleMenuBg(true)
+                .backMenuColor("#FFFFFF")
+                .setBgDrawable(activity.resources.getDrawable(ResUtils.getResId(activity, "yyxx_float_menu_bg", "drawable")))
+                //这个背景色需要和logo的背景色一致
+                .setFloatItems(features)
+                .defaultLocation(FloatLogoMenu.LEFT)
+                .drawRedPointNum(false)
+                .showWithListener(object : FloatMenuView.OnMenuClickListener {
+                    override fun onItemClick(position: Int, title: String?) {
+                    }
+
+                    override fun dismiss() {
+                    }
+
+                })
         }
-        LogUtils.d("show")
+
         floatView?.show()
     }
 

+ 2 - 0
demo/src/main/java/com/yyxx/support/demo/FloatViewServiceManager.kt

@@ -6,6 +6,7 @@ import android.content.Context
 import android.content.Intent
 import android.content.ServiceConnection
 import android.os.IBinder
+import cn.yyxx.support.hawkeye.LogUtils
 import com.yyxx.support.demo.FloatViewService.FloatViewServiceBinder
 
 /**
@@ -51,6 +52,7 @@ class FloatViewServiceManager {
     }
 
     fun release() {
+        LogUtils.d("release")
         mService?.release()
         if (isBindService) {
             mActivity?.apply {

BIN
demo/src/main/res/drawable-xhdpi/float_icon.png


+ 8 - 0
demo/src/main/res/drawable/yyxx_float_menu_bg.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="85dp" />
+    <solid android:color="#FFFFFF" />
+    <stroke
+        android:width="0.5dp"
+        android:color="#1DB1AD" />
+</shape>

+ 1 - 1
gradle.properties

@@ -12,6 +12,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
 # org.gradle.parallel=true
 android.injected.testOnly=false
-#android.useAndroidX=true
+android.useAndroidX=true
 #android.enableJetifier=true
 #android.enableAapt2=false

+ 8 - 1
library_support/build.gradle

@@ -42,7 +42,14 @@ android {
 
 dependencies {
     compileOnly files('../libs/oaid_sdk_1.0.25.jar')
-    compileOnly files('../libs/android-support-v4.jar')
+    if (USE_ANDROIDX_VOLLEY) {
+        implementation "org.chromium.net:cronet-embedded:76.3809.111"
+        implementation 'androidx.core:core:1.5.0'
+        api files('../libs/yyxx_support_volleyx_1.0.0.jar')
+    } else {
+        implementation files('../libs/android-support-v4.jar')
+        api files('../libs/yyxx_support_volley_1.0.0.jar')
+    }
 }
 
 apply from: 'buildJar.gradle'

+ 1 - 1
library_support/buildJar.gradle

@@ -1,5 +1,5 @@
 def SDK_BASE_NAME = 'yyxx_support'
-def SDK_VERSION = '1.0.0'
+def SDK_VERSION = '1.0.1'
 def SEPARATOR = '_'
 def SDK_DST_PATH = 'build/jar/'
 def ZIP_FILE = file('build/intermediates/aar_main_jar/release/classes.jar')

+ 6 - 1
library_support/proguard-rules.pro

@@ -118,13 +118,18 @@
 -keep class cn.yyxx.support.device.DeviceInfoUtils{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.DensityUtils{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.JsonUtils{ public <fields>;public <methods>;}
--keep class cn.yyxx.support.ui.**{ public <fields>; public <methods>;}
+-keep class cn.yyxx.support.ui.**{ *;}
 -keep class cn.yyxx.support.encryption.**{ public <fields>; public <methods>;}
 -keep class cn.yyxx.support.FileUtils{ public <fields>;public <methods>;}
+-keep class cn.yyxx.support.ReflectUtils{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.HostModelUtils{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.PropertiesUtils{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.ResUtils{ public <fields>;public <methods>;}
+-keep class cn.yyxx.support.StrUtils{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.scheduler.**{ public <fields>;public <methods>;}
 -keep class cn.yyxx.support.msa.**{ public <fields>; public <methods>;}
+-keep class cn.yyxx.support.gaid.**{ public <fields>; public <methods>;}
 -keep class cn.yyxx.support.hawkeye.**{ public <fields>; public <methods>;}
+-keep class cn.yyxx.support.cache.**{ public <fields>; public <methods>;}
+-keep class cn.yyxx.support.multidex.**{ public <fields>; public <methods>;}
 -keep class cn.yyxx.support.volley.**{ *;}

+ 17 - 2
library_support/src/main/java/cn/yyxx/support/AppUtils.java

@@ -69,19 +69,34 @@ public class AppUtils {
         return 0;
     }
 
+    /**
+     * 获得程序版本号
+     */
+    public static String getVersionCodeStr(Context context) {
+        if (context == null) {
+            return "0";
+        }
+        try {
+            return String.valueOf(context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode);
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return "0";
+    }
+
     /**
      * 获取应用程序版本名称信息
      */
     public static String getVersionName(Context context) {
         if (context == null) {
-            return "1";
+            return "0";
         }
         try {
             return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
         } catch (PackageManager.NameNotFoundException e) {
             e.printStackTrace();
         }
-        return "1";
+        return "0";
     }
 
     public static String getProcessName(Context context) {

+ 89 - 0
library_support/src/main/java/cn/yyxx/support/ReflectUtils.java

@@ -0,0 +1,89 @@
+package cn.yyxx.support;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author #Suyghur.
+ * Created on 2020/7/8
+ */
+public class ReflectUtils {
+
+
+    public static Object callInstanceMethod(Object instance, String methodName, Class<?>[] types, Object[] valuse) {
+        return callMethod(instance, methodName, types, valuse);
+    }
+
+    /**
+     * 调用该对象所有可调用的公有方法,包括父类方法
+     *
+     * @param obj        调用者对象
+     * @param methodName 调用的方法名,与obj合在一起即为 obj.methodName
+     * @param types      调用方法的参数类型
+     * @param values     调用方法的参数值
+     * @return methodName所返回的对象
+     */
+    public static Object callMethod(Object obj, String methodName, Class<?>[] types, Object[] values) {
+        // 注:数组类型为:基本类型+[].class,如String[]写成 new Class<?>[]{String[].class}
+        if (obj == null) {
+            return null;
+        }
+        Class<?> classz = obj.getClass();
+        Method method = null;
+        Object retValue = null;
+        try {
+            method = classz.getMethod(methodName, types);
+            retValue = method.invoke(obj, values);
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return retValue;
+    }
+
+    /**
+     * 调用该类的静态方法,包括静态方法
+     *
+     * @param clz        类类对象
+     * @param methodName 调用的方法名,与obj合在一起即为 obj.methodName
+     * @param types      调用方法的参数类型,数组类型为:基本类型+[].class,如String[]写成
+     *                   new Class<?>[]{String[].class},int 类型是为int.class
+     * @param values     调用方法的参数值
+     * @return methodName所返回的对象
+     */
+    public static Object callStaticMethod(Class<?> clz, String methodName, Class<?>[] types, Object[] values) {
+        Method method = null;
+        Object retValue = null;
+        try {
+            method = clz.getDeclaredMethod(methodName, types);
+            method.setAccessible(true);// 设置安全检查,设为true使得可以访问私有方法
+            retValue = method.invoke(null, values);
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return retValue;
+    }
+
+    /**
+     * 调用该类的静态方法,包括静态方法
+     *
+     * @param className  调用的方法名,与obj合在一起即为 obj.methodname
+     * @param methodName 调用的方法名,与obj合在一起即为 obj.methodname
+     * @param types      调用方法的参数类型,数组类型为:基本类型+[].class,如String[]写成
+     *                   new Class<?>[]{String[].class},int 类型是为int.class
+     * @param values     调用方法的参数值
+     * @return
+     */
+    public static Object callStaticMethod(String className, String methodName, Class<?>[] types, Object[] values) {
+        Class<?> clz;
+        try {
+            clz = Class.forName(className);
+            return callStaticMethod(clz, methodName, types, values);
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

+ 8 - 0
library_support/src/main/java/cn/yyxx/support/ResUtils.java

@@ -15,4 +15,12 @@ public class ResUtils {
     public static int getResId(Context context, String name, String type) {
         return context.getResources().getIdentifier(name, type, context.getPackageName());
     }
+
+    public static String getResString(Context context, String name) {
+        if (getResId(context, name, "string") == 0) {
+            return "";
+        } else {
+            return context.getResources().getString(getResId(context, name, "string"));
+        }
+    }
 }

+ 165 - 0
library_support/src/main/java/cn/yyxx/support/StrUtils.java

@@ -0,0 +1,165 @@
+package cn.yyxx.support;
+
+import android.text.TextUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+
+/**
+ * @author #Suyghur.
+ * Created on 2020/7/29
+ */
+public class StrUtils {
+
+    private StrUtils() {
+        /* cannot be instantiated */
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    /**
+     * 获取字符串3的数位
+     *
+     * @param s
+     * @return
+     */
+    public static String getEvenStr(String s) {
+        try {
+            StringBuilder newStr = new StringBuilder();
+            // get length of string
+            int temp = s.length();
+            for (int i = 0; i <= (temp - 1) && newStr.length() < 16; i += 3) {
+                // rebuild string
+                newStr.append(s.charAt(i));
+            }
+
+            return newStr.toString();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+//    public static String decodeResult(String result) {
+//        // TODO Auto-generated method stub
+//        if (TextUtils.isEmpty(result)) {
+//            return "";
+//        }
+//        try {
+//            JSONObject object = new JSONObject(result);
+//            String p = object.getString("d");
+//            String ts = object.getString("ts");
+//            String tsMD5 = Md5Utils.encodeByMD5(ts);
+//            String key = StrUtils.getEvenStr(tsMD5 + tsMD5);
+//            return AesUtils.decrypt(p, key);
+//
+//        } catch (JSONException e) {
+//            // TODO Auto-generated catch block
+//            e.printStackTrace();
+//        }
+//
+//        return "";
+//    }
+
+    public static String changeInputStream(InputStream _inputStream, String _encode) {
+        ByteArrayOutputStream ops = new ByteArrayOutputStream();
+        byte[] data = new byte[1024];
+        int len = 0;
+        String result = "";
+        try {
+
+            if (_inputStream != null) {
+                while ((len = _inputStream.read(data)) != -1) {
+                    data.toString();
+                    ops.write(data, 0, len);
+                }
+                result = new String(ops.toByteArray(), _encode);
+                ops.flush();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    /**
+     * 生成随机数字和字母,
+     */
+    public static String getRandomString(int length) {
+        StringBuilder val = new StringBuilder();
+        Random random = new Random();
+        //参数length,表示生成几位随机数
+        for (int i = 0; i < length; i++) {
+            String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
+            //输出字母还是数字
+            if ("char".equalsIgnoreCase(charOrNum)) {
+                //输出是大写字母还是小写字母
+                int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
+                val.append((char) (random.nextInt(26) + temp));
+            } else {
+                val.append(random.nextInt(10));
+            }
+        }
+        return val.toString();
+    }
+
+    /**
+     * String位数不够后面补0
+     *
+     * @param str
+     * @param strLength
+     * @return
+     */
+    public static String getStringAppendLength(String str, int strLength) {
+        int strLen = str.length();
+        if (strLen < strLength) {
+            StringBuilder strBuilder = new StringBuilder(str);
+            while (strLen < strLength) {
+                //sb.append("0").append(str);//左补0
+                strBuilder.append("0")//右补0
+                ;
+                strLen = strBuilder.length();
+            }
+            str = strBuilder.toString();
+        }
+
+        return str;
+    }
+
+
+    /**
+     * 获取url的文件名(包后缀)
+     *
+     * @param url
+     * @return
+     */
+    public static String getUrlFileName(String url) {
+        if (url == null) {
+            return null;
+        }
+        if (url.lastIndexOf("/") != -1) {
+            return url.substring(url.lastIndexOf("/") + 1, url.length());
+        } else if (url.lastIndexOf("\\") != -1) {
+            return url.substring(url.lastIndexOf("\\") + 1, url.length());
+        } else {
+            return null;
+        }
+    }
+
+    public static String reverseString(String s) {
+        if (TextUtils.isEmpty(s)) {
+            return s;
+        } else {
+            char[] chars = new char[s.length()];
+            chars = s.toCharArray();
+            StringBuilder sb = new StringBuilder();
+            for (int i = chars.length - 1; i >= 0; i--) {
+                sb.append(chars[i]);
+            }
+            return new String(sb);
+        }
+    }
+
+}

+ 10 - 4
library_support/src/main/java/cn/yyxx/support/device/DeviceInfoUtils.java

@@ -421,9 +421,6 @@ public class DeviceInfoUtils {
     /**
      * 获取运营商
      * 1、移动;2、联通;3、电信;4、其他
-     *
-     * @param context
-     * @return
      */
     public static String getSimOperator(Context context) {
         String code = getSimOperatorCode(context);
@@ -519,6 +516,15 @@ public class DeviceInfoUtils {
         return "none";
     }
 
+    public static String getNetworkType(Context context) {
+        String networkClz = getNetworkClass(context);
+        if ("none".equals(networkClz) || "wifi".equals(networkClz)) {
+            return "0";
+        } else {
+            return "1";
+        }
+    }
+
     public static boolean isCharged(Context context) {
         IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
         Intent batteryStatusIntent = context.registerReceiver(null, ifilter);
@@ -575,7 +581,7 @@ public class DeviceInfoUtils {
     public static boolean isPcKernel() {
         String str = "";
         try {
-            Process start = new ProcessBuilder(new String[]{"/system/bin/cat", "/proc/cpuinfo"}).start();
+            Process start = new ProcessBuilder("/system/bin/cat", "/proc/cpuinfo").start();
             StringBuilder stringBuffer = new StringBuilder();
             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(start.getInputStream(), StandardCharsets.UTF_8));
             while (true) {

+ 29 - 0
library_support/src/main/java/cn/yyxx/support/encryption/HexUtils.java

@@ -0,0 +1,29 @@
+package cn.yyxx.support.encryption;
+
+/**
+ * @author #Suyghur.
+ * Created on 2021/06/19
+ */
+public class HexUtils {
+
+    /**
+     * @param src 16进制字符串
+     * @return 字节数组
+     */
+    public static byte[] hexString2Bytes(String src) {
+        int l = src.length() / 2;
+        byte[] ret = new byte[l];
+        for (int i = 0; i < l; i++) {
+            ret[i] = Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();
+        }
+        return ret;
+    }
+
+    public static String bytes2HexString(byte[] b) {
+        StringBuilder result = new StringBuilder();
+        for (byte value : b) {
+            result.append(String.format("%02X", value));
+        }
+        return result.toString();
+    }
+}

+ 140 - 0
library_support/src/main/java/cn/yyxx/support/gaid/GAIDUtils.java

@@ -0,0 +1,140 @@
+package cn.yyxx.support.gaid;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.text.TextUtils;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import cn.yyxx.support.ReflectUtils;
+import cn.yyxx.support.hawkeye.LogUtils;
+import cn.yyxx.support.scheduler.SingleThreadFutureScheduler;
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public class GAIDUtils {
+
+    private GAIDUtils() {
+        /* cannot be instantiated */
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    private static final int ONE_SECOND = 1000;
+
+    private static String adid = "";
+
+    private static volatile SingleThreadFutureScheduler playAdIdScheduler = null;
+
+    public static Object getAdvertisingInfoObject(final Context context, long timeoutMilli) {
+        return runSyncInPlayAdIdSchedulerWithTimeout(context, new Callable<Object>() {
+            @Override
+            public Object call() {
+                String clzName = "com.google.android.gms.ads.identifier.AdvertisingIdClient";
+                String methodName = "getAdvertisingIdInfo";
+                return ReflectUtils.callStaticMethod(clzName, methodName, new Class[]{Context.class}, new Object[]{context});
+            }
+        }, timeoutMilli);
+    }
+
+    public static String getPlayAdId(final Context context, final Object advertisingInfoObject, long timeoutMilli) {
+        return runSyncInPlayAdIdSchedulerWithTimeout(context, new Callable<String>() {
+            @Override
+            public String call() {
+                return getPlayAdId(advertisingInfoObject);
+            }
+        }, timeoutMilli);
+    }
+
+    public static String getPlayAdId(Object AdvertisingInfoObject) {
+        try {
+            return (String) ReflectUtils.callMethod(AdvertisingInfoObject, "getId", null, null);
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static <R> R runSyncInPlayAdIdSchedulerWithTimeout(final Context context, Callable<R> callable, long timeoutMilli) {
+        if (playAdIdScheduler == null) {
+            synchronized (GAIDUtils.class) {
+                if (playAdIdScheduler == null) {
+                    playAdIdScheduler = new SingleThreadFutureScheduler("PlayAdIdLibrary", true);
+                }
+            }
+        }
+        ScheduledFuture<R> playAdIdFuture = playAdIdScheduler.scheduleFutureWithReturn(callable, 0);
+
+        try {
+            return playAdIdFuture.get(timeoutMilli, TimeUnit.MILLISECONDS);
+        } catch (ExecutionException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } catch (TimeoutException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    public static String getGoogleAdid() {
+        return adid;
+    }
+
+    public static void initGoogleAdid(Context context, final OnDeviceIdsRead onDeviceIdRead) {
+
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            LogUtils.e("GoogleAdId should read in the background");
+            String googleAdId = initGoogleAdid(context);
+            LogUtils.e("GoogleAdId read " + googleAdId);
+            adid = googleAdId;
+            onDeviceIdRead.onGoogleAdIdRead(-1, googleAdId);
+            return;
+        }
+
+        LogUtils.d("GoogleAdId is reading in the foreground");
+        new AsyncTask<Context, Void, String>() {
+            @Override
+            protected String doInBackground(Context... params) {
+                Context innerContext = params[0];
+                String innerResult = initGoogleAdid(innerContext);
+                LogUtils.d("GoogleAdId read " + innerResult);
+                return innerResult;
+            }
+
+            @Override
+            protected void onPostExecute(String playAdiId) {
+                if (TextUtils.isEmpty(playAdiId)) {
+                    onDeviceIdRead.onGoogleAdIdRead(-1, "Failed to connect to Google Service Framework, or Google Service Framework is unavailable");
+                } else {
+                    adid = playAdiId;
+                    onDeviceIdRead.onGoogleAdIdRead(0, playAdiId);
+                }
+            }
+        }.execute(context);
+    }
+
+    private static String initGoogleAdid(Context context) {
+        String googleAdId = null;
+        try {
+            GooglePlayServicesClient.GooglePlayServicesInfo gpsInfo = GooglePlayServicesClient.getGooglePlayServicesInfo(context, ONE_SECOND * 11);
+            googleAdId = gpsInfo.getGpsAdid();
+            if (googleAdId == null) {
+                Object advertisingInfoObject = getAdvertisingInfoObject(context, ONE_SECOND * 11);
+
+                if (advertisingInfoObject != null) {
+                    googleAdId = getPlayAdId(context, advertisingInfoObject, ONE_SECOND);
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return googleAdId;
+    }
+}

+ 143 - 0
library_support/src/main/java/cn/yyxx/support/gaid/GooglePlayServicesClient.java

@@ -0,0 +1,143 @@
+package cn.yyxx.support.gaid;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.RemoteException;
+
+import java.io.IOException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public class GooglePlayServicesClient {
+    public static final class GooglePlayServicesInfo {
+        private final String gpsAdid;
+        private final Boolean trackingEnabled;
+
+        GooglePlayServicesInfo(String gpdAdid, Boolean trackingEnabled) {
+            this.gpsAdid = gpdAdid;
+            this.trackingEnabled = trackingEnabled;
+        }
+
+        public String getGpsAdid() {
+            return this.gpsAdid;
+        }
+
+        public Boolean isTrackingEnabled() {
+            return this.trackingEnabled;
+        }
+    }
+
+    public static GooglePlayServicesInfo getGooglePlayServicesInfo(Context context, long timeoutMilliSec) throws Exception {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            throw new IllegalStateException("Google Play Services info can't be accessed from the main thread");
+        }
+
+        try {
+            PackageManager pm = context.getPackageManager();
+            pm.getPackageInfo("com.android.vending", 0);
+        } catch (Exception e) {
+            throw e;
+        }
+
+        GooglePlayServicesConnection connection = new GooglePlayServicesConnection(timeoutMilliSec);
+        Intent intent = new Intent("com.google.android.gms.ads.identifier.service.START");
+        intent.setPackage("com.google.android.gms");
+        if (context.bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
+            try {
+                GooglePlayServicesInterface gpsInterface = new GooglePlayServicesInterface(connection.getBinder());
+                return new GooglePlayServicesInfo(gpsInterface.getGpsAdid(), gpsInterface.getTrackingEnabled(true));
+            } catch (Exception exception) {
+                throw exception;
+            } finally {
+                    context.unbindService(connection);
+            }
+        }
+        throw new IOException("Google Play connection failed");
+    }
+
+    private static final class GooglePlayServicesConnection implements ServiceConnection {
+        long timeoutMilliSec;
+        boolean retrieved = false;
+        private final LinkedBlockingQueue<IBinder> queue = new LinkedBlockingQueue<>(1);
+
+        public GooglePlayServicesConnection(long timeoutMilliSec) {
+            this.timeoutMilliSec = timeoutMilliSec;
+        }
+
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            try {
+                this.queue.put(service);
+            } catch (InterruptedException localInterruptedException) {
+                localInterruptedException.printStackTrace();
+            }
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+        }
+
+        public IBinder getBinder() throws InterruptedException {
+            if (this.retrieved) {
+                throw new IllegalStateException();
+            }
+            this.retrieved = true;
+            return (IBinder) this.queue.poll(this.timeoutMilliSec, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    private static final class GooglePlayServicesInterface implements IInterface {
+        private IBinder binder;
+
+        public GooglePlayServicesInterface(IBinder pBinder) {
+            binder = pBinder;
+        }
+
+        public IBinder asBinder() {
+            return binder;
+        }
+
+        public String getGpsAdid() throws RemoteException {
+            Parcel data = Parcel.obtain();
+            Parcel reply = Parcel.obtain();
+            String id;
+            try {
+                data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService");
+                binder.transact(1, data, reply, 0);
+                reply.readException();
+                id = reply.readString();
+            } finally {
+                reply.recycle();
+                data.recycle();
+            }
+            return id;
+        }
+
+        public Boolean getTrackingEnabled(boolean paramBoolean) throws RemoteException {
+            Parcel data = Parcel.obtain();
+            Parcel reply = Parcel.obtain();
+            Boolean limitAdTracking;
+            try {
+                data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService");
+                data.writeInt(paramBoolean ? 1 : 0);
+                binder.transact(2, data, reply, 0);
+                reply.readException();
+                limitAdTracking = 0 != reply.readInt();
+            } finally {
+                reply.recycle();
+                data.recycle();
+            }
+            return limitAdTracking != null ? !limitAdTracking : null;
+        }
+    }
+}
+

+ 9 - 0
library_support/src/main/java/cn/yyxx/support/gaid/OnDeviceIdsRead.java

@@ -0,0 +1,9 @@
+package cn.yyxx.support.gaid;
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public interface OnDeviceIdsRead {
+    void onGoogleAdIdRead(int code, String result);
+}

+ 488 - 0
library_support/src/main/java/cn/yyxx/support/multidex/MultiDex.java

@@ -0,0 +1,488 @@
+package cn.yyxx.support.multidex;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Build.VERSION;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipFile;
+
+import dalvik.system.DexFile;
+
+/**
+ * @author #Suyghur.
+ * Created on 2020/7/7
+ */
+public final class MultiDex {
+    static final String TAG = "MultiDex";
+    private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes";
+    private static final String CODE_CACHE_NAME = "code_cache";
+    private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "secondary-dexes";
+    private static final int MAX_SUPPORTED_SDK_VERSION = 20;
+    private static final int MIN_SDK_VERSION = 4;
+    private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
+    private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
+    private static final String NO_KEY_PREFIX = "";
+    private static final Set<File> INSTALLED_APK = new HashSet();
+    private static final boolean IS_VM_MULTIDEX_CAPABLE = isVMMultidexCapable(System.getProperty("java.vm.version"));
+
+    private MultiDex() {
+    }
+
+    public static void install(Context context) {
+        Log.i("MultiDex", "Installing application");
+        if (IS_VM_MULTIDEX_CAPABLE) {
+            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
+        } else if (VERSION.SDK_INT < 4) {
+            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
+        } else {
+            try {
+                ApplicationInfo applicationInfo = getApplicationInfo(context);
+                if (applicationInfo == null) {
+                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
+                    return;
+                }
+
+                doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
+            } catch (Exception var2) {
+                Log.e("MultiDex", "MultiDex installation failure", var2);
+                throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
+            }
+
+            Log.i("MultiDex", "install done");
+        }
+    }
+
+    public static void installInstrumentation(Context instrumentationContext, Context targetContext) {
+        Log.i("MultiDex", "Installing instrumentation");
+        if (IS_VM_MULTIDEX_CAPABLE) {
+            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
+        } else if (VERSION.SDK_INT < 4) {
+            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
+        } else {
+            try {
+                ApplicationInfo instrumentationInfo = getApplicationInfo(instrumentationContext);
+                if (instrumentationInfo == null) {
+                    Log.i("MultiDex", "No ApplicationInfo available for instrumentation, i.e. running on a test Context: MultiDex support library is disabled.");
+                    return;
+                }
+
+                ApplicationInfo applicationInfo = getApplicationInfo(targetContext);
+                if (applicationInfo == null) {
+                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
+                    return;
+                }
+
+                String instrumentationPrefix = instrumentationContext.getPackageName() + ".";
+                File dataDir = new File(applicationInfo.dataDir);
+                doInstallation(targetContext, new File(instrumentationInfo.sourceDir), dataDir, instrumentationPrefix + "secondary-dexes", instrumentationPrefix, false);
+                doInstallation(targetContext, new File(applicationInfo.sourceDir), dataDir, "secondary-dexes", "", false);
+            } catch (Exception var6) {
+                Log.e("MultiDex", "MultiDex installation failure", var6);
+                throw new RuntimeException("MultiDex installation failed (" + var6.getMessage() + ").");
+            }
+
+            Log.i("MultiDex", "Installation done");
+        }
+    }
+
+    private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
+        synchronized (INSTALLED_APK) {
+            if (!INSTALLED_APK.contains(sourceApk)) {
+                INSTALLED_APK.add(sourceApk);
+                if (VERSION.SDK_INT > 20) {
+                    Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
+                }
+
+                ClassLoader loader;
+                try {
+                    loader = mainContext.getClassLoader();
+                } catch (RuntimeException var25) {
+                    Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
+                    return;
+                }
+
+                if (loader == null) {
+                    Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
+                } else {
+                    try {
+                        clearOldDexDir(mainContext);
+                    } catch (Throwable var24) {
+                        Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
+                    }
+
+                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
+                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
+                    IOException closeException = null;
+
+                    try {
+                        List files = extractor.load(mainContext, prefsKeyPrefix, false);
+
+                        try {
+                            installSecondaryDexes(loader, dexDir, files);
+                        } catch (IOException var26) {
+                            if (!reinstallOnPatchRecoverableException) {
+                                throw var26;
+                            }
+
+                            Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
+                            files = extractor.load(mainContext, prefsKeyPrefix, true);
+                            installSecondaryDexes(loader, dexDir, files);
+                        }
+                    } finally {
+                        try {
+                            extractor.close();
+                        } catch (IOException var23) {
+                            closeException = var23;
+                        }
+
+                    }
+
+                    if (closeException != null) {
+                        throw closeException;
+                    }
+                }
+            }
+        }
+    }
+
+    private static ApplicationInfo getApplicationInfo(Context context) {
+        try {
+            return context.getApplicationInfo();
+        } catch (RuntimeException var2) {
+            Log.w("MultiDex", "Failure while trying to obtain ApplicationInfo from Context. Must be running in test mode. Skip patching.", var2);
+            return null;
+        }
+    }
+
+    static boolean isVMMultidexCapable(String versionString) {
+        boolean isMultidexCapable = false;
+        if (versionString != null) {
+            Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
+            if (matcher.matches()) {
+                try {
+                    int major = Integer.parseInt(matcher.group(1));
+                    int minor = Integer.parseInt(matcher.group(2));
+                    isMultidexCapable = major > 2 || major == 2 && minor >= 1;
+                } catch (NumberFormatException var5) {
+                    var5.printStackTrace();
+                }
+            }
+        }
+
+        Log.i("MultiDex", "VM with version " + versionString + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
+        return isMultidexCapable;
+    }
+
+    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
+        if (!files.isEmpty()) {
+            if (VERSION.SDK_INT >= 19) {
+                V19.install(loader, files, dexDir);
+            } else if (VERSION.SDK_INT >= 14) {
+                V14.install(loader, files);
+            } else {
+                V4.install(loader, files);
+            }
+        }
+
+    }
+
+    private static Field findField(Object instance, String name) throws NoSuchFieldException {
+        Class clazz = instance.getClass();
+
+        while (clazz != null) {
+            try {
+                Field field = clazz.getDeclaredField(name);
+                if (!field.isAccessible()) {
+                    field.setAccessible(true);
+                }
+
+                return field;
+            } catch (NoSuchFieldException var4) {
+                clazz = clazz.getSuperclass();
+            }
+        }
+
+        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
+    }
+
+    private static Method findMethod(Object instance, String name, Class<?>... parameterTypes) throws NoSuchMethodException {
+        Class clazz = instance.getClass();
+
+        while (clazz != null) {
+            try {
+                Method method = clazz.getDeclaredMethod(name, parameterTypes);
+                if (!method.isAccessible()) {
+                    method.setAccessible(true);
+                }
+
+                return method;
+            } catch (NoSuchMethodException var5) {
+                clazz = clazz.getSuperclass();
+            }
+        }
+
+        throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
+    }
+
+    private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
+        Field jlrField = findField(instance, fieldName);
+        Object[] original = (Object[]) ((Object[]) jlrField.get(instance));
+        Object[] combined = (Object[]) ((Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
+        System.arraycopy(original, 0, combined, 0, original.length);
+        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
+        jlrField.set(instance, combined);
+    }
+
+    private static void clearOldDexDir(Context context) throws Exception {
+        File dexDir = new File(context.getFilesDir(), "secondary-dexes");
+        if (dexDir.isDirectory()) {
+            Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
+            File[] files = dexDir.listFiles();
+            if (files == null) {
+                Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
+                return;
+            }
+
+            File[] var3 = files;
+            int var4 = files.length;
+
+            for (int var5 = 0; var5 < var4; ++var5) {
+                File oldFile = var3[var5];
+                Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
+                if (!oldFile.delete()) {
+                    Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
+                } else {
+                    Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
+                }
+            }
+
+            if (!dexDir.delete()) {
+                Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
+            } else {
+                Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
+            }
+        }
+
+    }
+
+    private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
+        File cache = new File(dataDir, "code_cache");
+
+        try {
+            mkdirChecked(cache);
+        } catch (IOException var5) {
+            cache = new File(context.getFilesDir(), "code_cache");
+            mkdirChecked(cache);
+        }
+
+        File dexDir = new File(cache, secondaryFolderName);
+        mkdirChecked(dexDir);
+        return dexDir;
+    }
+
+    private static void mkdirChecked(File dir) throws IOException {
+        dir.mkdir();
+        if (!dir.isDirectory()) {
+            File parent = dir.getParentFile();
+            if (parent == null) {
+                Log.e("MultiDex", "Failed to create dir " + dir.getPath() + ". Parent file is null.");
+            } else {
+                Log.e("MultiDex", "Failed to create dir " + dir.getPath() + ". parent file is a dir " + parent.isDirectory() + ", a file " + parent.isFile() + ", exists " + parent.exists() + ", readable " + parent.canRead() + ", writable " + parent.canWrite());
+            }
+
+            throw new IOException("Failed to create directory " + dir.getPath());
+        }
+    }
+
+    private static final class V4 {
+        private V4() {
+        }
+
+        static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, IOException {
+            int extraSize = additionalClassPathEntries.size();
+            Field pathField = MultiDex.findField(loader, "path");
+            StringBuilder path = new StringBuilder((String) pathField.get(loader));
+            String[] extraPaths = new String[extraSize];
+            File[] extraFiles = new File[extraSize];
+            ZipFile[] extraZips = new ZipFile[extraSize];
+            DexFile[] extraDexs = new DexFile[extraSize];
+
+            String entryPath;
+            int index;
+            for (ListIterator iterator = additionalClassPathEntries.listIterator(); iterator.hasNext(); extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0)) {
+                File additionalEntry = (File) iterator.next();
+                entryPath = additionalEntry.getAbsolutePath();
+                path.append(':').append(entryPath);
+                index = iterator.previousIndex();
+                extraPaths[index] = entryPath;
+                extraFiles[index] = additionalEntry;
+                extraZips[index] = new ZipFile(additionalEntry);
+            }
+
+            pathField.set(loader, path.toString());
+            MultiDex.expandFieldArray(loader, "mPaths", extraPaths);
+            MultiDex.expandFieldArray(loader, "mFiles", extraFiles);
+            MultiDex.expandFieldArray(loader, "mZips", extraZips);
+            MultiDex.expandFieldArray(loader, "mDexs", extraDexs);
+        }
+    }
+
+    private static final class V14 {
+        private static final int EXTRACTED_SUFFIX_LENGTH = ".zip".length();
+        private final ElementConstructor elementConstructor;
+
+        static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries) throws IOException, SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
+            Field pathListField = MultiDex.findField(loader, "pathList");
+            Object dexPathList = pathListField.get(loader);
+            Object[] elements = (new V14()).makeDexElements(additionalClassPathEntries);
+
+            try {
+                MultiDex.expandFieldArray(dexPathList, "dexElements", elements);
+            } catch (NoSuchFieldException var6) {
+                Log.w("MultiDex", "Failed find field 'dexElements' attempting 'pathElements'", var6);
+                MultiDex.expandFieldArray(dexPathList, "pathElements", elements);
+            }
+
+        }
+
+        private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
+            Class elementClass = Class.forName("dalvik.system.DexPathList$Element");
+
+            Object constructor;
+            try {
+                constructor = new ICSElementConstructor(elementClass);
+            } catch (NoSuchMethodException var6) {
+                try {
+                    constructor = new JBMR11ElementConstructor(elementClass);
+                } catch (NoSuchMethodException var5) {
+                    constructor = new JBMR2ElementConstructor(elementClass);
+                }
+            }
+
+            this.elementConstructor = (ElementConstructor) constructor;
+        }
+
+        private Object[] makeDexElements(List<? extends File> files) throws IOException, SecurityException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+            Object[] elements = new Object[files.size()];
+
+            for (int i = 0; i < elements.length; ++i) {
+                File file = (File) files.get(i);
+                elements[i] = this.elementConstructor.newInstance(file, DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
+            }
+
+            return elements;
+        }
+
+        private static String optimizedPathFor(File path) {
+            File optimizedDirectory = path.getParentFile();
+            String fileName = path.getName();
+            String optimizedFileName = fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH) + ".dex";
+            File result = new File(optimizedDirectory, optimizedFileName);
+            return result.getPath();
+        }
+
+        private static class JBMR2ElementConstructor implements ElementConstructor {
+            private final Constructor<?> elementConstructor;
+
+            JBMR2ElementConstructor(Class<?> elementClass) throws SecurityException, NoSuchMethodException {
+                this.elementConstructor = elementClass.getConstructor(File.class, Boolean.TYPE, File.class, DexFile.class);
+                this.elementConstructor.setAccessible(true);
+            }
+
+            @Override
+            public Object newInstance(File file, DexFile dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+                return this.elementConstructor.newInstance(file, Boolean.FALSE, file, dex);
+            }
+        }
+
+        private static class JBMR11ElementConstructor implements ElementConstructor {
+            private final Constructor<?> elementConstructor;
+
+            JBMR11ElementConstructor(Class<?> elementClass) throws SecurityException, NoSuchMethodException {
+                this.elementConstructor = elementClass.getConstructor(File.class, File.class, DexFile.class);
+                this.elementConstructor.setAccessible(true);
+            }
+
+            @Override
+            public Object newInstance(File file, DexFile dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+                return this.elementConstructor.newInstance(file, file, dex);
+            }
+        }
+
+        private static class ICSElementConstructor implements ElementConstructor {
+            private final Constructor<?> elementConstructor;
+
+            ICSElementConstructor(Class<?> elementClass) throws SecurityException, NoSuchMethodException {
+                this.elementConstructor = elementClass.getConstructor(File.class, ZipFile.class, DexFile.class);
+                this.elementConstructor.setAccessible(true);
+            }
+
+            @Override
+            public Object newInstance(File file, DexFile dex) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException {
+                return this.elementConstructor.newInstance(file, new ZipFile(file), dex);
+            }
+        }
+
+        private interface ElementConstructor {
+            Object newInstance(File var1, DexFile var2) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, IOException;
+        }
+    }
+
+    private static final class V19 {
+        private V19() {
+        }
+
+        static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
+            Field pathListField = MultiDex.findField(loader, "pathList");
+            Object dexPathList = pathListField.get(loader);
+            ArrayList<IOException> suppressedExceptions = new ArrayList();
+            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
+            if (suppressedExceptions.size() > 0) {
+                Iterator var6 = suppressedExceptions.iterator();
+
+                while (var6.hasNext()) {
+                    IOException e = (IOException) var6.next();
+                    Log.w("MultiDex", "Exception in makeDexElement", e);
+                }
+
+                Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
+                IOException[] dexElementsSuppressedExceptions = (IOException[]) ((IOException[]) suppressedExceptionsField.get(dexPathList));
+                if (dexElementsSuppressedExceptions == null) {
+                    dexElementsSuppressedExceptions = (IOException[]) suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
+                } else {
+                    IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
+                    suppressedExceptions.toArray(combined);
+                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
+                    dexElementsSuppressedExceptions = combined;
+                }
+
+                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
+                IOException exception = new IOException("I/O exception during makeDexElement");
+                exception.initCause((Throwable) suppressedExceptions.get(0));
+                throw exception;
+            }
+        }
+
+        private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
+            return (Object[]) ((Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
+        }
+    }
+}

+ 323 - 0
library_support/src/main/java/cn/yyxx/support/multidex/MultiDexExtractor.java

@@ -0,0 +1,323 @@
+package cn.yyxx.support.multidex;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.util.Log;
+
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Created by #Suyghur, on 2019/04/08.
+ * Description : Migrate from android.support.multidex
+ */
+final class MultiDexExtractor implements Closeable {
+    private static final String TAG = "MultiDex";
+    private static final String DEX_PREFIX = "classes";
+    static final String DEX_SUFFIX = ".dex";
+    private static final String EXTRACTED_NAME_EXT = ".classes";
+    static final String EXTRACTED_SUFFIX = ".zip";
+    private static final int MAX_EXTRACT_ATTEMPTS = 3;
+    private static final String PREFS_FILE = "multidex.version";
+    private static final String KEY_TIME_STAMP = "timestamp";
+    private static final String KEY_CRC = "crc";
+    private static final String KEY_DEX_NUMBER = "dex.number";
+    private static final String KEY_DEX_CRC = "dex.crc.";
+    private static final String KEY_DEX_TIME = "dex.time.";
+    private static final int BUFFER_SIZE = 16384;
+    private static final long NO_VALUE = -1L;
+    private static final String LOCK_FILENAME = "MultiDex.lock";
+    private final File sourceApk;
+    private final long sourceCrc;
+    private final File dexDir;
+    private final RandomAccessFile lockRaf;
+    private final FileChannel lockChannel;
+    private final FileLock cacheLock;
+
+    MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
+        Log.i("MultiDex", "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
+        this.sourceApk = sourceApk;
+        this.dexDir = dexDir;
+        this.sourceCrc = getZipCrc(sourceApk);
+        File lockFile = new File(dexDir, "MultiDex.lock");
+        this.lockRaf = new RandomAccessFile(lockFile, "rw");
+
+        try {
+            this.lockChannel = this.lockRaf.getChannel();
+
+            try {
+                Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
+                this.cacheLock = this.lockChannel.lock();
+            } catch (RuntimeException | Error | IOException var5) {
+                closeQuietly(this.lockChannel);
+                throw var5;
+            }
+
+            Log.i("MultiDex", lockFile.getPath() + " locked");
+        } catch (RuntimeException | Error | IOException var6) {
+            closeQuietly(this.lockRaf);
+            throw var6;
+        }
+    }
+
+    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
+        Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
+        if (!this.cacheLock.isValid()) {
+            throw new IllegalStateException("MultiDexExtractor was closed");
+        } else {
+            List files;
+            if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
+                try {
+                    files = this.loadExistingExtractions(context, prefsKeyPrefix);
+                } catch (IOException var6) {
+                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
+                    files = this.performExtractions();
+                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
+                }
+            } else {
+                if (forceReload) {
+                    Log.i("MultiDex", "Forced extraction must be performed.");
+                } else {
+                    Log.i("MultiDex", "Detected that extraction must be performed.");
+                }
+
+                files = this.performExtractions();
+                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
+            }
+
+            Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
+            return files;
+        }
+    }
+
+    public void close() throws IOException {
+        this.cacheLock.release();
+        this.lockChannel.close();
+        this.lockRaf.close();
+    }
+
+    private List<ExtractedDex> loadExistingExtractions(Context context, String prefsKeyPrefix) throws IOException {
+        Log.i("MultiDex", "loading existing secondary dex files");
+        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
+        SharedPreferences multiDexPreferences = getMultiDexPreferences(context);
+        int totalDexNumber = multiDexPreferences.getInt(prefsKeyPrefix + "dex.number", 1);
+        List<ExtractedDex> files = new ArrayList(totalDexNumber - 1);
+
+        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
+            String fileName = extractedFilePrefix + secondaryNumber + ".zip";
+            ExtractedDex extractedFile = new ExtractedDex(this.dexDir, fileName);
+            if (!extractedFile.isFile()) {
+                throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
+            }
+
+            extractedFile.crc = getZipCrc(extractedFile);
+            long expectedCrc = multiDexPreferences.getLong(prefsKeyPrefix + "dex.crc." + secondaryNumber, -1L);
+            long expectedModTime = multiDexPreferences.getLong(prefsKeyPrefix + "dex.time." + secondaryNumber, -1L);
+            long lastModified = extractedFile.lastModified();
+            if (expectedModTime != lastModified || expectedCrc != extractedFile.crc) {
+                throw new IOException("Invalid extracted dex: " + extractedFile + " (key \"" + prefsKeyPrefix + "\"), expected modification time: " + expectedModTime + ", modification time: " + lastModified + ", expected crc: " + expectedCrc + ", file crc: " + extractedFile.crc);
+            }
+
+            files.add(extractedFile);
+        }
+
+        return files;
+    }
+
+    private static boolean isModified(Context context, File archive, long currentCrc, String prefsKeyPrefix) {
+        SharedPreferences prefs = getMultiDexPreferences(context);
+        return prefs.getLong(prefsKeyPrefix + "timestamp", -1L) != getTimeStamp(archive) || prefs.getLong(prefsKeyPrefix + "crc", -1L) != currentCrc;
+    }
+
+    private static long getTimeStamp(File archive) {
+        long timeStamp = archive.lastModified();
+        if (timeStamp == -1L) {
+            --timeStamp;
+        }
+
+        return timeStamp;
+    }
+
+    private static long getZipCrc(File archive) throws IOException {
+        long computedValue = ZipUtil.getZipCrc(archive);
+        if (computedValue == -1L) {
+            --computedValue;
+        }
+
+        return computedValue;
+    }
+
+    private List<ExtractedDex> performExtractions() throws IOException {
+        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
+        this.clearDexDir();
+        List<ExtractedDex> files = new ArrayList();
+        ZipFile apk = new ZipFile(this.sourceApk);
+
+        try {
+            int secondaryNumber = 2;
+
+            for (ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
+                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
+                ExtractedDex extractedFile = new ExtractedDex(this.dexDir, fileName);
+                files.add(extractedFile);
+                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
+                int numAttempts = 0;
+                boolean isExtractionSuccessful = false;
+
+                while (numAttempts < 3 && !isExtractionSuccessful) {
+                    ++numAttempts;
+                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
+
+                    try {
+                        extractedFile.crc = getZipCrc(extractedFile);
+                        isExtractionSuccessful = true;
+                    } catch (IOException var18) {
+                        isExtractionSuccessful = false;
+                        Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
+                    }
+
+                    Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
+                    if (!isExtractionSuccessful) {
+                        extractedFile.delete();
+                        if (extractedFile.exists()) {
+                            Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
+                        }
+                    }
+                }
+
+                if (!isExtractionSuccessful) {
+                    throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
+                }
+
+                ++secondaryNumber;
+            }
+        } finally {
+            try {
+                apk.close();
+            } catch (IOException var17) {
+                Log.w("MultiDex", "Failed to close resource", var17);
+            }
+
+        }
+
+        return files;
+    }
+
+    private static void putStoredApkInfo(Context context, String keyPrefix, long timeStamp, long crc, List<ExtractedDex> extractedDexes) {
+        SharedPreferences prefs = getMultiDexPreferences(context);
+        SharedPreferences.Editor edit = prefs.edit();
+        edit.putLong(keyPrefix + "timestamp", timeStamp);
+        edit.putLong(keyPrefix + "crc", crc);
+        edit.putInt(keyPrefix + "dex.number", extractedDexes.size() + 1);
+        int extractedDexId = 2;
+
+        for (Iterator var10 = extractedDexes.iterator(); var10.hasNext(); ++extractedDexId) {
+            ExtractedDex dex = (ExtractedDex) var10.next();
+            edit.putLong(keyPrefix + "dex.crc." + extractedDexId, dex.crc);
+            edit.putLong(keyPrefix + "dex.time." + extractedDexId, dex.lastModified());
+        }
+
+        edit.commit();
+    }
+
+    private static SharedPreferences getMultiDexPreferences(Context context) {
+        return context.getSharedPreferences("multidex.version", Build.VERSION.SDK_INT < 11 ? 0 : 4);
+    }
+
+    private void clearDexDir() {
+        File[] files = this.dexDir.listFiles(new FileFilter() {
+            public boolean accept(File pathname) {
+                return !pathname.getName().equals("MultiDex.lock");
+            }
+        });
+        if (files == null) {
+            Log.w("MultiDex", "Failed to list secondary dex dir content (" + this.dexDir.getPath() + ").");
+        } else {
+            File[] var2 = files;
+            int var3 = files.length;
+
+            for (int var4 = 0; var4 < var3; ++var4) {
+                File oldFile = var2[var4];
+                Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
+                if (!oldFile.delete()) {
+                    Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
+                } else {
+                    Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
+                }
+            }
+
+        }
+    }
+
+    private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
+        InputStream in = apk.getInputStream(dexFile);
+        ZipOutputStream out = null;
+        File tmp = File.createTempFile("tmp-" + extractedFilePrefix, ".zip", extractTo.getParentFile());
+        Log.i("MultiDex", "Extracting " + tmp.getPath());
+
+        try {
+            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
+
+            try {
+                ZipEntry classesDex = new ZipEntry("classes.dex");
+                classesDex.setTime(dexFile.getTime());
+                out.putNextEntry(classesDex);
+                byte[] buffer = new byte[16384];
+
+                for (int length = in.read(buffer); length != -1; length = in.read(buffer)) {
+                    out.write(buffer, 0, length);
+                }
+
+                out.closeEntry();
+            } finally {
+                out.close();
+            }
+
+            if (!tmp.setReadOnly()) {
+                throw new IOException("Failed to mark readonly \"" + tmp.getAbsolutePath() + "\" (tmp of \"" + extractTo.getAbsolutePath() + "\")");
+            }
+
+            Log.i("MultiDex", "Renaming to " + extractTo.getPath());
+            if (!tmp.renameTo(extractTo)) {
+                throw new IOException("Failed to rename \"" + tmp.getAbsolutePath() + "\" to \"" + extractTo.getAbsolutePath() + "\"");
+            }
+        } finally {
+            closeQuietly(in);
+            tmp.delete();
+        }
+
+    }
+
+    private static void closeQuietly(Closeable closeable) {
+        try {
+            closeable.close();
+        } catch (IOException var2) {
+            Log.w("MultiDex", "Failed to close resource", var2);
+        }
+
+    }
+
+    private static class ExtractedDex extends File {
+        public long crc = -1L;
+
+        public ExtractedDex(File dexDir, String fileName) {
+            super(dexDir, fileName);
+        }
+    }
+}

+ 95 - 0
library_support/src/main/java/cn/yyxx/support/multidex/ZipUtil.java

@@ -0,0 +1,95 @@
+package cn.yyxx.support.multidex;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.zip.CRC32;
+import java.util.zip.ZipException;
+
+/**
+ * @author #Suyghur.
+ * Created on 2020/7/7
+ */
+final class ZipUtil {
+    private static final int ENDHDR = 22;
+    private static final int ENDSIG = 101010256;
+    private static final int BUFFER_SIZE = 16384;
+
+    ZipUtil() {
+    }
+
+    static long getZipCrc(File apk) throws IOException {
+        RandomAccessFile raf = new RandomAccessFile(apk, "r");
+
+        long var3;
+        try {
+            CentralDirectory dir = findCentralDirectory(raf);
+            var3 = computeCrcOfCentralDir(raf, dir);
+        } finally {
+            raf.close();
+        }
+
+        return var3;
+    }
+
+    static CentralDirectory findCentralDirectory(RandomAccessFile raf) throws IOException, ZipException {
+        long scanOffset = raf.length() - 22L;
+        if (scanOffset < 0L) {
+            throw new ZipException("File too short to be a zip file: " + raf.length());
+        } else {
+            long stopOffset = scanOffset - 65536L;
+            if (stopOffset < 0L) {
+                stopOffset = 0L;
+            }
+
+            int endSig = Integer.reverseBytes(101010256);
+
+            do {
+                raf.seek(scanOffset);
+                if (raf.readInt() == endSig) {
+                    raf.skipBytes(2);
+                    raf.skipBytes(2);
+                    raf.skipBytes(2);
+                    raf.skipBytes(2);
+                    CentralDirectory dir = new CentralDirectory();
+                    dir.size = (long) Integer.reverseBytes(raf.readInt()) & 4294967295L;
+                    dir.offset = (long) Integer.reverseBytes(raf.readInt()) & 4294967295L;
+                    return dir;
+                }
+
+                --scanOffset;
+            } while (scanOffset >= stopOffset);
+
+            throw new ZipException("End Of Central Directory signature not found");
+        }
+    }
+
+    static long computeCrcOfCentralDir(RandomAccessFile raf, CentralDirectory dir) throws IOException {
+        CRC32 crc = new CRC32();
+        long stillToRead = dir.size;
+        raf.seek(dir.offset);
+        int length = (int) Math.min(16384L, stillToRead);
+        byte[] buffer = new byte[16384];
+
+        for (length = raf.read(buffer, 0, length); length != -1; length = raf.read(buffer, 0, length)) {
+            crc.update(buffer, 0, length);
+            stillToRead -= (long) length;
+            if (stillToRead == 0L) {
+                break;
+            }
+
+            length = (int) Math.min(16384L, stillToRead);
+        }
+
+        return crc.getValue();
+    }
+
+    static class CentralDirectory {
+        long offset;
+        long size;
+
+        CentralDirectory() {
+        }
+    }
+}
+

+ 19 - 0
library_support/src/main/java/cn/yyxx/support/scheduler/FutureScheduler.java

@@ -0,0 +1,19 @@
+package cn.yyxx.support.scheduler;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public interface FutureScheduler {
+
+    ScheduledFuture<?> scheduleFuture(Runnable command, long millisecondDelay);
+
+    ScheduledFuture<?> scheduleFutureWithFixedDelay(Runnable command, long initialMillisecondDelay, long millisecondDelay);
+
+    <V> ScheduledFuture<V> scheduleFutureWithReturn(Callable<V> callable, long millisecondDelay);
+
+    void teardown();
+}

+ 25 - 0
library_support/src/main/java/cn/yyxx/support/scheduler/RunnableWrapper.java

@@ -0,0 +1,25 @@
+package cn.yyxx.support.scheduler;
+
+import cn.yyxx.support.hawkeye.LogUtils;
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public class RunnableWrapper implements Runnable {
+
+    private Runnable runnable;
+
+    RunnableWrapper(Runnable runnable) {
+        this.runnable = runnable;
+    }
+
+    @Override
+    public void run() {
+        try {
+            runnable.run();
+        } catch (Throwable t) {
+            LogUtils.e("Runnable error " + t.getMessage() + " of type " + t.getClass().getCanonicalName());
+        }
+    }
+}

+ 67 - 0
library_support/src/main/java/cn/yyxx/support/scheduler/SingleThreadFutureScheduler.java

@@ -0,0 +1,67 @@
+package cn.yyxx.support.scheduler;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import cn.yyxx.support.hawkeye.LogUtils;
+
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public class SingleThreadFutureScheduler implements FutureScheduler {
+
+    private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
+
+
+    public SingleThreadFutureScheduler(final String source, boolean doKeepAlive) {
+        this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryWrapper(source), new RejectedExecutionHandler() {
+            // Logs rejected runnables rejected from the entering the pool
+            @Override
+            public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
+                LogUtils.d("Runnable " + runnable.toString() + " rejected from " + source);
+            }
+        }
+        );
+
+        if (!doKeepAlive) {
+            scheduledThreadPoolExecutor.setKeepAliveTime(10L, TimeUnit.MILLISECONDS);
+            scheduledThreadPoolExecutor.allowCoreThreadTimeOut(true);
+        }
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleFuture(Runnable command, long millisecondDelay) {
+        return scheduledThreadPoolExecutor.schedule(new RunnableWrapper(command), millisecondDelay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleFutureWithFixedDelay(Runnable command, long initialMillisecondDelay, long millisecondDelay) {
+        return scheduledThreadPoolExecutor.scheduleWithFixedDelay(new RunnableWrapper(command), initialMillisecondDelay, millisecondDelay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> scheduleFutureWithReturn(final Callable<V> callable, long millisecondDelay) {
+        return scheduledThreadPoolExecutor.schedule(new Callable<V>() {
+            @Override
+            public V call() {
+                try {
+                    return callable.call();
+                } catch (Throwable t) {
+                    LogUtils.e("Callable error " + t.getMessage() + " of type " + t.getClass().getCanonicalName());
+                    return null;
+                }
+            }
+        }, millisecondDelay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void teardown() {
+        scheduledThreadPoolExecutor.shutdown();
+    }
+}

+ 39 - 0
library_support/src/main/java/cn/yyxx/support/scheduler/ThreadFactoryWrapper.java

@@ -0,0 +1,39 @@
+package cn.yyxx.support.scheduler;
+
+import android.os.Process;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import cn.yyxx.support.hawkeye.LogUtils;
+
+/**
+ * @author #Suyghur.
+ * Created on 10/26/20
+ */
+public class ThreadFactoryWrapper implements ThreadFactory {
+    private String source;
+    private static final String THREAD_PREFIX = "YYXXSupport";
+
+    public ThreadFactoryWrapper(String source) {
+        this.source = source;
+    }
+
+    @Override
+    public Thread newThread(Runnable runnable) {
+        Thread thread = Executors.defaultThreadFactory().newThread(runnable);
+
+        thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
+        thread.setName("THREAD_PREFIX" + thread.getName() + "-" + source);
+        thread.setDaemon(true);
+
+        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+            @Override
+            public void uncaughtException(Thread th, Throwable tr) {
+                LogUtils.e("Thread " + th.getName() + " with error " + tr.getMessage());
+            }
+        });
+
+        return thread;
+    }
+}

+ 11 - 10
library_support/src/main/java/cn/yyxx/support/ui/DragViewLayout.java

@@ -1,7 +1,6 @@
 package cn.yyxx.support.ui;
 
 import android.animation.ValueAnimator;
-import android.app.Activity;
 import android.content.Context;
 import android.graphics.PixelFormat;
 import android.view.Gravity;
@@ -10,14 +9,14 @@ import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
-
-import cn.yyxx.support.hawkeye.LogUtils;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
 
 /**
  * @author #Suyghur.
  * Created on 2021/05/12
  */
-public class DragViewLayout extends FrameLayout {
+public class DragViewLayout extends LinearLayout {
 
     //view所在位置
     private int mLastX, mLastY;
@@ -26,10 +25,10 @@ public class DragViewLayout extends FrameLayout {
     private int mScreenWidth, mScreenHeight;
 
     //view宽高
-    private int mWidth, mHeight;
+    protected int mWidth, mHeight;
 
     //是否在拖拽过程中
-    private boolean isDrag = false;
+    protected boolean isDrag = false;
 
     //系统最新滑动距离
     private int mTouchSlop = 0;
@@ -37,6 +36,7 @@ public class DragViewLayout extends FrameLayout {
     private WindowManager.LayoutParams floatLayoutParams;
     private WindowManager mWindowManager;
 
+
     //手指触摸位置
     private float xInScreen;
     private float yInScreen;
@@ -117,7 +117,7 @@ public class DragViewLayout extends FrameLayout {
         }
     }
 
-    private void updateFloatPosition(boolean isUp) {
+    protected void updateFloatPosition(boolean isUp) {
         int x = (int) (xInScreen - xInView);
         int y = (int) (yInScreen - yInView);
         if (isUp) {
@@ -132,11 +132,11 @@ public class DragViewLayout extends FrameLayout {
         mWindowManager.updateViewLayout(this, floatLayoutParams);
     }
 
-    private boolean isRightFloat() {
-        return xInScreen > mScreenWidth / 2;
+    protected boolean isRightFloat() {
+        return xInScreen > mScreenWidth / 2.0f;
     }
 
-    private void startAnim() {
+    protected void startAnim() {
         ValueAnimator valueAnimator;
         if (floatLayoutParams.x < mScreenWidth / 2) {
             valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, 0);
@@ -165,4 +165,5 @@ public class DragViewLayout extends FrameLayout {
     public void release() {
         mWindowManager.removeView(this);
     }
+
 }

+ 394 - 0
library_support/src/main/java/cn/yyxx/support/ui/floating/DotImageView.java

@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *  Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package cn.yyxx.support.ui.floating;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Camera;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+import cn.yyxx.support.DensityUtils;
+
+/**
+ * 00%=FF(不透明)    5%=F2    10%=E5    15%=D8    20%=CC    25%=BF    30%=B2    35%=A5    40%=99    45%=8c    50%=7F
+ * 55%=72    60%=66    65%=59    70%=4c    75%=3F    80%=33    85%=21    90%=19    95%=0c    100%=00(全透明)
+ */
+public class DotImageView extends View {
+    private static final String TAG = DotImageView.class.getSimpleName();
+    public static final int NORMAL = 0;//不隐藏
+    public static final int HIDE_LEFT = 1;//左边隐藏
+    public static final int HIDE_RIGHT = 2;//右边隐藏
+    private Paint mPaint;//用于画anything
+
+    private Paint mPaintBg;//用于画anything
+    private String dotNum = null;//红点数字
+    private float mAlphaValue;//透明度动画值
+    private float mRelateValue = 1f;//旋转动画值
+    private boolean inited = false;//标记透明动画是否执行过,防止因onreseme 切换导致重复执行
+
+
+    private Bitmap mBitmap;//logo
+    private int mLogoBackgroundRadius = 0;//logo的灰色背景圆的半径
+    private int mLogoWhiteRadius = 0;//logo的白色背景的圆的半径
+    private int mRedPointRadiusWithNum = 0;//红点圆半径
+    private int mRedPointRadius = 0;//红点圆半径
+    private int mRedPointOffset = 0;//红点对logo的偏移量,比如左红点就是logo中心的 x - mRedPointOffset
+
+    private boolean isDragging = false;//是否 绘制旋转放大动画,只有 非停靠边缘才绘制
+    private float scaleOffset;//放大偏移值
+    private ValueAnimator mDraggingValueAnimator;//放大、旋转 属性动画
+    private LinearInterpolator mLinearInterpolator = new LinearInterpolator();//通用用加速器
+    public boolean mDrawDarkBg = true;//是否绘制黑色背景,当菜单关闭时,才绘制灰色背景
+    private static final float hideOffset = 0.5f;//往左右隐藏多少宽度的偏移值, 隐藏宽度的0.4
+    private static final float hideOffsetAlpha = 0.5f;//往左右隐藏多少宽度的偏移值, 隐藏宽度的0.4
+    private Camera mCamera;//camera用于执行3D动画
+
+    private boolean mDrawNum = false;//只绘制红点还是红点+白色数字
+
+    private int mStatus = NORMAL;//0 正常,1 左,2右,3 中间方法旋转
+    private int mLastStatus = mStatus;
+    private Matrix mMatrix;
+    private boolean mIsResetPosition;
+
+    private int mBgColor = 0x99000000;
+
+
+    public void setBgColor(int bgColor) {
+        mBgColor = bgColor;
+    }
+
+
+    public void setDrawNum(boolean drawNum) {
+        this.mDrawNum = drawNum;
+    }
+
+    public void setDrawDarkBg(boolean drawDarkBg) {
+        mDrawDarkBg = drawDarkBg;
+        invalidate();
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+
+    public void setStatus(int status) {
+        this.mStatus = status;
+        isDragging = false;
+        if (this.mStatus != NORMAL) {
+            setDrawNum(mDrawNum);
+            this.mDrawDarkBg = true;
+        }
+        invalidate();
+
+    }
+
+    public void setBitmap(Bitmap bitmap) {
+        mBitmap = bitmap;
+    }
+
+    public DotImageView(Context context, Bitmap bitmap) {
+        super(context);
+        this.mBitmap = bitmap;
+        init(context);
+    }
+
+
+//    public DotImageView(Context context) {
+//        super(context);
+//        init();
+//    }
+//
+//    public DotImageView(Context context, AttributeSet attrs) {
+//        super(context, attrs);
+//        init();
+//    }
+//
+//    public DotImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+//        super(context, attrs, defStyleAttr);
+//        init();
+//    }
+
+    private void init(Context context) {
+
+
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setTextSize(sp2px(10));
+        mPaint.setStyle(Paint.Style.FILL);
+
+        mPaintBg = new Paint();
+        mPaintBg.setAntiAlias(true);
+        mPaintBg.setStyle(Paint.Style.FILL);
+        mPaintBg.setColor(mBgColor);//60% 黑色背景 (透明度 40%)
+
+        mCamera = new Camera();
+        mMatrix = new Matrix();
+
+        mLogoBackgroundRadius = DensityUtils.dip2px(context, 25);//logo的灰色背景圆的半径
+        mLogoWhiteRadius = DensityUtils.dip2px(context, 20);//logo的白色背景的圆的半径
+        mRedPointRadiusWithNum = DensityUtils.dip2px(context, 6);//红点圆半径
+        mRedPointRadius = DensityUtils.dip2px(context, 3);//红点圆半径
+        mRedPointOffset = DensityUtils.dip2px(context, 10);//红点对logo的偏移量,比如左红点就是logo中心的 x - mRedPointOffset
+    }
+
+    /**
+     * 这个方法是否有优化空间
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int wh = mLogoBackgroundRadius * 2;
+        setMeasuredDimension(wh, wh);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        float centerX = getWidth() / 2f;
+        float centerY = getHeight() / 2f;
+        canvas.save();//保存一份快照,方便后面恢复
+        mCamera.save();
+        setAlpha(1.0f);
+        if (mStatus == NORMAL) {
+            if (mLastStatus != NORMAL) {
+                canvas.restore();//恢复画布的原始快照
+                mCamera.restore();
+            }
+
+            if (isDragging) {
+
+                //如果当前是拖动状态则放大并旋转
+                // canvas.scale((scaleOffset + 1f), (scaleOffset + 1f), getWidth() / 2, getHeight() / 2);
+                if (mIsResetPosition) {
+                    //手指拖动后离开屏幕复位时使用 x轴旋转 3d动画
+                    mCamera.save();
+                    mCamera.rotateX(720 * scaleOffset);//0-720度 最多转两圈
+                    mCamera.getMatrix(mMatrix);
+
+                    mMatrix.preTranslate(-getWidth() / 2f, -getHeight() / 2f);
+                    mMatrix.postTranslate(getWidth() / 2f, getHeight() / 2f);
+                    canvas.concat(mMatrix);
+                    mCamera.restore();
+                } else {
+                    //手指拖动且手指未离开屏幕则使用 绕图心2d旋转动画
+                    canvas.rotate(60 * mRelateValue, getWidth() / 2f, getHeight() / 2f);
+                }
+            }
+
+
+        } else if (mStatus == HIDE_LEFT) {
+            canvas.translate(-getWidth() * hideOffset, 0);
+            canvas.rotate(0, getWidth() / 2f, getHeight() / 2f);
+            setAlpha(hideOffsetAlpha);
+
+        } else if (mStatus == HIDE_RIGHT) {
+            canvas.translate(getWidth() * hideOffset, 0);
+            canvas.rotate(0, getWidth() / 2f, getHeight() / 2f);
+            setAlpha(hideOffsetAlpha);
+        }
+        canvas.save();
+        if (!isDragging) {
+            if (mDrawDarkBg) {
+                mPaintBg.setColor(Color.TRANSPARENT);
+                canvas.drawCircle(centerX, centerY, mLogoBackgroundRadius, mPaintBg);
+                // 60% 白色 (透明度 40%)
+                mPaint.setColor(0x99ffffff);
+            } else {
+                //100% 白色背景 (透明度 0%)
+                mPaint.setColor(0xFFFFFFFF);
+            }
+            if (mAlphaValue != 0) {
+                mPaint.setAlpha((int) (mAlphaValue * 255));
+            }
+            canvas.drawCircle(centerX, centerY, mLogoWhiteRadius, mPaint);
+        }
+
+        canvas.restore();
+        //100% 白色背景 (透明度 0%)
+        mPaint.setColor(0xFFFFFFFF);
+        int left = (int) (centerX - mBitmap.getWidth() / 2);
+        int top = (int) (centerY - mBitmap.getHeight() / 2);
+        canvas.drawBitmap(mBitmap, left, top, mPaint);
+
+
+        if (!TextUtils.isEmpty(dotNum)) {
+            int readPointRadus = (mDrawNum ? mRedPointRadiusWithNum : mRedPointRadius);
+            mPaint.setColor(Color.RED);
+            if (mStatus == HIDE_LEFT) {
+                canvas.drawCircle(centerX + mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint);
+                if (mDrawNum) {
+                    mPaint.setColor(Color.WHITE);
+                    canvas.drawText(dotNum, centerX + mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint);
+                }
+            } else if (mStatus == HIDE_RIGHT) {
+                canvas.drawCircle(centerX - mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint);
+                if (mDrawNum) {
+                    mPaint.setColor(Color.WHITE);
+                    canvas.drawText(dotNum, centerX - mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint);
+                }
+            } else {
+                if (mLastStatus == HIDE_LEFT) {
+                    canvas.drawCircle(centerX + mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint);
+                    if (mDrawNum) {
+                        mPaint.setColor(Color.WHITE);
+                        canvas.drawText(dotNum, centerX + mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint);
+                    }
+                } else if (mLastStatus == HIDE_RIGHT) {
+                    canvas.drawCircle(centerX - mRedPointOffset, centerY - mRedPointOffset, readPointRadus, mPaint);
+                    if (mDrawNum) {
+                        mPaint.setColor(Color.WHITE);
+                        canvas.drawText(dotNum, centerX - mRedPointOffset - getTextWidth(dotNum, mPaint) / 2, centerY - mRedPointOffset + getTextHeight(dotNum, mPaint) / 2, mPaint);
+                    }
+                }
+            }
+        }
+        mLastStatus = mStatus;
+    }
+
+
+    public void setDotNum(int num, Animator.AnimatorListener l) {
+        if (!inited) {
+            startAnim(num, l);
+        } else {
+            refreshDot(num);
+        }
+    }
+
+    public void refreshDot(int num) {
+        if (num > 0) {
+            String dotNumTmp = String.valueOf(num);
+            if (!TextUtils.equals(dotNum, dotNumTmp)) {
+                dotNum = dotNumTmp;
+                invalidate();
+            }
+        } else {
+            dotNum = null;
+        }
+    }
+
+    public void startAnim(final int num, Animator.AnimatorListener listener) {
+        ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0.6f, 1f, 0.6f);
+        valueAnimator.setInterpolator(mLinearInterpolator);
+        valueAnimator.setDuration(3000);
+        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mAlphaValue = (float) animation.getAnimatedValue();
+                invalidate();
+
+            }
+        });
+        valueAnimator.addListener(listener);
+        valueAnimator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                inited = true;
+                refreshDot(num);
+                mAlphaValue = 0;
+
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mAlphaValue = 0;
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+
+            }
+        });
+        valueAnimator.start();
+    }
+
+    public void setDragging(boolean dragging, float offset, boolean isResetPosition) {
+        isDragging = dragging;
+        this.mIsResetPosition = isResetPosition;
+        if (offset > 0 && offset != this.scaleOffset) {
+            this.scaleOffset = offset;
+        }
+        if (isDragging && mStatus == NORMAL) {
+            if (mDraggingValueAnimator != null) {
+                if (mDraggingValueAnimator.isRunning()) return;
+            }
+            mDraggingValueAnimator = ValueAnimator.ofFloat(0, 6f, 12f, 0f);
+            mDraggingValueAnimator.setInterpolator(mLinearInterpolator);
+            mDraggingValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    mRelateValue = (float) animation.getAnimatedValue();
+                    invalidate();
+                }
+            });
+            mDraggingValueAnimator.addListener(new Animator.AnimatorListener() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    isDragging = false;
+                    mIsResetPosition = false;
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator animation) {
+
+                }
+            });
+            mDraggingValueAnimator.setDuration(1000);
+            mDraggingValueAnimator.start();
+        }
+    }
+
+    private int dip2px(float dipValue) {
+        final float scale = getContext().getResources().getDisplayMetrics().density;
+        return (int) (dipValue * scale + 0.5f);
+    }
+
+    private int sp2px(float spValue) {
+        final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity;
+        return (int) (spValue * fontScale + 0.5f);
+    }
+
+    private float getTextHeight(String text, Paint paint) {
+        Rect rect = new Rect();
+        paint.getTextBounds(text, 0, text.length(), rect);
+        return rect.height() / 1.1f;
+    }
+
+    private float getTextWidth(String text, Paint paint) {
+        return paint.measureText(text);
+    }
+}

+ 73 - 0
library_support/src/main/java/cn/yyxx/support/ui/floating/FloatItem.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *  Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package cn.yyxx.support.ui.floating;
+
+import android.graphics.Bitmap;
+
+/**
+ * Created by wengyiming on 2017/7/21.
+ */
+
+public class FloatItem {
+    public String title;
+    public String titleColor;
+    public String bgColor;
+    public Bitmap icon;
+    public String dotNum = null;
+
+    public FloatItem(String title, String titleColor, String bgColor, Bitmap icon, String dotNum) {
+        this.title = title;
+        this.titleColor = titleColor;
+        this.bgColor = bgColor;
+        this.icon = icon;
+        this.dotNum = dotNum;
+    }
+
+    public FloatItem(String title, String titleColor, String bgColor, Bitmap bitmap) {
+        this.title = title;
+        this.titleColor = titleColor;
+        this.bgColor = bgColor;
+        this.icon = bitmap;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) return true;
+
+        if (obj instanceof FloatItem) {
+            FloatItem floatItem = (FloatItem) obj;
+            return floatItem.title.equals(this.title);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return title.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "FloatItem{" +
+                "title='" + title + '\'' +
+                ", titleColor=" + titleColor +
+                ", bgColor=" + bgColor +
+                ", icon=" + icon +
+                ", dotNum='" + dotNum + '\'' +
+                '}';
+    }
+}

+ 1036 - 0
library_support/src/main/java/cn/yyxx/support/ui/floating/FloatLogoMenu.java

@@ -0,0 +1,1036 @@
+/*
+ * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *  Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package cn.yyxx.support.ui.floating;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import cn.yyxx.support.DensityUtils;
+
+/**
+ * Created by wengyiming on 2017/7/20.
+ */
+public class FloatLogoMenu {
+    /**
+     * 记录 logo 停放的位置,以备下次恢复
+     */
+    private static final String LOCATION_X = "hintLocation";
+    private static final String LOCATION_Y = "locationY";
+
+    /**
+     * 悬浮球 坐落 左 右 标记
+     */
+    public static final int LEFT = 0;
+    public static final int RIGHT = 1;
+
+    /**
+     * 记录系统状态栏的高度
+     */
+    private int mStatusBarHeight;
+    /**
+     * 记录当前手指位置在屏幕上的横坐标值
+     */
+    private float mXInScreen;
+
+    /**
+     * 记录当前手指位置在屏幕上的纵坐标值
+     */
+    private float mYInScreen;
+
+    /**
+     * 记录手指按下时在屏幕上的横坐标的值
+     */
+    private float mXDownInScreen;
+
+    /**
+     * 记录手指按下时在屏幕上的纵坐标的值
+     */
+    private float mYDownInScreen;
+
+    /**
+     * 记录手指按下时在小悬浮窗的View上的横坐标的值
+     */
+    private float mXInView;
+
+    /**
+     * 记录手指按下时在小悬浮窗的View上的纵坐标的值
+     */
+    private float mYinview;
+
+    /**
+     * 记录屏幕的宽度
+     */
+    private int mScreenWidth;
+
+    /**
+     * 来自 activity 的 wManager
+     */
+    private WindowManager wManager;
+
+
+    /**
+     * 为 wManager 设置 LayoutParams
+     */
+    private WindowManager.LayoutParams wmParams;
+
+    /**
+     * 带透明度动画、旋转、放大的悬浮球
+     */
+    private DotImageView mFloatLogo;
+
+
+    /**
+     * 用于 定时 隐藏 logo的定时器
+     */
+    private CountDownTimer mHideTimer;
+
+
+    /**
+     * float menu的高度
+     */
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+
+
+    /**
+     * 悬浮窗左右移动到默认位置 动画的 加速器
+     */
+    private Interpolator mLinearInterpolator = new LinearInterpolator();
+
+    /**
+     * 用于记录上次菜单打开的时间,判断时间间隔
+     */
+    private static double DOUBLE_CLICK_TIME = 0L;
+
+    /**
+     * 标记是否拖动中
+     */
+    private boolean isDraging = false;
+
+    /**
+     * 用于恢复悬浮球的location的属性动画值
+     */
+    private int mResetLocationValue;
+
+    /**
+     * 手指离开屏幕后 用于恢复 悬浮球的 logo 的左右位置
+     */
+    private Runnable updatePositionRunnable = new Runnable() {
+        @Override
+        public void run() {
+            isDraging = true;
+            checkPosition();
+
+        }
+    };
+
+    /**
+     * 这个事件不做任何事情、直接return false则 onclick 事件生效
+     */
+    private OnTouchListener mDefaultOnTouchListerner = new OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            isDraging = false;
+            return false;
+        }
+    };
+
+    /**
+     * 这个事件用于处理移动、自定义点击或者其它事情,return true可以保证onclick事件失效
+     */
+    private OnTouchListener touchListener = new OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    floatEventDown(event);
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    floatEventMove(event);
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    floatEventUp();
+                    break;
+            }
+            return true;
+        }
+    };
+
+
+    /**
+     * 菜单背景颜色
+     */
+    private String mBackMenuColor;
+
+    /**
+     * 是否绘制红点数字
+     */
+    private boolean mDrawRedPointNum;
+
+
+    /**
+     * 是否绘制圆形菜单项,false绘制方形
+     */
+    private boolean mCicleMenuBg;
+
+
+    /**
+     * R.drawable.game_logo
+     *
+     * @param floatItems
+     */
+    private Bitmap mLogoRes;
+
+    /**
+     * 用于显示在 mActivity 上的 mActivity
+     */
+    private Context mContext;
+
+    /**
+     * 菜单 点击、关闭 监听
+     */
+    private FloatMenuView.OnMenuClickListener mOnMenuClickListener;
+
+
+    /**
+     * 停靠默认位置
+     */
+    private int mDefaultLocation = RIGHT;
+
+
+    /**
+     * 悬浮窗 坐落 位置
+     */
+    private int mHintLocation = mDefaultLocation;
+
+
+    /**
+     * 用于记录菜单项内容
+     */
+    private List<FloatItem> mFloatItems;
+
+    private LinearLayout rootViewRight;
+
+    private LinearLayout rootView;
+
+    private ValueAnimator valueAnimator;
+
+    private boolean isExpaned = false;
+
+    private Drawable mBackground;
+
+
+    private FloatLogoMenu(Builder builder) {
+        mBackMenuColor = builder.mBackMenuColor;
+        mDrawRedPointNum = builder.mDrawRedPointNum;
+        mCicleMenuBg = builder.mCicleMenuBg;
+        mLogoRes = builder.mLogoRes;
+        mContext = builder.mContext;
+        mOnMenuClickListener = builder.mOnMenuClickListener;
+        mDefaultLocation = builder.mDefaultLocation;
+        mFloatItems = builder.mFloatItems;
+        mBackground = builder.mDrawable;
+
+
+//        if (mActivity == null || mActivity.isFinishing() || mActivity.getWindowManager() == null) {
+//            throw new IllegalArgumentException("Activity = null, or Activity is isFinishing ,or this Activity`s  token is bad");
+//        }
+
+        if (mLogoRes == null) {
+            throw new IllegalArgumentException("No logo found,you can setLogo/showWithLogo to set a FloatLogo ");
+        }
+
+        if (mFloatItems.isEmpty()) {
+            throw new IllegalArgumentException("At least one menu item!");
+        }
+
+        initFloatWindow();
+        initTimer();
+        initFloat();
+
+    }
+
+    public void setFloatItemList(List<FloatItem> floatItems) {
+        this.mFloatItems = floatItems;
+        caculateDotNum();
+    }
+
+    /**
+     * 初始化悬浮球 window
+     */
+    private void initFloatWindow() {
+//        wmParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT,
+//                WindowManager.LayoutParams.WRAP_CONTENT,
+//                WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
+//                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+//                        | WindowManager.LayoutParams.FLAG_FULLSCREEN,
+//                PixelFormat.TRANSLUCENT);
+
+        wmParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
+                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+                PixelFormat.RGBA_8888);
+
+        if (mContext instanceof Activity) {
+            Activity activity = (Activity) mContext;
+            wManager = activity.getWindowManager();
+            //类似dialog,寄托在activity的windows上,activity关闭时需要关闭当前float
+            wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+        } else {
+            wManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                if (Build.VERSION.SDK_INT > 23) {
+                    //在android7.1以上系统需要使用TYPE_PHONE类型 配合运行时权限
+                    wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
+                } else {
+                    wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
+                }
+            } else {
+                wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
+            }
+        }
+
+        mScreenWidth = wManager.getDefaultDisplay().getWidth();
+        int screenHeigth = wManager.getDefaultDisplay().getHeight();
+
+        //判断状态栏是否显示 如果不显示则statusBarHeight为0
+        mStatusBarHeight = dp2Px(0, mContext);
+
+        wmParams.format = PixelFormat.RGBA_8888;
+        wmParams.gravity = Gravity.LEFT | Gravity.TOP;
+
+
+        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | WindowManager.LayoutParams.FLAG_FULLSCREEN;
+        mHintLocation = getSetting(LOCATION_X, mDefaultLocation);
+        int defaultY = ((screenHeigth - mStatusBarHeight) / 2) / 3;
+        int y = getSetting(LOCATION_Y, defaultY);
+        if (mHintLocation == LEFT) {
+            wmParams.x = 0;
+        } else {
+            wmParams.x = mScreenWidth;
+        }
+
+
+        if (y != 0 && y != defaultY) {
+            wmParams.y = y;
+        } else {
+            wmParams.y = defaultY;
+        }
+
+        wmParams.alpha = 1;
+        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+    }
+
+
+    /**
+     * 初始化悬浮球
+     */
+    private void initFloat() {
+        generateLeftLineLayout();
+        generateRightLineLayout();
+        mFloatLogo = new DotImageView(mContext, mLogoRes);
+        mFloatLogo.setLayoutParams(new LinearLayout.LayoutParams(dp2Px(30, mContext), dp2Px(30, mContext)));
+        mFloatLogo.setDrawNum(mDrawRedPointNum);
+        mFloatLogo.setBgColor(Color.parseColor(mBackMenuColor));
+        mFloatLogo.setDrawDarkBg(true);
+
+        caculateDotNum();
+        floatBtnEvent();
+        try {
+            wManager.addView(mFloatLogo, wmParams);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    private void generateLeftLineLayout() {
+
+        rootView = new LinearLayout(mContext);
+        rootView.setOrientation(LinearLayout.HORIZONTAL);
+        rootView.setGravity(Gravity.CENTER);
+        rootView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, DensityUtils.dip2px(mContext, 30)));
+
+        rootView.setBackgroundDrawable(mBackground);
+
+        DotImageView floatLogo = new DotImageView(mContext, mLogoRes);
+        floatLogo.setLayoutParams(new LinearLayout.LayoutParams(DensityUtils.dip2px(mContext, 30), DensityUtils.dip2px(mContext, 30)));
+        floatLogo.setDrawNum(mDrawRedPointNum);
+        floatLogo.setDrawDarkBg(false);
+        floatLogo.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (isExpaned) {
+                    try {
+                        wManager.removeViewImmediate(rootView);
+                        wManager.addView(FloatLogoMenu.this.mFloatLogo, wmParams);
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                    isExpaned = false;
+                }
+            }
+        });
+
+        final FloatMenuView mFloatMenuView = new FloatMenuView.Builder(mContext)
+                .setFloatItems(mFloatItems)
+                .setBackgroundColor(Color.TRANSPARENT)
+                .setCircleBg(mCicleMenuBg)
+                .setStatus(FloatMenuView.STATUS_LEFT)
+                .setMenuBackgroundColor(Color.TRANSPARENT)
+                .drawNum(mDrawRedPointNum)
+                .create();
+        setMenuClickListener(mFloatMenuView);
+
+        rootView.addView(floatLogo);
+        rootView.addView(mFloatMenuView);
+    }
+
+    public void hideMenu() {
+        if (isExpaned) {
+            try {
+                wManager.removeViewImmediate(mHintLocation == LEFT ? rootView : rootViewRight);
+                wManager.addView(FloatLogoMenu.this.mFloatLogo, wmParams);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            isExpaned = false;
+        }
+    }
+
+    private void generateRightLineLayout() {
+        final DotImageView floatLogo = new DotImageView(mContext, mLogoRes);
+        floatLogo.setLayoutParams(new LinearLayout.LayoutParams(DensityUtils.dip2px(mContext, 30), DensityUtils.dip2px(mContext, 30)));
+        floatLogo.setDrawNum(mDrawRedPointNum);
+        floatLogo.setDrawDarkBg(false);
+
+        floatLogo.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (isExpaned) {
+                    try {
+                        wManager.removeViewImmediate(rootViewRight);
+                        wManager.addView(FloatLogoMenu.this.mFloatLogo, wmParams);
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                    isExpaned = false;
+                }
+            }
+        });
+
+        rootViewRight = new LinearLayout(mContext);
+        rootViewRight.setOrientation(LinearLayout.HORIZONTAL);
+        rootViewRight.setGravity(Gravity.CENTER);
+        rootViewRight.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, DensityUtils.dip2px(mContext, 30)));
+        rootViewRight.setBackgroundDrawable(mBackground);
+
+
+        FloatMenuView mFloatMenuView = new FloatMenuView.Builder(mContext)
+                .setFloatItems(mFloatItems)
+                .setBackgroundColor(Color.TRANSPARENT)
+                .setCircleBg(mCicleMenuBg)
+                .setStatus(FloatMenuView.STATUS_RIGHT)
+                .setMenuBackgroundColor(Color.TRANSPARENT)
+                .drawNum(mDrawRedPointNum)
+                .create();
+        setMenuClickListener(mFloatMenuView);
+
+        rootViewRight.addView(mFloatMenuView);
+        rootViewRight.addView(floatLogo);
+
+
+    }
+
+    /**
+     * 初始化 隐藏悬浮球的定时器
+     */
+    private void initTimer() {
+        if (mHideTimer != null) {
+            mHideTimer.cancel();
+            mHideTimer = null;
+        }
+        mHideTimer = new CountDownTimer(2000, 10) {        //悬浮窗超过5秒没有操作的话会自动隐藏
+            @Override
+            public void onTick(long millisUntilFinished) {
+                if (isExpaned) {
+                    mHideTimer.cancel();
+                }
+            }
+
+            @Override
+            public void onFinish() {
+                if (isExpaned) {
+                    mHideTimer.cancel();
+                    return;
+                }
+                if (!isDraging) {
+                    if (mHintLocation == LEFT) {
+                        mFloatLogo.setStatus(DotImageView.HIDE_LEFT);
+                    } else {
+                        mFloatLogo.setStatus(DotImageView.HIDE_RIGHT);
+                    }
+                    mFloatLogo.setDrawDarkBg(true);
+//                    mFloatLogo.setOnTouchListener(mDefaultOnTouchListerner);//把onClick事件分发下去,防止onclick无效
+                }
+            }
+        };
+    }
+
+
+    /**
+     * 用于 拦截 菜单项的 关闭事件,以方便开始 隐藏定时器
+     *
+     * @param mFloatMenuView
+     */
+    private void setMenuClickListener(final FloatMenuView mFloatMenuView) {
+        mFloatMenuView.setOnMenuClickListener(new FloatMenuView.OnMenuClickListener() {
+            @Override
+            public void onItemClick(int position, String title) {
+
+
+                mFloatLogo.refreshDot(0);
+
+                mOnMenuClickListener.onItemClick(position, title);
+
+
+            }
+
+            @Override
+            public void dismiss() {
+                mFloatLogo.setDrawDarkBg(true);
+                mOnMenuClickListener.dismiss();
+                mHideTimer.start();
+            }
+        });
+
+    }
+
+    private void closeMenu() {
+        wManager.removeViewImmediate(rootView);
+        wManager.addView(FloatLogoMenu.this.mFloatLogo, wmParams);
+    }
+
+    /**
+     * 悬浮窗的点击事件和touch事件的切换
+     */
+    private void floatBtnEvent() {
+        //这里的onCick只有 touchListener = mDefaultOnTouchListerner 才会触发
+//        mFloatLogo.setOnClickListener(new OnClickListener() {
+//            @Override
+//            public void onClick(View v) {
+//                if (!isDraging) {
+//                    if (mFloatLogo.getStatus() != DotImageView.NORMAL) {
+//                        mFloatLogo.setBitmap(mLogoRes);
+//                        mFloatLogo.setStatus(DotImageView.NORMAL);
+//                        if (!mFloatLogo.mDrawDarkBg) {
+//                            mFloatLogo.setDrawDarkBg(true);
+//                        }
+//                    }
+//                    mFloatLogo.setOnTouchListener(touchListener);
+//                    mHideTimer.start();
+//                }
+//            }
+//        });
+
+        mFloatLogo.setOnTouchListener(touchListener);//恢复touch事件
+    }
+
+    /**
+     * 悬浮窗touch事件的 down 事件
+     */
+    private void floatEventDown(MotionEvent event) {
+        isDraging = false;
+        mHideTimer.cancel();
+        if (mFloatLogo.getStatus() != DotImageView.NORMAL) {
+            mFloatLogo.setStatus(DotImageView.NORMAL);
+        }
+        if (!mFloatLogo.mDrawDarkBg) {
+            mFloatLogo.setDrawDarkBg(true);
+        }
+        if (mFloatLogo.getStatus() != DotImageView.NORMAL) {
+            mFloatLogo.setStatus(DotImageView.NORMAL);
+        }
+        mXInView = event.getX();
+        mYinview = event.getY();
+        mXDownInScreen = event.getRawX();
+        mYDownInScreen = event.getRawY();
+        mXInScreen = event.getRawX();
+        mYInScreen = event.getRawY();
+    }
+
+    /**
+     * 悬浮窗touch事件的 move 事件
+     */
+    private void floatEventMove(MotionEvent event) {
+        mXInScreen = event.getRawX();
+        mYInScreen = event.getRawY();
+
+
+        //连续移动的距离超过3则更新一次视图位置
+        if (Math.abs(mXInScreen - mXDownInScreen) > mFloatLogo.getWidth() / 4f || Math.abs(mYInScreen - mYDownInScreen) > mFloatLogo.getWidth() / 4) {
+            wmParams.x = (int) (mXInScreen - mXInView);
+            wmParams.y = (int) (mYInScreen - mYinview) - mFloatLogo.getHeight() / 2;
+            updateViewPosition(); // 手指移动的时候更新小悬浮窗的位置
+            double a = mScreenWidth / 2;
+            float offset = (float) ((a - (Math.abs(wmParams.x - a))) / a);
+            mFloatLogo.setDragging(isDraging, offset, false);
+        } else {
+            isDraging = false;
+            mFloatLogo.setDragging(false, 0, true);
+        }
+    }
+
+    /**
+     * 悬浮窗touch事件的 up 事件
+     */
+    private void floatEventUp() {
+        if (mXInScreen < mScreenWidth / 2f) {   //在左边
+            mHintLocation = LEFT;
+        } else {                   //在右边
+            mHintLocation = RIGHT;
+        }
+        if (valueAnimator == null) {
+            valueAnimator = ValueAnimator.ofInt(64);
+            valueAnimator.setInterpolator(mLinearInterpolator);
+            valueAnimator.setDuration(1000);
+            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    mResetLocationValue = (int) animation.getAnimatedValue();
+                    mHandler.post(updatePositionRunnable);
+                }
+            });
+
+            valueAnimator.addListener(new Animator.AnimatorListener() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (Math.abs(wmParams.x) < 0) {
+                        wmParams.x = 0;
+                    } else if (Math.abs(wmParams.x) > mScreenWidth) {
+                        wmParams.x = mScreenWidth;
+                    }
+                    updateViewPosition();
+                    isDraging = false;
+                    mFloatLogo.setDragging(false, 0, true);
+                    mHideTimer.start();
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    if (Math.abs(wmParams.x) < 0) {
+                        wmParams.x = 0;
+                    } else if (Math.abs(wmParams.x) > mScreenWidth) {
+                        wmParams.x = mScreenWidth;
+                    }
+
+                    updateViewPosition();
+                    isDraging = false;
+                    mFloatLogo.setDragging(false, 0, true);
+                    mHideTimer.start();
+
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator animation) {
+
+                }
+            });
+        }
+        if (!valueAnimator.isRunning()) {
+            valueAnimator.start();
+        }
+
+//        //这里需要判断如果如果手指所在位置和logo所在位置在一个宽度内则不移动,
+        if (Math.abs(mXInScreen - mXDownInScreen) > mFloatLogo.getWidth() / 5f || Math.abs(mYInScreen - mYDownInScreen) > mFloatLogo.getHeight() / 5f) {
+            isDraging = false;
+        } else {
+            openMenu();
+        }
+
+    }
+
+
+    /**
+     * 用于检查并更新悬浮球的位置
+     */
+    private void checkPosition() {
+        if (wmParams.x > 0 && wmParams.x < mScreenWidth) {
+            if (mHintLocation == LEFT) {
+                wmParams.x = wmParams.x - mResetLocationValue;
+            } else {
+                wmParams.x = wmParams.x + mResetLocationValue;
+            }
+            updateViewPosition();
+            double a = mScreenWidth / 2f;
+            float offset = (float) ((a - (Math.abs(wmParams.x - a))) / a);
+            mFloatLogo.setDragging(isDraging, offset, true);
+            return;
+        }
+
+
+        if (Math.abs(wmParams.x) < 0) {
+            wmParams.x = 0;
+        } else if (Math.abs(wmParams.x) > mScreenWidth) {
+            wmParams.x = mScreenWidth;
+        }
+        if (valueAnimator.isRunning()) {
+            valueAnimator.cancel();
+        }
+
+        updateViewPosition();
+        isDraging = false;
+
+    }
+
+
+    /**
+     * 打开菜单
+     */
+    private void openMenu() {
+        if (isDraging) return;
+
+        if (!isExpaned) {
+            mFloatLogo.setDrawDarkBg(false);
+            try {
+                wManager.removeViewImmediate(mFloatLogo);
+                if (mHintLocation == RIGHT) {
+                    wManager.addView(rootViewRight, wmParams);
+                } else {
+                    wManager.addView(rootView, wmParams);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            isExpaned = true;
+            mHideTimer.cancel();
+        } else {
+            mFloatLogo.setDrawDarkBg(true);
+            if (isExpaned) {
+                try {
+                    wManager.removeViewImmediate(mHintLocation == LEFT ? rootView : rootViewRight);
+                    wManager.addView(mFloatLogo, wmParams);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+
+                isExpaned = false;
+            }
+            mHideTimer.start();
+        }
+
+    }
+
+
+    /**
+     * 更新悬浮窗在屏幕中的位置。
+     */
+    private void updateViewPosition() {
+        isDraging = true;
+        try {
+            if (!isExpaned) {
+                if (wmParams.y - mFloatLogo.getHeight() / 2 <= 0) {
+                    wmParams.y = mStatusBarHeight;
+                    isDraging = true;
+                }
+                wManager.updateViewLayout(mFloatLogo, wmParams);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void show() {
+
+        if (wManager != null && wmParams != null && mFloatLogo != null) {
+            try {
+                wManager.removeView(mFloatLogo);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            wManager.addView(mFloatLogo, wmParams);
+
+        }
+
+        if (mHideTimer == null) {
+            initTimer();
+        }
+        mHideTimer.start();
+    }
+
+
+    /**
+     * 关闭菜单
+     */
+    public void hide() {
+        release();
+    }
+
+
+    /**
+     * 移除所有悬浮窗 释放资源
+     */
+    public void release() {
+        //记录上次的位置logo的停放位置,以备下次恢复
+        saveSetting(LOCATION_X, mHintLocation);
+        saveSetting(LOCATION_Y, wmParams.y);
+        mFloatLogo.clearAnimation();
+        if (mHideTimer != null) {
+            mHideTimer.cancel();
+            mHideTimer = null;
+        }
+        try {
+            if (isExpaned) {
+                wManager.removeViewImmediate(mHintLocation == LEFT ? rootView : rootViewRight);
+            } else {
+                wManager.removeViewImmediate(mFloatLogo);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        isExpaned = false;
+        isDraging = false;
+        wManager = null;
+    }
+
+    /**
+     * 计算总红点数
+     */
+    private void caculateDotNum() {
+        int dotNum = 0;
+        for (FloatItem floatItem : mFloatItems) {
+            if (!TextUtils.isEmpty(floatItem.dotNum)) {
+                int num = Integer.parseInt(floatItem.dotNum);
+                dotNum = dotNum + num;
+            }
+        }
+        mFloatLogo.setDrawNum(mDrawRedPointNum);
+        setDotNum(dotNum);
+    }
+
+    /**
+     * 绘制悬浮球的红点
+     */
+    private void setDotNum(int dotNum) {
+        mFloatLogo.setDotNum(dotNum, new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!isDraging) {
+                    mHideTimer.start();
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+
+            }
+        });
+    }
+
+    /**
+     * 用于暴露给外部判断是否包含某个菜单项
+     *
+     * @param menuLabel string
+     * @return boolean
+     */
+    public boolean hasMenu(String menuLabel) {
+        for (FloatItem menuItem : mFloatItems) {
+            if (TextUtils.equals(menuItem.title, menuLabel)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 用于保存悬浮球的位置记录
+     *
+     * @param key          String
+     * @param defaultValue int
+     * @return int
+     */
+    private int getSetting(String key, int defaultValue) {
+        try {
+            SharedPreferences sp = mContext.getSharedPreferences("floatLogo", 0);
+            return sp.getInt(key, defaultValue);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return defaultValue;
+    }
+
+    /**
+     * 用于保存悬浮球的位置记录
+     *
+     * @param key   String
+     * @param value int
+     */
+    public void saveSetting(String key, int value) {
+        try {
+            SharedPreferences.Editor sp = mContext.getSharedPreferences("floatLogo", 0).edit();
+            sp.putInt(key, value);
+            sp.apply();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static int dp2Px(float dp, Context mContext) {
+        return (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                dp,
+                mContext.getResources().getDisplayMetrics());
+    }
+
+
+    public interface OnMenuClickListener {
+        void onMenuExpended(boolean isExpened);
+    }
+
+
+    public void setValueAnimator() {
+
+    }
+
+    public static final class Builder {
+        private String mBackMenuColor;
+        private boolean mDrawRedPointNum;
+        private boolean mCicleMenuBg;
+        private Bitmap mLogoRes;
+        private int mDefaultLocation;
+        private List<FloatItem> mFloatItems = new ArrayList<>();
+        private Context mContext;
+        private FloatMenuView.OnMenuClickListener mOnMenuClickListener;
+        private Drawable mDrawable;
+
+
+        public Builder setBgDrawable(Drawable drawable) {
+            mDrawable = drawable;
+            return this;
+        }
+
+        public Builder() {
+        }
+
+        public Builder setFloatItems(List<FloatItem> mFloatItems) {
+            this.mFloatItems = mFloatItems;
+            return this;
+        }
+
+        public Builder addFloatItem(FloatItem floatItem) {
+            this.mFloatItems.add(floatItem);
+            return this;
+        }
+
+        public Builder backMenuColor(String color) {
+            mBackMenuColor = color;
+            return this;
+        }
+
+        public Builder drawRedPointNum(boolean val) {
+            mDrawRedPointNum = val;
+            return this;
+        }
+
+        public Builder drawCicleMenuBg(boolean val) {
+            mCicleMenuBg = val;
+            return this;
+        }
+
+        public Builder logo(Bitmap bitmap) {
+            mLogoRes = bitmap;
+            return this;
+        }
+
+        public Builder withActivity(Activity activity) {
+            mContext = activity;
+            return this;
+        }
+
+        public Builder withContext(Context context) {
+            mContext = context;
+            return this;
+        }
+
+        public Builder setOnMenuItemClickListener(FloatMenuView.OnMenuClickListener val) {
+            mOnMenuClickListener = val;
+            return this;
+        }
+
+        public Builder defaultLocation(int location) {
+            mDefaultLocation = location;
+            return this;
+        }
+
+        public FloatLogoMenu showWithListener(FloatMenuView.OnMenuClickListener listener) {
+            mOnMenuClickListener = listener;
+            return new FloatLogoMenu(this);
+        }
+
+        public FloatLogoMenu showWithLogo(Bitmap logo) {
+            mLogoRes = logo;
+            return new FloatLogoMenu(this);
+        }
+
+        public FloatLogoMenu show() {
+            return new FloatLogoMenu(this);
+        }
+    }
+
+
+}

+ 413 - 0
library_support/src/main/java/cn/yyxx/support/ui/floating/FloatMenuView.java

@@ -0,0 +1,413 @@
+/*
+ * Copyright (c) 2016, Shanghai YUEWEN Information Technology Co., Ltd.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *  Neither the name of Shanghai YUEWEN Information Technology Co., Ltd. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY SHANGHAI YUEWEN INFORMATION TECHNOLOGY CO., LTD. AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package cn.yyxx.support.ui.floating;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import cn.yyxx.support.hawkeye.LogUtils;
+
+public class FloatMenuView extends View {
+    public static final int STATUS_LEFT = 3;//展开左边菜单
+    public static final int STATUS_RIGHT = 4;//展开右边菜单
+
+    private int mStatus = STATUS_RIGHT;//默认右边
+
+    private Paint mPaint;//画笔
+    private int mBackgroundColor = 0x00FFFFFF;//默认背景颜色 完全透明的白色
+
+    private int mMenuBackgroundColor = -1;//菜单的背景颜色
+
+    private RectF mBgRect;//菜单的背景矩阵
+    private int mItemWidth = dip2px(30);//菜单项的宽度
+    private int mItemHeight = dip2px(30);//菜单项的高度
+    private int mItemLeft = 0;//菜单项左边的默认偏移值,这里是0
+    //    private int mCorner = dip2px(2);//菜单背景的圆角多出的宽度
+    private int mCorner = 0;
+
+    private int mRadius = dip2px(4);//红点消息半径
+    private final int mRedPointRadiusWithNoNum = dip2px(3);//红点圆半径
+
+    private int mFontSizePointNum = sp2px(10);//红点消息数字的文字大小
+
+    private int mFontSizeTitle = sp2px(10);//菜单项的title的文字大小
+    private float mFirstItemTop;//菜单项的最小y值,上面起始那条线
+    private boolean mDrawNum = false;//是否绘制数字,false则只绘制红点
+    private boolean circleBg = false;//菜单项背景是否绘制成圆形,false则绘制矩阵
+
+    private Canvas canvas = null;
+    private List<FloatItem> mItemList = new ArrayList<>();//菜单项的内容
+    private List<RectF> mItemRectList = new ArrayList<>();//菜单项所占用位置的记录,用于判断点击事件
+
+    private OnMenuClickListener mOnMenuClickListener;//菜单项的点击事件回调
+
+    private ObjectAnimator mAlphaAnim;//消失关闭动画的透明值
+
+    //设置菜单内容集合
+    public void setItemList(List<FloatItem> itemList) {
+        mItemList = itemList;
+    }
+
+    //设置是否绘制红点数字
+    public void drawNum(boolean drawNum) {
+        mDrawNum = drawNum;
+    }
+
+    //设置是否绘制圆形菜单,否则矩阵
+    public void setCircleBg(boolean circleBg) {
+        this.circleBg = circleBg;
+    }
+
+    //用于标记所依赖的view的screen的坐标,实际view的坐标是window坐标,所以这里后面会减去状态栏的高度
+
+
+    //设置菜单的背景颜色
+    public void setMenuBackgroundColor(int mMenuBackgroundColor) {
+        this.mMenuBackgroundColor = mMenuBackgroundColor;
+    }
+
+    //设置这个view(整个屏幕)的背景,这里默认透明
+    public void setBackgroundColor(int BackgroundColor) {
+        this.mBackgroundColor = BackgroundColor;
+    }
+
+
+    //下面开始的注释我写不动了,看不懂的话请自行领悟吧
+    public FloatMenuView(Context context) {
+        super(context);
+    }
+
+    public FloatMenuView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FloatMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public FloatMenuView(Context baseContext, int status) {
+        super(baseContext);
+        mStatus = status;
+        int screenWidth = getResources().getDisplayMetrics().widthPixels;
+        int screenHeight = getResources().getDisplayMetrics().heightPixels;
+        LogUtils.d(screenWidth);
+        LogUtils.d(screenHeight);
+        mBgRect = new RectF(0, 0, screenWidth, screenHeight);
+        initView();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        setMeasuredDimension(mItemWidth * mItemList.size(), mItemHeight);
+    }
+
+    private void initView() {
+        mPaint = new Paint();
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.FILL);
+        mPaint.setTextSize(sp2px(10));
+
+        mAlphaAnim = ObjectAnimator.ofFloat(this, "alpha", 1.0f, 0f);
+        mAlphaAnim.setDuration(50);
+        mAlphaAnim.addListener(new MyAnimListener() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mOnMenuClickListener != null) {
+                    removeView();
+                    mOnMenuClickListener.dismiss();
+                }
+            }
+        });
+
+        mFirstItemTop = 0;
+        mItemLeft = 0;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        this.canvas = canvas;
+        switch (mStatus) {
+            case STATUS_LEFT:
+            case STATUS_RIGHT:
+                drawBackground(canvas);
+                drawFloatLeftItem(canvas);
+                break;
+        }
+    }
+
+    private void drawBackground(Canvas canvas) {
+        mPaint.setColor(mBackgroundColor);
+        canvas.drawRect(mBgRect, mPaint);
+
+    }
+
+    private void drawFloatLeftItem(Canvas canvas) {
+        mItemRectList.clear();
+        for (int i = 0; i < mItemList.size(); i++) {
+            canvas.save();
+            mPaint.setColor(mMenuBackgroundColor);
+            if (circleBg) {
+                float cx = (mItemLeft + i * mItemWidth) + mItemWidth / 2f;//x中心点
+                float cy = mFirstItemTop + mItemHeight / 2f;//y中心点
+                float radius = mItemWidth / 2f;//半径
+                canvas.drawCircle(cx, cy, radius, mPaint);
+            } else {
+                mPaint.setColor(Color.parseColor(mItemList.get(i).bgColor));
+                canvas.drawRect(mItemLeft + i * mItemWidth, mFirstItemTop, mItemLeft + mItemWidth + i * mItemWidth, mFirstItemTop + mItemHeight, mPaint);
+            }
+
+            mItemRectList.add(new RectF(mItemLeft + i * mItemWidth, mFirstItemTop, mItemLeft + mItemWidth + i * mItemWidth, mFirstItemTop + mItemHeight));
+            mPaint.setColor(Color.parseColor(mItemList.get(i).bgColor));
+            drawIconTitleDot(canvas, i);
+        }
+        canvas.restore();
+    }
+
+
+    private void drawIconTitleDot(Canvas canvas, int position) {
+        FloatItem floatItem = mItemList.get(position);
+
+        if (floatItem.icon != null) {
+            float centerX = mItemLeft + mItemWidth / 2f + (mItemWidth) * position;//计算每一个item的中心点x的坐标值
+            float centerY = mFirstItemTop + mItemHeight / 2f;//计算每一个item的中心点的y坐标值
+
+            float left = centerX - mItemWidth / 4f;//计算icon的左坐标值 中心点往左移宽度的四分之一
+            float right = centerX + mItemWidth / 4f;
+
+            float iconH = mItemHeight * 0.5f;//计算出icon的宽度 = icon的高度
+
+            float textH = getTextHeight(floatItem.title, mPaint);
+            float paddingH = (mItemHeight - iconH - textH - mRadius) / 2;//总高度减去文字的高度,减去icon高度,再除以2就是上下的间距剩余
+
+            float top = centerY - mItemHeight / 2f + paddingH;//计算icon的上坐标值
+            float bottom = top + iconH;//剩下的高度空间用于画文字
+
+            //画icon
+            mPaint.setColor(Color.parseColor(floatItem.titleColor));
+            canvas.drawBitmap(floatItem.icon, null, new RectF(left, top, right, bottom), mPaint);
+
+            if (!TextUtils.isEmpty(floatItem.dotNum) && !floatItem.dotNum.equals("0")) {
+                float dotLeft = centerX + mItemWidth / 5f;
+                float cx = dotLeft + mCorner;//x中心点
+                float cy = top + mCorner;//y中心点
+
+                int radiu = mDrawNum ? mRadius : mRedPointRadiusWithNoNum;
+                //画红点
+                mPaint.setColor(Color.RED);
+                canvas.drawCircle(cx, cy, radiu, mPaint);
+                if (mDrawNum) {
+                    mPaint.setColor(Color.WHITE);
+                    mPaint.setTextSize(mFontSizePointNum);
+                    //画红点消息数
+                    canvas.drawText(floatItem.dotNum, cx - getTextWidth(floatItem.dotNum, mPaint) / 2, cy + getTextHeight(floatItem.dotNum, mPaint) / 2, mPaint);
+                }
+            }
+            mPaint.setColor(Color.parseColor(floatItem.titleColor));
+            mPaint.setTextSize(mFontSizeTitle);
+            //画menu title
+            canvas.drawText(floatItem.title, centerX - getTextWidth(floatItem.title, mPaint) / 2, centerY + iconH / 2 + getTextHeight(floatItem.title, mPaint) / 2, mPaint);
+        }
+    }
+
+
+    public void startAnim() {
+        if (mItemList.size() == 0) {
+            return;
+        }
+        invalidate();
+    }
+
+
+    public void dismiss() {
+        if (!mAlphaAnim.isRunning()) {
+            mAlphaAnim.start();
+        }
+    }
+
+    private void removeView() {
+        ViewGroup vg = (ViewGroup) this.getParent();
+        if (vg != null) {
+            vg.removeView(this);
+        }
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        if (visibility == GONE) {
+            if (mOnMenuClickListener != null) {
+                mOnMenuClickListener.dismiss();
+            }
+        }
+        super.onWindowVisibilityChanged(visibility);
+
+
+    }
+
+    public void setOnMenuClickListener(OnMenuClickListener onMenuClickListener) {
+        this.mOnMenuClickListener = onMenuClickListener;
+
+    }
+
+    public interface OnMenuClickListener {
+        void onItemClick(int position, String title);
+
+        void dismiss();
+
+    }
+
+    private abstract class MyAnimListener implements Animator.AnimatorListener {
+        @Override
+        public void onAnimationStart(Animator animation) {
+
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+
+        }
+    }
+
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                for (int i = 0; i < mItemRectList.size(); i++) {
+                    if (mOnMenuClickListener != null && isPointInRect(new PointF(event.getX(), event.getY()), mItemRectList.get(i))) {
+                        mOnMenuClickListener.onItemClick(i, mItemList.get(i).title);
+                        return true;
+                    }
+                }
+                dismiss();
+        }
+        return false;
+    }
+
+    private boolean isPointInRect(PointF pointF, RectF targetRect) {
+        return pointF.x >= targetRect.left && pointF.x <= targetRect.right && pointF.y >= targetRect.top && pointF.y <= targetRect.bottom;
+    }
+
+
+    public static class Builder {
+        private Context mContext;
+        private List<FloatItem> mFloatItems = new ArrayList<>();
+        private int mBgColor = Color.TRANSPARENT;
+        private int mStatus = STATUS_LEFT;
+        private boolean circleBg = false;
+        private int mMenuBackgroundColor = -1;
+        private boolean mDrawNum = false;
+
+
+        public Builder(Context activity) {
+            mContext = activity;
+        }
+
+        public Builder drawNum(boolean drawNum) {
+            mDrawNum = drawNum;
+            return this;
+        }
+
+
+        public Builder setMenuBackgroundColor(int mMenuBackgroundColor) {
+            this.mMenuBackgroundColor = mMenuBackgroundColor;
+            return this;
+        }
+
+
+        public Builder setCircleBg(boolean circleBg) {
+            this.circleBg = circleBg;
+            return this;
+        }
+
+        public Builder setStatus(int status) {
+            mStatus = status;
+            return this;
+        }
+
+        public Builder setFloatItems(List<FloatItem> floatItems) {
+            this.mFloatItems = floatItems;
+            return this;
+        }
+
+
+        public Builder addItem(FloatItem floatItem) {
+            mFloatItems.add(floatItem);
+            return this;
+        }
+
+        public Builder addItems(List<FloatItem> list) {
+            mFloatItems.addAll(list);
+            return this;
+        }
+
+        public Builder setBackgroundColor(int color) {
+            mBgColor = color;
+            return this;
+        }
+
+        public FloatMenuView create() {
+            FloatMenuView floatMenuView = new FloatMenuView(mContext, mStatus);
+            floatMenuView.setItemList(mFloatItems);
+            floatMenuView.setBackgroundColor(mBgColor);
+            floatMenuView.setCircleBg(circleBg);
+            floatMenuView.startAnim();
+            floatMenuView.drawNum(mDrawNum);
+            floatMenuView.setMenuBackgroundColor(mMenuBackgroundColor);
+            return floatMenuView;
+        }
+
+    }
+
+
+    private int dip2px(float dipValue) {
+        final float scale = getContext().getResources().getDisplayMetrics().density;
+        return (int) (dipValue * scale + 0.5f);
+    }
+
+    private int sp2px(float spValue) {
+        final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity;
+        return (int) (spValue * fontScale + 0.5f);
+    }
+
+    private float getTextHeight(String text, Paint paint) {
+        Rect rect = new Rect();
+        paint.getTextBounds(text, 0, text.length(), rect);
+        return rect.height() / 1.1f;
+    }
+
+    private float getTextWidth(String text, Paint paint) {
+        return paint.measureText(text);
+    }
+}

+ 1 - 0
library_volley/.gitignore

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

+ 47 - 0
library_volley/build.gradle

@@ -0,0 +1,47 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdkVersion COMPILE_SDK_VERSION
+    buildToolsVersion BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        minSdkVersion MIN_SDK_VERSION
+        targetSdkVersion TARGET_SDK_VERSION
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles 'proguard-rules.pro'
+        }
+    }
+
+    buildFeatures {
+        buildConfig = false
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    repositories {
+        flatDir {
+            dirs 'libs'
+        }
+    }
+
+    dexOptions {
+        preDexLibraries = false
+    }
+
+    //api23以上使用 httpClient
+    useLibrary 'org.apache.http.legacy'
+}
+
+dependencies {
+    compileOnly files('../libs/android-support-v4.jar')
+}
+
+apply from: 'buildJar.gradle'

+ 17 - 0
library_volley/buildJar.gradle

@@ -0,0 +1,17 @@
+def SDK_BASE_NAME = 'yyxx_support_volley'
+def SDK_VERSION = '1.0.0'
+def SEPARATOR = '_'
+def SDK_DST_PATH = 'build/jar/'
+def ZIP_FILE = file('build/intermediates/aar_main_jar/release/classes.jar')
+
+task deleteBaseBuild(type: Delete) {
+    delete SDK_DST_PATH
+}
+
+task makeJar(type: Jar) {
+    from zipTree(ZIP_FILE)
+    baseName = SDK_BASE_NAME + SEPARATOR + SDK_VERSION
+    destinationDir = file(SDK_DST_PATH)
+}
+
+makeJar.dependsOn(deleteBaseBuild, build)

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

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

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

+ 0 - 0
library_support/src/main/java/android/support/annotation/GuardedBy.java → library_volley/src/main/java/android/support/annotation/GuardedBy.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/AuthFailureError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/AuthFailureError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/Cache.java → library_volley/src/main/java/cn/yyxx/support/volley/source/Cache.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/CacheDispatcher.java → library_volley/src/main/java/cn/yyxx/support/volley/source/CacheDispatcher.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/ClientError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/ClientError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/DefaultRetryPolicy.java → library_volley/src/main/java/cn/yyxx/support/volley/source/DefaultRetryPolicy.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/ExecutorDelivery.java → library_volley/src/main/java/cn/yyxx/support/volley/source/ExecutorDelivery.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/Header.java → library_volley/src/main/java/cn/yyxx/support/volley/source/Header.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/Network.java → library_volley/src/main/java/cn/yyxx/support/volley/source/Network.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/NetworkDispatcher.java → library_volley/src/main/java/cn/yyxx/support/volley/source/NetworkDispatcher.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/NetworkError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/NetworkError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/NetworkResponse.java → library_volley/src/main/java/cn/yyxx/support/volley/source/NetworkResponse.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/NoConnectionError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/NoConnectionError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/ParseError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/ParseError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/Request.java → library_volley/src/main/java/cn/yyxx/support/volley/source/Request.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/RequestQueue.java → library_volley/src/main/java/cn/yyxx/support/volley/source/RequestQueue.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/Response.java → library_volley/src/main/java/cn/yyxx/support/volley/source/Response.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/ResponseDelivery.java → library_volley/src/main/java/cn/yyxx/support/volley/source/ResponseDelivery.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/RetryPolicy.java → library_volley/src/main/java/cn/yyxx/support/volley/source/RetryPolicy.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/ServerError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/ServerError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/TimeoutError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/TimeoutError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/VolleyError.java → library_volley/src/main/java/cn/yyxx/support/volley/source/VolleyError.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/VolleyLog.java → library_volley/src/main/java/cn/yyxx/support/volley/source/VolleyLog.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/AdaptedHttpStack.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/AdaptedHttpStack.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/AndroidAuthenticator.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/AndroidAuthenticator.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/Authenticator.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/Authenticator.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/BaseHttpStack.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/BaseHttpStack.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/BasicNetwork.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/BasicNetwork.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ByteArrayPool.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ByteArrayPool.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ClearCacheRequest.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ClearCacheRequest.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/DiskBasedCache.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/DiskBasedCache.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpClientStack.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpClientStack.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpHeaderParser.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpHeaderParser.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpResponse.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpResponse.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpStack.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpStack.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HurlStack.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/HurlStack.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageLoader.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageLoader.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageRequest.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageRequest.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonArrayRequest.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonArrayRequest.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonObjectRequest.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonObjectRequest.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonRequest.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonRequest.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/NetworkImageView.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/NetworkImageView.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/NoCache.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/NoCache.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/PoolingByteArrayOutputStream.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/PoolingByteArrayOutputStream.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/RequestFuture.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/RequestFuture.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/StringRequest.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/StringRequest.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/Threads.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/Threads.java


+ 0 - 0
library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/Volley.java → library_volley/src/main/java/cn/yyxx/support/volley/source/toolbox/Volley.java


+ 1 - 0
library_volleyx/.gitignore

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

+ 48 - 0
library_volleyx/build.gradle

@@ -0,0 +1,48 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdkVersion COMPILE_SDK_VERSION
+    buildToolsVersion BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        minSdkVersion MIN_SDK_VERSION
+        targetSdkVersion TARGET_SDK_VERSION
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles 'proguard-rules.pro'
+        }
+    }
+
+    buildFeatures {
+        buildConfig = false
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    repositories {
+        flatDir {
+            dirs 'libs'
+        }
+    }
+
+    dexOptions {
+        preDexLibraries = false
+    }
+
+    //api23以上使用 httpClient
+    useLibrary 'org.apache.http.legacy'
+}
+
+dependencies {
+    implementation "org.chromium.net:cronet-embedded:76.3809.111"
+    implementation 'androidx.core:core:1.5.0'
+}
+
+apply from: 'buildJar.gradle'

+ 17 - 0
library_volleyx/buildJar.gradle

@@ -0,0 +1,17 @@
+def SDK_BASE_NAME = 'yyxx_support_volleyx'
+def SDK_VERSION = '1.0.0'
+def SEPARATOR = '_'
+def SDK_DST_PATH = 'build/jar/'
+def ZIP_FILE = file('build/intermediates/aar_main_jar/release/classes.jar')
+
+task deleteBaseBuild(type: Delete) {
+    delete SDK_DST_PATH
+}
+
+task makeJar(type: Jar) {
+    from zipTree(ZIP_FILE)
+    baseName = SDK_BASE_NAME + SEPARATOR + SDK_VERSION
+    destinationDir = file(SDK_DST_PATH)
+}
+
+makeJar.dependsOn(deleteBaseBuild, build)

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

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

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

+ 89 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/AsyncCache.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import androidx.annotation.Nullable;
+
+/** Asynchronous equivalent to the {@link Cache} interface. */
+public abstract class AsyncCache {
+
+    public interface OnGetCompleteCallback {
+        /**
+         * Invoked when the read from the cache is complete.
+         *
+         * @param entry The entry read from the cache, or null if the read failed or the key did not
+         *     exist in the cache.
+         */
+        void onGetComplete(@Nullable Cache.Entry entry);
+    }
+
+    /**
+     * Retrieves an entry from the cache and sends it back through the {@link
+     * OnGetCompleteCallback#onGetComplete} function
+     *
+     * @param key Cache key
+     * @param callback Callback that will be notified when the information has been retrieved
+     */
+    public abstract void get(String key, OnGetCompleteCallback callback);
+
+    public interface OnWriteCompleteCallback {
+        /** Invoked when the cache operation is complete */
+        void onWriteComplete();
+    }
+
+    /**
+     * Writes a {@link Cache.Entry} to the cache, and calls {@link
+     * OnWriteCompleteCallback#onWriteComplete} after the operation is finished.
+     *
+     * @param key Cache key
+     * @param entry The entry to be written to the cache
+     * @param callback Callback that will be notified when the information has been written
+     */
+    public abstract void put(String key, Cache.Entry entry, OnWriteCompleteCallback callback);
+
+    /**
+     * Clears the cache. Deletes all cached files from disk. Calls {@link
+     * OnWriteCompleteCallback#onWriteComplete} after the operation is finished.
+     */
+    public abstract void clear(OnWriteCompleteCallback callback);
+
+    /**
+     * Initializes the cache and calls {@link OnWriteCompleteCallback#onWriteComplete} after the
+     * operation is finished.
+     */
+    public abstract void initialize(OnWriteCompleteCallback callback);
+
+    /**
+     * Invalidates an entry in the cache and calls {@link OnWriteCompleteCallback#onWriteComplete}
+     * after the operation is finished.
+     *
+     * @param key Cache key
+     * @param fullExpire True to fully expire the entry, false to soft expire
+     * @param callback Callback that's invoked once the entry has been invalidated
+     */
+    public abstract void invalidate(
+            String key, boolean fullExpire, OnWriteCompleteCallback callback);
+
+    /**
+     * Removes a {@link Cache.Entry} from the cache, and calls {@link
+     * OnWriteCompleteCallback#onWriteComplete} after the operation is finished.
+     *
+     * @param key Cache key
+     * @param callback Callback that's invoked once the entry has been removed
+     */
+    public abstract void remove(String key, OnWriteCompleteCallback callback);
+}

+ 156 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/AsyncNetwork.java

@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import androidx.annotation.RestrictTo;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An asynchronous implementation of {@link Network} to perform requests.
+ */
+public abstract class AsyncNetwork implements Network {
+    private ExecutorService mBlockingExecutor;
+    private ExecutorService mNonBlockingExecutor;
+    private ScheduledExecutorService mNonBlockingScheduledExecutor;
+
+    protected AsyncNetwork() {
+    }
+
+    /**
+     * Interface for callback to be called after request is processed.
+     */
+    public interface OnRequestComplete {
+        /**
+         * Method to be called after successful network request.
+         */
+        void onSuccess(NetworkResponse networkResponse);
+
+        /**
+         * Method to be called after unsuccessful network request.
+         */
+        void onError(VolleyError volleyError);
+    }
+
+    /**
+     * Non-blocking method to perform the specified request.
+     *
+     * @param request  Request to process
+     * @param callback to be called once NetworkResponse is received
+     */
+    public abstract void performRequest(Request<?> request, OnRequestComplete callback);
+
+    /**
+     * Blocking method to perform network request.
+     *
+     * @param request Request to process
+     * @return response retrieved from the network
+     * @throws VolleyError in the event of an error
+     */
+    @Override
+    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicReference<NetworkResponse> response = new AtomicReference<>();
+        final AtomicReference<VolleyError> error = new AtomicReference<>();
+        performRequest(
+                request,
+                new OnRequestComplete() {
+                    @Override
+                    public void onSuccess(NetworkResponse networkResponse) {
+                        response.set(networkResponse);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(VolleyError volleyError) {
+                        error.set(volleyError);
+                        latch.countDown();
+                    }
+                });
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            VolleyLog.e(e, "while waiting for CountDownLatch");
+            Thread.currentThread().interrupt();
+            throw new VolleyError(e);
+        }
+
+        if (response.get() != null) {
+            return response.get();
+        } else if (error.get() != null) {
+            throw error.get();
+        } else {
+            throw new VolleyError("Neither response entry was set");
+        }
+    }
+
+    /**
+     * This method sets the non blocking executor to be used by the network for non-blocking tasks.
+     *
+     * <p>This method must be called before performing any requests.
+     */
+    @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+    public void setNonBlockingExecutor(ExecutorService executor) {
+        mNonBlockingExecutor = executor;
+    }
+
+    /**
+     * This method sets the blocking executor to be used by the network for potentially blocking
+     * tasks.
+     *
+     * <p>This method must be called before performing any requests.
+     */
+    @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+    public void setBlockingExecutor(ExecutorService executor) {
+        mBlockingExecutor = executor;
+    }
+
+    /**
+     * This method sets the scheduled executor to be used by the network for non-blocking tasks to
+     * be scheduled.
+     *
+     * <p>This method must be called before performing any requests.
+     */
+    @RestrictTo({RestrictTo.Scope.LIBRARY_GROUP})
+    public void setNonBlockingScheduledExecutor(ScheduledExecutorService executor) {
+        mNonBlockingScheduledExecutor = executor;
+    }
+
+    /**
+     * Gets blocking executor to perform any potentially blocking tasks.
+     */
+    protected ExecutorService getBlockingExecutor() {
+        return mBlockingExecutor;
+    }
+
+    /**
+     * Gets non-blocking executor to perform any non-blocking tasks.
+     */
+    protected ExecutorService getNonBlockingExecutor() {
+        return mNonBlockingExecutor;
+    }
+
+    /**
+     * Gets scheduled executor to perform any non-blocking tasks that need to be scheduled.
+     */
+    protected ScheduledExecutorService getNonBlockingScheduledExecutor() {
+        return mNonBlockingScheduledExecutor;
+    }
+}

+ 676 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/AsyncRequestQueue.java

@@ -0,0 +1,676 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.net.HttpURLConnection;
+import java.util.Comparator;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import cn.yyxx.support.volley.source.AsyncCache.OnGetCompleteCallback;
+import cn.yyxx.support.volley.source.AsyncNetwork.OnRequestComplete;
+import cn.yyxx.support.volley.source.Cache.Entry;
+
+/**
+ * An asynchronous request dispatch queue.
+ *
+ * <p>Add requests to the queue with {@link #add(Request)}. Once completed, responses will be
+ * delivered on the main thread (unless a custom {@link ResponseDelivery} has been provided)
+ */
+public class AsyncRequestQueue extends RequestQueue {
+    /**
+     * Default number of blocking threads to start.
+     */
+    private static final int DEFAULT_BLOCKING_THREAD_POOL_SIZE = 4;
+
+    /**
+     * AsyncCache used to retrieve and store responses.
+     *
+     * <p>{@code null} indicates use of blocking Cache.
+     */
+    @Nullable
+    private final AsyncCache mAsyncCache;
+
+    /**
+     * AsyncNetwork used to perform nework requests.
+     */
+    private final AsyncNetwork mNetwork;
+
+    /**
+     * Executor for non-blocking tasks.
+     */
+    private ExecutorService mNonBlockingExecutor;
+
+    /**
+     * Executor to be used for non-blocking tasks that need to be scheduled.
+     */
+    private ScheduledExecutorService mNonBlockingScheduledExecutor;
+
+    /**
+     * Executor for blocking tasks.
+     *
+     * <p>Some tasks in handling requests may not be easy to implement in a non-blocking way, such
+     * as reading or parsing the response data. This executor is used to run these tasks.
+     */
+    private ExecutorService mBlockingExecutor;
+
+    /**
+     * This interface may be used by advanced applications to provide custom executors according to
+     * their needs. Apps must create ExecutorServices dynamically given a blocking queue rather than
+     * providing them directly so that Volley can provide a PriorityQueue which will prioritize
+     * requests according to Request#getPriority.
+     */
+    private ExecutorFactory mExecutorFactory;
+
+    /**
+     * Manage list of waiting requests and de-duplicate requests with same cache key.
+     */
+    private final WaitingRequestManager mWaitingRequestManager = new WaitingRequestManager(this);
+
+    /**
+     * Sets all the variables, but processing does not begin until {@link #start()} is called.
+     *
+     * @param cache            to use for persisting responses to disk. If an AsyncCache was provided, then
+     *                         this will be a {@link ThrowingCache}
+     * @param network          to perform HTTP requests
+     * @param asyncCache       to use for persisting responses to disk. May be null to indicate use of
+     *                         blocking cache
+     * @param responseDelivery interface for posting responses and errors
+     * @param executorFactory  Interface to be used to provide custom executors according to the
+     *                         users needs.
+     */
+    private AsyncRequestQueue(
+            Cache cache,
+            AsyncNetwork network,
+            @Nullable AsyncCache asyncCache,
+            ResponseDelivery responseDelivery,
+            ExecutorFactory executorFactory) {
+        super(cache, network, /* threadPoolSize= */ 0, responseDelivery);
+        mAsyncCache = asyncCache;
+        mNetwork = network;
+        mExecutorFactory = executorFactory;
+    }
+
+    /**
+     * Sets the executors and initializes the cache.
+     */
+    @Override
+    public void start() {
+        stop(); // Make sure any currently running threads are stopped
+
+        // Create blocking / non-blocking executors and set them in the network and stack.
+        mNonBlockingExecutor = mExecutorFactory.createNonBlockingExecutor(getBlockingQueue());
+        mBlockingExecutor = mExecutorFactory.createBlockingExecutor(getBlockingQueue());
+        mNonBlockingScheduledExecutor = mExecutorFactory.createNonBlockingScheduledExecutor();
+        mNetwork.setBlockingExecutor(mBlockingExecutor);
+        mNetwork.setNonBlockingExecutor(mNonBlockingExecutor);
+        mNetwork.setNonBlockingScheduledExecutor(mNonBlockingScheduledExecutor);
+
+        mNonBlockingExecutor.execute(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        // This is intentionally blocking, because we don't want to process any
+                        // requests until the cache is initialized.
+                        if (mAsyncCache != null) {
+                            final CountDownLatch latch = new CountDownLatch(1);
+                            mAsyncCache.initialize(
+                                    new AsyncCache.OnWriteCompleteCallback() {
+                                        @Override
+                                        public void onWriteComplete() {
+                                            latch.countDown();
+                                        }
+                                    });
+                            try {
+                                latch.await();
+                            } catch (InterruptedException e) {
+                                VolleyLog.e(e, "Thread was interrupted while initializing the cache.");
+                                Thread.currentThread().interrupt();
+                                throw new RuntimeException(e);
+                            }
+                        } else {
+                            getCache().initialize();
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Shuts down and nullifies both executors
+     */
+    @Override
+    public void stop() {
+        if (mNonBlockingExecutor != null) {
+            mNonBlockingExecutor.shutdownNow();
+            mNonBlockingExecutor = null;
+        }
+        if (mBlockingExecutor != null) {
+            mBlockingExecutor.shutdownNow();
+            mBlockingExecutor = null;
+        }
+        if (mNonBlockingScheduledExecutor != null) {
+            mNonBlockingScheduledExecutor.shutdownNow();
+            mNonBlockingScheduledExecutor = null;
+        }
+    }
+
+    /**
+     * Begins the request by sending it to the Cache or Network.
+     */
+    @Override
+    <T> void beginRequest(Request<T> request) {
+        // If the request is uncacheable, send it over the network.
+        if (request.shouldCache()) {
+            if (mAsyncCache != null) {
+                mNonBlockingExecutor.execute(new CacheTask<>(request));
+            } else {
+                mBlockingExecutor.execute(new CacheTask<>(request));
+            }
+        } else {
+            sendRequestOverNetwork(request);
+        }
+    }
+
+    @Override
+    <T> void sendRequestOverNetwork(Request<T> request) {
+        mNonBlockingExecutor.execute(new NetworkTask<>(request));
+    }
+
+    /**
+     * Runnable that gets an entry from the cache.
+     */
+    private class CacheTask<T> extends RequestTask<T> {
+        CacheTask(Request<T> request) {
+            super(request);
+        }
+
+        @Override
+        public void run() {
+            // If the request has been canceled, don't bother dispatching it.
+            if (mRequest.isCanceled()) {
+                mRequest.finish("cache-discard-canceled");
+                return;
+            }
+
+            mRequest.addMarker("cache-queue-take");
+
+            // Attempt to retrieve this item from cache.
+            if (mAsyncCache != null) {
+                mAsyncCache.get(
+                        mRequest.getCacheKey(),
+                        new OnGetCompleteCallback() {
+                            @Override
+                            public void onGetComplete(Entry entry) {
+                                handleEntry(entry, mRequest);
+                            }
+                        });
+            } else {
+                Entry entry = getCache().get(mRequest.getCacheKey());
+                handleEntry(entry, mRequest);
+            }
+        }
+    }
+
+    /**
+     * Helper method that handles the cache entry after getting it from the Cache.
+     */
+    private void handleEntry(final Entry entry, final Request<?> mRequest) {
+        if (entry == null) {
+            mRequest.addMarker("cache-miss");
+            // Cache miss; send off to the network dispatcher.
+            if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
+                sendRequestOverNetwork(mRequest);
+            }
+            return;
+        }
+
+        // If it is completely expired, just send it to the network.
+        if (entry.isExpired()) {
+            mRequest.addMarker("cache-hit-expired");
+            mRequest.setCacheEntry(entry);
+            if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
+                sendRequestOverNetwork(mRequest);
+            }
+            return;
+        }
+
+        // We have a cache hit; parse its data for delivery back to the request.
+        mBlockingExecutor.execute(new CacheParseTask<>(mRequest, entry));
+    }
+
+    private class CacheParseTask<T> extends RequestTask<T> {
+        Cache.Entry entry;
+
+        CacheParseTask(Request<T> request, Cache.Entry entry) {
+            super(request);
+            this.entry = entry;
+        }
+
+        @Override
+        public void run() {
+            mRequest.addMarker("cache-hit");
+            Response<?> response =
+                    mRequest.parseNetworkResponse(
+                            new NetworkResponse(
+                                    HttpURLConnection.HTTP_OK,
+                                    entry.data,
+                                    /* notModified= */ false,
+                                    /* networkTimeMs= */ 0,
+                                    entry.allResponseHeaders));
+            mRequest.addMarker("cache-hit-parsed");
+
+            if (!entry.refreshNeeded()) {
+                // Completely unexpired cache hit. Just deliver the response.
+                getResponseDelivery().postResponse(mRequest, response);
+            } else {
+                // Soft-expired cache hit. We can deliver the cached response,
+                // but we need to also send the request to the network for
+                // refreshing.
+                mRequest.addMarker("cache-hit-refresh-needed");
+                mRequest.setCacheEntry(entry);
+                // Mark the response as intermediate.
+                response.intermediate = true;
+
+                if (!mWaitingRequestManager.maybeAddToWaitingRequests(mRequest)) {
+                    // Post the intermediate response back to the user and have
+                    // the delivery then forward the request along to the network.
+                    getResponseDelivery()
+                            .postResponse(
+                                    mRequest,
+                                    response,
+                                    new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            sendRequestOverNetwork(mRequest);
+                                        }
+                                    });
+                } else {
+                    // request has been added to list of waiting requests
+                    // to receive the network response from the first request once it
+                    // returns.
+                    getResponseDelivery().postResponse(mRequest, response);
+                }
+            }
+        }
+    }
+
+    private class ParseErrorTask<T> extends RequestTask<T> {
+        VolleyError volleyError;
+
+        ParseErrorTask(Request<T> request, VolleyError volleyError) {
+            super(request);
+            this.volleyError = volleyError;
+        }
+
+        @Override
+        public void run() {
+            VolleyError parsedError = mRequest.parseNetworkError(volleyError);
+            getResponseDelivery().postError(mRequest, parsedError);
+            mRequest.notifyListenerResponseNotUsable();
+        }
+    }
+
+    /**
+     * Runnable that performs the network request
+     */
+    private class NetworkTask<T> extends RequestTask<T> {
+        NetworkTask(Request<T> request) {
+            super(request);
+        }
+
+        @Override
+        public void run() {
+            // If the request was cancelled already, do not perform the network request.
+            if (mRequest.isCanceled()) {
+                mRequest.finish("network-discard-cancelled");
+                mRequest.notifyListenerResponseNotUsable();
+                return;
+            }
+
+            final long startTimeMs = SystemClock.elapsedRealtime();
+            mRequest.addMarker("network-queue-take");
+
+            // TODO: Figure out what to do with traffic stats tags. Can this be pushed to the
+            // HTTP stack, or is it no longer feasible to support?
+
+            // Perform the network request.
+            mNetwork.performRequest(
+                    mRequest,
+                    new OnRequestComplete() {
+                        @Override
+                        public void onSuccess(final NetworkResponse networkResponse) {
+                            mRequest.addMarker("network-http-complete");
+
+                            // If the server returned 304 AND we delivered a response already,
+                            // we're done -- don't deliver a second identical response.
+                            if (networkResponse.notModified && mRequest.hasHadResponseDelivered()) {
+                                mRequest.finish("not-modified");
+                                mRequest.notifyListenerResponseNotUsable();
+                                return;
+                            }
+
+                            // Parse the response here on the worker thread.
+                            mBlockingExecutor.execute(
+                                    new NetworkParseTask<>(mRequest, networkResponse));
+                        }
+
+                        @Override
+                        public void onError(final VolleyError volleyError) {
+                            volleyError.setNetworkTimeMs(
+                                    SystemClock.elapsedRealtime() - startTimeMs);
+                            mBlockingExecutor.execute(new ParseErrorTask<>(mRequest, volleyError));
+                        }
+                    });
+        }
+    }
+
+    /**
+     * Runnable that parses a network response.
+     */
+    private class NetworkParseTask<T> extends RequestTask<T> {
+        NetworkResponse networkResponse;
+
+        NetworkParseTask(Request<T> request, NetworkResponse networkResponse) {
+            super(request);
+            this.networkResponse = networkResponse;
+        }
+
+        @Override
+        public void run() {
+            final Response<?> response = mRequest.parseNetworkResponse(networkResponse);
+            mRequest.addMarker("network-parse-complete");
+
+            // Write to cache if applicable.
+            // TODO: Only update cache metadata instead of entire
+            // record for 304s.
+            if (mRequest.shouldCache() && response.cacheEntry != null) {
+                if (mAsyncCache != null) {
+                    mNonBlockingExecutor.execute(new CachePutTask<>(mRequest, response));
+                } else {
+                    mBlockingExecutor.execute(new CachePutTask<>(mRequest, response));
+                }
+            } else {
+                finishRequest(mRequest, response, /* cached= */ false);
+            }
+        }
+    }
+
+    private class CachePutTask<T> extends RequestTask<T> {
+        Response<?> response;
+
+        CachePutTask(Request<T> request, Response<?> response) {
+            super(request);
+            this.response = response;
+        }
+
+        @Override
+        public void run() {
+            if (mAsyncCache != null) {
+                mAsyncCache.put(
+                        mRequest.getCacheKey(),
+                        response.cacheEntry,
+                        new AsyncCache.OnWriteCompleteCallback() {
+                            @Override
+                            public void onWriteComplete() {
+                                finishRequest(mRequest, response, /* cached= */ true);
+                            }
+                        });
+            } else {
+                getCache().put(mRequest.getCacheKey(), response.cacheEntry);
+                finishRequest(mRequest, response, /* cached= */ true);
+            }
+        }
+    }
+
+    /**
+     * Posts response and notifies listener
+     */
+    private void finishRequest(Request<?> mRequest, Response<?> response, boolean cached) {
+        if (cached) {
+            mRequest.addMarker("network-cache-written");
+        }
+        // Post the response back.
+        mRequest.markDelivered();
+        getResponseDelivery().postResponse(mRequest, response);
+        mRequest.notifyListenerResponseReceived(response);
+    }
+
+    /**
+     * Factory to create/provide the executors which Volley will use.
+     *
+     * <p>This class may be used by advanced applications to provide custom executors according to
+     * their needs.
+     *
+     * <p>For applications which rely on setting request priority via {@link Request#getPriority}, a
+     * task queue is provided which will prioritize requests of higher priority should the thread
+     * pool itself be exhausted. If a shared pool is provided which does not make use of the given
+     * queue, then lower-priority requests may have tasks executed before higher-priority requests
+     * when enough tasks are in flight to fully saturate the shared pool.
+     */
+    public abstract static class ExecutorFactory {
+        public abstract ExecutorService createNonBlockingExecutor(
+                BlockingQueue<Runnable> taskQueue);
+
+        public abstract ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue);
+
+        public abstract ScheduledExecutorService createNonBlockingScheduledExecutor();
+    }
+
+    /**
+     * Provides a BlockingQueue to be used to create executors.
+     */
+    private static PriorityBlockingQueue<Runnable> getBlockingQueue() {
+        return new PriorityBlockingQueue<>(
+                /* initialCapacity= */ 11,
+                new Comparator<Runnable>() {
+                    @Override
+                    public int compare(Runnable r1, Runnable r2) {
+                        // Vanilla runnables are prioritized first, then RequestTasks are ordered
+                        // by the underlying Request.
+                        if (r1 instanceof RequestTask) {
+                            if (r2 instanceof RequestTask) {
+                                return ((RequestTask<?>) r1).compareTo(((RequestTask<?>) r2));
+                            }
+                            return 1;
+                        }
+                        return r2 instanceof RequestTask ? -1 : 0;
+                    }
+                });
+    }
+
+    /**
+     * Builder is used to build an instance of {@link .AsyncRequestQueue} from values configured by
+     * the setters.
+     */
+    public static class Builder {
+        @Nullable
+        private AsyncCache mAsyncCache = null;
+        private final AsyncNetwork mNetwork;
+        @Nullable
+        private Cache mCache = null;
+        @Nullable
+        private ExecutorFactory mExecutorFactory = null;
+        @Nullable
+        private ResponseDelivery mResponseDelivery = null;
+
+        public Builder(AsyncNetwork asyncNetwork) {
+            if (asyncNetwork == null) {
+                throw new IllegalArgumentException("Network cannot be null");
+            }
+            mNetwork = asyncNetwork;
+        }
+
+        /**
+         * Sets the executor factory to be used by the AsyncRequestQueue. If this is not called,
+         * Volley will create suitable private thread pools.
+         */
+        public Builder setExecutorFactory(ExecutorFactory executorFactory) {
+            mExecutorFactory = executorFactory;
+            return this;
+        }
+
+        /**
+         * Sets the response deliver to be used by the AsyncRequestQueue. If this is not called, we
+         * will default to creating a new {@link ExecutorDelivery} with the application's main
+         * thread.
+         */
+        public Builder setResponseDelivery(ResponseDelivery responseDelivery) {
+            mResponseDelivery = responseDelivery;
+            return this;
+        }
+
+        /**
+         * Sets the AsyncCache to be used by the AsyncRequestQueue.
+         */
+        public Builder setAsyncCache(AsyncCache asyncCache) {
+            mAsyncCache = asyncCache;
+            return this;
+        }
+
+        /**
+         * Sets the Cache to be used by the AsyncRequestQueue.
+         */
+        public Builder setCache(Cache cache) {
+            mCache = cache;
+            return this;
+        }
+
+        /**
+         * Provides a default ExecutorFactory to use, if one is never set.
+         */
+        private ExecutorFactory getDefaultExecutorFactory() {
+            return new ExecutorFactory() {
+                @Override
+                public ExecutorService createNonBlockingExecutor(
+                        BlockingQueue<Runnable> taskQueue) {
+                    return getNewThreadPoolExecutor(
+                            /* maximumPoolSize= */ 1,
+                            /* threadNameSuffix= */ "Non-BlockingExecutor",
+                            taskQueue);
+                }
+
+                @Override
+                public ExecutorService createBlockingExecutor(BlockingQueue<Runnable> taskQueue) {
+                    return getNewThreadPoolExecutor(
+                            /* maximumPoolSize= */ DEFAULT_BLOCKING_THREAD_POOL_SIZE,
+                            /* threadNameSuffix= */ "BlockingExecutor",
+                            taskQueue);
+                }
+
+                @Override
+                public ScheduledExecutorService createNonBlockingScheduledExecutor() {
+                    return new ScheduledThreadPoolExecutor(
+                            /* corePoolSize= */ 0, getThreadFactory("ScheduledExecutor"));
+                }
+
+                private ThreadPoolExecutor getNewThreadPoolExecutor(
+                        int maximumPoolSize,
+                        final String threadNameSuffix,
+                        BlockingQueue<Runnable> taskQueue) {
+                    return new ThreadPoolExecutor(
+                            /* corePoolSize= */ 0,
+                            /* maximumPoolSize= */ maximumPoolSize,
+                            /* keepAliveTime= */ 60,
+                            /* unit= */ TimeUnit.SECONDS,
+                            taskQueue,
+                            getThreadFactory(threadNameSuffix));
+                }
+
+                private ThreadFactory getThreadFactory(final String threadNameSuffix) {
+                    return new ThreadFactory() {
+                        @Override
+                        public Thread newThread(@NonNull Runnable runnable) {
+                            Thread t = Executors.defaultThreadFactory().newThread(runnable);
+                            t.setName("Volley-" + threadNameSuffix);
+                            return t;
+                        }
+                    };
+                }
+            };
+        }
+
+        public AsyncRequestQueue build() {
+            // If neither cache is set by the caller, throw an illegal argument exception.
+            if (mCache == null && mAsyncCache == null) {
+                throw new IllegalArgumentException("You must set one of the cache objects");
+            }
+            if (mCache == null) {
+                // if no cache is provided, we will provide one that throws
+                // UnsupportedOperationExceptions to pass into the parent class.
+                mCache = new ThrowingCache();
+            }
+            if (mResponseDelivery == null) {
+                mResponseDelivery = new ExecutorDelivery(new Handler(Looper.getMainLooper()));
+            }
+            if (mExecutorFactory == null) {
+                mExecutorFactory = getDefaultExecutorFactory();
+            }
+            return new AsyncRequestQueue(
+                    mCache, mNetwork, mAsyncCache, mResponseDelivery, mExecutorFactory);
+        }
+    }
+
+    /**
+     * A cache that throws an error if a method is called.
+     */
+    private static class ThrowingCache implements Cache {
+        @Override
+        public Entry get(String key) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void put(String key, Entry entry) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void initialize() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void invalidate(String key, boolean fullExpire) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void remove(String key) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void clear() {
+            throw new UnsupportedOperationException();
+        }
+    }
+}

+ 56 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/AuthFailureError.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import android.content.Intent;
+
+/** Error indicating that there was an authentication failure when performing a Request. */
+@SuppressWarnings("serial")
+public class AuthFailureError extends VolleyError {
+    /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
+    private Intent mResolutionIntent;
+
+    public AuthFailureError() {}
+
+    public AuthFailureError(Intent intent) {
+        mResolutionIntent = intent;
+    }
+
+    public AuthFailureError(NetworkResponse response) {
+        super(response);
+    }
+
+    public AuthFailureError(String message) {
+        super(message);
+    }
+
+    public AuthFailureError(String message, Exception reason) {
+        super(message, reason);
+    }
+
+    public Intent getResolutionIntent() {
+        return mResolutionIntent;
+    }
+
+    @Override
+    public String getMessage() {
+        if (mResolutionIntent != null) {
+            return "User needs to (re)enter credentials.";
+        }
+        return super.getMessage();
+    }
+}

+ 114 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/Cache.java

@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import androidx.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** An interface for a cache keyed by a String with a byte array as data. */
+public interface Cache {
+    /**
+     * Retrieves an entry from the cache.
+     *
+     * @param key Cache key
+     * @return An {@link Entry} or null in the event of a cache miss
+     */
+    @Nullable
+    Entry get(String key);
+
+    /**
+     * Adds or replaces an entry to the cache.
+     *
+     * @param key Cache key
+     * @param entry Data to store and metadata for cache coherency, TTL, etc.
+     */
+    void put(String key, Entry entry);
+
+    /**
+     * Performs any potentially long-running actions needed to initialize the cache; will be called
+     * from a worker thread.
+     */
+    void initialize();
+
+    /**
+     * Invalidates an entry in the cache.
+     *
+     * @param key Cache key
+     * @param fullExpire True to fully expire the entry, false to soft expire
+     */
+    void invalidate(String key, boolean fullExpire);
+
+    /**
+     * Removes an entry from the cache.
+     *
+     * @param key Cache key
+     */
+    void remove(String key);
+
+    /** Empties the cache. */
+    void clear();
+
+    /** Data and metadata for an entry returned by the cache. */
+    class Entry {
+        /** The data returned from cache. */
+        public byte[] data;
+
+        /** ETag for cache coherency. */
+        public String etag;
+
+        /** Date of this response as reported by the server. */
+        public long serverDate;
+
+        /** The last modified date for the requested object. */
+        public long lastModified;
+
+        /** TTL for this record. */
+        public long ttl;
+
+        /** Soft TTL for this record. */
+        public long softTtl;
+
+        /**
+         * Response headers as received from server; must be non-null. Should not be mutated
+         * directly.
+         *
+         * <p>Note that if the server returns two headers with the same (case-insensitive) name,
+         * this map will only contain the one of them. {@link #allResponseHeaders} may contain all
+         * headers if the {@link .Cache} implementation supports it.
+         */
+        public Map<String, String> responseHeaders = Collections.emptyMap();
+
+        /**
+         * All response headers. May be null depending on the {@link .Cache} implementation. Should
+         * not be mutated directly.
+         */
+        public List<Header> allResponseHeaders;
+
+        /** True if the entry is expired. */
+        public boolean isExpired() {
+            return this.ttl < System.currentTimeMillis();
+        }
+
+        /** True if a refresh is needed from the original data source. */
+        public boolean refreshNeeded() {
+            return this.softTtl < System.currentTimeMillis();
+        }
+    }
+}

+ 220 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/CacheDispatcher.java

@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import android.os.Process;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Provides a thread for performing cache triage on a queue of requests.
+ *
+ * <p>Requests added to the specified cache queue are resolved from cache. Any deliverable response
+ * is posted back to the caller via a {@link ResponseDelivery}. Cache misses and responses that
+ * require refresh are enqueued on the specified network queue for processing by a {@link
+ * NetworkDispatcher}.
+ */
+public class CacheDispatcher extends Thread {
+
+    private static final boolean DEBUG = VolleyLog.DEBUG;
+
+    /**
+     * The queue of requests coming in for triage.
+     */
+    private final BlockingQueue<Request<?>> mCacheQueue;
+
+    /**
+     * The queue of requests going out to the network.
+     */
+    private final BlockingQueue<Request<?>> mNetworkQueue;
+
+    /**
+     * The cache to read from.
+     */
+    private final Cache mCache;
+
+    /**
+     * For posting responses.
+     */
+    private final ResponseDelivery mDelivery;
+
+    /**
+     * Used for telling us to die.
+     */
+    private volatile boolean mQuit = false;
+
+    /**
+     * Manage list of waiting requests and de-duplicate requests with same cache key.
+     */
+    private final WaitingRequestManager mWaitingRequestManager;
+
+    /**
+     * Creates a new cache triage dispatcher thread. You must call {@link #start()} in order to
+     * begin processing.
+     *
+     * @param cacheQueue   Queue of incoming requests for triage
+     * @param networkQueue Queue to post requests that require network to
+     * @param cache        Cache interface to use for resolution
+     * @param delivery     Delivery interface to use for posting responses
+     */
+    public CacheDispatcher(
+            BlockingQueue<Request<?>> cacheQueue,
+            BlockingQueue<Request<?>> networkQueue,
+            Cache cache,
+            ResponseDelivery delivery) {
+        mCacheQueue = cacheQueue;
+        mNetworkQueue = networkQueue;
+        mCache = cache;
+        mDelivery = delivery;
+        mWaitingRequestManager = new WaitingRequestManager(this, networkQueue, delivery);
+    }
+
+    /**
+     * Forces this dispatcher to quit immediately. If any requests are still in the queue, they are
+     * not guaranteed to be processed.
+     */
+    public void quit() {
+        mQuit = true;
+        interrupt();
+    }
+
+    @Override
+    public void run() {
+        if (DEBUG) VolleyLog.v("start new dispatcher");
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+        // Make a blocking call to initialize the cache.
+        mCache.initialize();
+
+        while (true) {
+            try {
+                processRequest();
+            } catch (InterruptedException e) {
+                // We may have been interrupted because it was time to quit.
+                if (mQuit) {
+                    Thread.currentThread().interrupt();
+                    return;
+                }
+                VolleyLog.e(
+                        "Ignoring spurious interrupt of CacheDispatcher thread; "
+                                + "use quit() to terminate it");
+            }
+        }
+    }
+
+    // Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
+    // This is needed to avoid keeping previous request references alive for an indeterminate amount
+    // of time. Update consumer-proguard-rules.pro when modifying this. See also
+    // https://github.com/google/volley/issues/114
+    private void processRequest() throws InterruptedException {
+        // Get a request from the cache triage queue, blocking until
+        // at least one is available.
+        final Request<?> request = mCacheQueue.take();
+        processRequest(request);
+    }
+
+    @VisibleForTesting
+    void processRequest(final Request<?> request) throws InterruptedException {
+        request.addMarker("cache-queue-take");
+        request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED);
+
+        try {
+            // If the request has been canceled, don't bother dispatching it.
+            if (request.isCanceled()) {
+                request.finish("cache-discard-canceled");
+                return;
+            }
+
+            // Attempt to retrieve this item from cache.
+            Cache.Entry entry = mCache.get(request.getCacheKey());
+            if (entry == null) {
+                request.addMarker("cache-miss");
+                // Cache miss; send off to the network dispatcher.
+                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                    mNetworkQueue.put(request);
+                }
+                return;
+            }
+
+            // If it is completely expired, just send it to the network.
+            if (entry.isExpired()) {
+                request.addMarker("cache-hit-expired");
+                request.setCacheEntry(entry);
+                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                    mNetworkQueue.put(request);
+                }
+                return;
+            }
+
+            // We have a cache hit; parse its data for delivery back to the request.
+            request.addMarker("cache-hit");
+            Response<?> response =
+                    request.parseNetworkResponse(
+                            new NetworkResponse(entry.data, entry.responseHeaders));
+            request.addMarker("cache-hit-parsed");
+
+            if (!response.isSuccess()) {
+                request.addMarker("cache-parsing-failed");
+                mCache.invalidate(request.getCacheKey(), true);
+                request.setCacheEntry(null);
+                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                    mNetworkQueue.put(request);
+                }
+                return;
+            }
+            if (!entry.refreshNeeded()) {
+                // Completely unexpired cache hit. Just deliver the response.
+                mDelivery.postResponse(request, response);
+            } else {
+                // Soft-expired cache hit. We can deliver the cached response,
+                // but we need to also send the request to the network for
+                // refreshing.
+                request.addMarker("cache-hit-refresh-needed");
+                request.setCacheEntry(entry);
+                // Mark the response as intermediate.
+                response.intermediate = true;
+
+                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
+                    // Post the intermediate response back to the user and have
+                    // the delivery then forward the request along to the network.
+                    mDelivery.postResponse(
+                            request,
+                            response,
+                            new Runnable() {
+                                @Override
+                                public void run() {
+                                    try {
+                                        mNetworkQueue.put(request);
+                                    } catch (InterruptedException e) {
+                                        // Restore the interrupted status
+                                        Thread.currentThread().interrupt();
+                                    }
+                                }
+                            });
+                } else {
+                    // request has been added to list of waiting requests
+                    // to receive the network response from the first request once it returns.
+                    mDelivery.postResponse(request, response);
+                }
+            }
+        } finally {
+            request.sendEvent(RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
+        }
+    }
+}

+ 34 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/ClientError.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+/**
+ * Indicates that the server responded with an error response indicating that the client has erred.
+ *
+ * <p>For backwards compatibility, extends ServerError which used to be thrown for all server
+ * errors, including 4xx error codes indicating a client error.
+ */
+@SuppressWarnings("serial")
+public class ClientError extends ServerError {
+    public ClientError(NetworkResponse networkResponse) {
+        super(networkResponse);
+    }
+
+    public ClientError() {
+        super();
+    }
+}

+ 121 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/DefaultRetryPolicy.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+/**
+ * Default retry policy for requests.
+ */
+public class DefaultRetryPolicy implements RetryPolicy {
+    /**
+     * The current timeout in milliseconds.
+     */
+    private int mCurrentTimeoutMs;
+
+    /**
+     * The current retry count.
+     */
+    private int mCurrentRetryCount;
+
+    /**
+     * The maximum number of attempts.
+     */
+    private final int mMaxNumRetries;
+
+    /**
+     * The backoff multiplier for the policy.
+     */
+    private final float mBackoffMultiplier;
+
+    /**
+     * The default socket timeout in milliseconds
+     */
+    public static final int DEFAULT_TIMEOUT_MS = 2500;
+
+    /**
+     * The default number of retries
+     */
+    public static final int DEFAULT_MAX_RETRIES = 1;
+
+    /**
+     * The default backoff multiplier
+     */
+    public static final float DEFAULT_BACKOFF_MULT = 1f;
+
+    /**
+     * Constructs a new retry policy using the default timeouts.
+     */
+    public DefaultRetryPolicy() {
+        this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
+    }
+
+    /**
+     * Constructs a new retry policy.
+     *
+     * @param initialTimeoutMs  The initial timeout for the policy.
+     * @param maxNumRetries     The maximum number of retries.
+     * @param backoffMultiplier Backoff multiplier for the policy.
+     */
+    public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
+        mCurrentTimeoutMs = initialTimeoutMs;
+        mMaxNumRetries = maxNumRetries;
+        mBackoffMultiplier = backoffMultiplier;
+    }
+
+    /**
+     * Returns the current timeout.
+     */
+    @Override
+    public int getCurrentTimeout() {
+        return mCurrentTimeoutMs;
+    }
+
+    /**
+     * Returns the current retry count.
+     */
+    @Override
+    public int getCurrentRetryCount() {
+        return mCurrentRetryCount;
+    }
+
+    /**
+     * Returns the backoff multiplier for the policy.
+     */
+    public float getBackoffMultiplier() {
+        return mBackoffMultiplier;
+    }
+
+    /**
+     * Prepares for the next retry by applying a backoff to the timeout.
+     *
+     * @param error The error code of the last attempt.
+     */
+    @Override
+    public void retry(VolleyError error) throws VolleyError {
+        mCurrentRetryCount++;
+        mCurrentTimeoutMs += (int) (mCurrentTimeoutMs * mBackoffMultiplier);
+        if (!hasAttemptRemaining()) {
+            throw error;
+        }
+    }
+
+    /**
+     * Returns true if this policy has attempts remaining, false otherwise.
+     */
+    protected boolean hasAttemptRemaining() {
+        return mCurrentRetryCount <= mMaxNumRetries;
+    }
+}

+ 122 - 0
library_volleyx/src/main/java/cn/yyxx/support/volley/source/ExecutorDelivery.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cn.yyxx.support.volley.source;
+
+import android.os.Handler;
+
+import java.util.concurrent.Executor;
+
+/** Delivers responses and errors. */
+public class ExecutorDelivery implements ResponseDelivery {
+    /** Used for posting responses, typically to the main thread. */
+    private final Executor mResponsePoster;
+
+    /**
+     * Creates a new response delivery interface.
+     *
+     * @param handler {@link Handler} to post responses on
+     */
+    public ExecutorDelivery(final Handler handler) {
+        // Make an Executor that just wraps the handler.
+        mResponsePoster =
+                new Executor() {
+                    @Override
+                    public void execute(Runnable command) {
+                        handler.post(command);
+                    }
+                };
+    }
+
+    /**
+     * Creates a new response delivery interface, mockable version for testing.
+     *
+     * @param executor For running delivery tasks
+     */
+    public ExecutorDelivery(Executor executor) {
+        mResponsePoster = executor;
+    }
+
+    @Override
+    public void postResponse(Request<?> request, Response<?> response) {
+        postResponse(request, response, null);
+    }
+
+    @Override
+    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
+        request.markDelivered();
+        request.addMarker("post-response");
+        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
+    }
+
+    @Override
+    public void postError(Request<?> request, VolleyError error) {
+        request.addMarker("post-error");
+        Response<?> response = Response.error(error);
+        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
+    }
+
+    /** A Runnable used for delivering network responses to a listener on the main thread. */
+    @SuppressWarnings("rawtypes")
+    private static class ResponseDeliveryRunnable implements Runnable {
+        private final Request mRequest;
+        private final Response mResponse;
+        private final Runnable mRunnable;
+
+        public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
+            mRequest = request;
+            mResponse = response;
+            mRunnable = runnable;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public void run() {
+            // NOTE: If cancel() is called off the thread that we're currently running in (by
+            // default, the main thread), we cannot guarantee that deliverResponse()/deliverError()
+            // won't be called, since it may be canceled after we check isCanceled() but before we
+            // deliver the response. Apps concerned about this guarantee must either call cancel()
+            // from the same thread or implement their own guarantee about not invoking their
+            // listener after cancel() has been called.
+
+            // If this request has canceled, finish it and don't deliver.
+            if (mRequest.isCanceled()) {
+                mRequest.finish("canceled-at-delivery");
+                return;
+            }
+
+            // Deliver a normal response or error, depending.
+            if (mResponse.isSuccess()) {
+                mRequest.deliverResponse(mResponse.result);
+            } else {
+                mRequest.deliverError(mResponse.error);
+            }
+
+            // If this is an intermediate response, add a marker, otherwise we're done
+            // and the request can be finished.
+            if (mResponse.intermediate) {
+                mRequest.addMarker("intermediate-response");
+            } else {
+                mRequest.finish("done");
+            }
+
+            // If we have been provided a post-delivery runnable, run it.
+            if (mRunnable != null) {
+                mRunnable.run();
+            }
+        }
+    }
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików