Переглянути джерело

v1.0.0开发:移植Volley框架

#Suyghur 3 роки тому
батько
коміт
97b82ca65e
80 змінених файлів з 8025 додано та 237 видалено
  1. 1 2
      build.gradle
  2. 1 0
      demo/build.gradle
  3. 1 2
      demo/src/main/AndroidManifest.xml
  4. 0 30
      demo/src/main/res/drawable-v24/ic_launcher_foreground.xml
  5. 0 170
      demo/src/main/res/drawable/ic_launcher_background.xml
  6. 0 5
      demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  7. 0 5
      demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  8. BIN
      demo/src/main/res/mipmap-hdpi/ic_launcher.png
  9. BIN
      demo/src/main/res/mipmap-hdpi/ic_launcher_round.png
  10. BIN
      demo/src/main/res/mipmap-mdpi/ic_launcher.png
  11. BIN
      demo/src/main/res/mipmap-mdpi/ic_launcher_round.png
  12. BIN
      demo/src/main/res/mipmap-xhdpi/ic_launcher.png
  13. BIN
      demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  14. BIN
      demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
  15. BIN
      demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  16. BIN
      demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  17. BIN
      demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  18. 0 10
      demo/src/main/res/values-night/themes.xml
  19. 1 1
      demo/src/main/res/values/strings.xml
  20. 5 0
      demo/src/main/res/values/styles.xml
  21. 0 10
      demo/src/main/res/values/themes.xml
  22. 8 1
      library_support/build.gradle
  23. 17 0
      library_support/buildJar.gradle
  24. 0 0
      library_support/consumer-rules.pro
  25. BIN
      library_support/libs/android-support-v4.jar
  26. 100 1
      library_support/proguard-rules.pro
  27. 47 0
      library_support/src/main/java/android/support/annotation/GuardedBy.java
  28. 98 0
      library_support/src/main/java/cn/yyxx/support/AppUtils.java
  29. 35 0
      library_support/src/main/java/cn/yyxx/support/BeanUtils.java
  30. 32 0
      library_support/src/main/java/cn/yyxx/support/HostModelUtils.java
  31. 18 0
      library_support/src/main/java/cn/yyxx/support/ResUtils.java
  32. 97 0
      library_support/src/main/java/cn/yyxx/support/hawkeye/LogUtils.java
  33. 64 0
      library_support/src/main/java/cn/yyxx/support/volley/VolleySingleton.java
  34. 61 0
      library_support/src/main/java/cn/yyxx/support/volley/source/AuthFailureError.java
  35. 133 0
      library_support/src/main/java/cn/yyxx/support/volley/source/Cache.java
  36. 318 0
      library_support/src/main/java/cn/yyxx/support/volley/source/CacheDispatcher.java
  37. 34 0
      library_support/src/main/java/cn/yyxx/support/volley/source/ClientError.java
  38. 121 0
      library_support/src/main/java/cn/yyxx/support/volley/source/DefaultRetryPolicy.java
  39. 127 0
      library_support/src/main/java/cn/yyxx/support/volley/source/ExecutorDelivery.java
  40. 61 0
      library_support/src/main/java/cn/yyxx/support/volley/source/Header.java
  41. 31 0
      library_support/src/main/java/cn/yyxx/support/volley/source/Network.java
  42. 188 0
      library_support/src/main/java/cn/yyxx/support/volley/source/NetworkDispatcher.java
  43. 35 0
      library_support/src/main/java/cn/yyxx/support/volley/source/NetworkError.java
  44. 204 0
      library_support/src/main/java/cn/yyxx/support/volley/source/NetworkResponse.java
  45. 31 0
      library_support/src/main/java/cn/yyxx/support/volley/source/NoConnectionError.java
  46. 34 0
      library_support/src/main/java/cn/yyxx/support/volley/source/ParseError.java
  47. 768 0
      library_support/src/main/java/cn/yyxx/support/volley/source/Request.java
  48. 383 0
      library_support/src/main/java/cn/yyxx/support/volley/source/RequestQueue.java
  49. 100 0
      library_support/src/main/java/cn/yyxx/support/volley/source/Response.java
  50. 35 0
      library_support/src/main/java/cn/yyxx/support/volley/source/ResponseDelivery.java
  51. 60 0
      library_support/src/main/java/cn/yyxx/support/volley/source/RetryPolicy.java
  52. 31 0
      library_support/src/main/java/cn/yyxx/support/volley/source/ServerError.java
  53. 24 0
      library_support/src/main/java/cn/yyxx/support/volley/source/TimeoutError.java
  54. 58 0
      library_support/src/main/java/cn/yyxx/support/volley/source/VolleyError.java
  55. 191 0
      library_support/src/main/java/cn/yyxx/support/volley/source/VolleyLog.java
  56. 80 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/AdaptedHttpStack.java
  57. 128 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/AndroidAuthenticator.java
  58. 36 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/Authenticator.java
  59. 94 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/BaseHttpStack.java
  60. 380 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/BasicNetwork.java
  61. 140 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ByteArrayPool.java
  62. 70 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ClearCacheRequest.java
  63. 683 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/DiskBasedCache.java
  64. 203 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpClientStack.java
  65. 215 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpHeaderParser.java
  66. 89 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpResponse.java
  67. 48 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HttpStack.java
  68. 307 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/HurlStack.java
  69. 572 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageLoader.java
  70. 298 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/ImageRequest.java
  71. 83 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonArrayRequest.java
  72. 96 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonObjectRequest.java
  73. 129 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/JsonRequest.java
  74. 290 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/NetworkImageView.java
  75. 49 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/NoCache.java
  76. 94 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/PoolingByteArrayOutputStream.java
  77. 162 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/RequestFuture.java
  78. 106 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/StringRequest.java
  79. 14 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/Threads.java
  80. 106 0
      library_support/src/main/java/cn/yyxx/support/volley/source/toolbox/Volley.java

+ 1 - 2
build.gradle

@@ -1,9 +1,8 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 buildscript {
 
-    ext.COMM_LIB_DEV_ENABLE = true
     // 混淆开关
-    ext.MINIFY_ENABLE = false
+    ext.MINIFY_ENABLE = true
     // ndk版本
     ext.NDK_VERSION = '21.3.6528147'
     // kotlin版本

+ 1 - 0
demo/build.gradle

@@ -67,4 +67,5 @@ android {
 }
 
 dependencies {
+    implementation project(':library_support')
 }

+ 1 - 2
demo/src/main/AndroidManifest.xml

@@ -6,8 +6,7 @@
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
-        android:theme="@style/Theme.YYXXSupportSdk" />
+        android:theme="@style/AppTheme" />
 
 </manifest>

+ 0 - 30
demo/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -1,30 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportWidth="108"
-    android:viewportHeight="108">
-    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
-        <aapt:attr name="android:fillColor">
-            <gradient
-                android:endX="85.84757"
-                android:endY="92.4963"
-                android:startX="42.9492"
-                android:startY="49.59793"
-                android:type="linear">
-                <item
-                    android:color="#44000000"
-                    android:offset="0.0" />
-                <item
-                    android:color="#00000000"
-                    android:offset="1.0" />
-            </gradient>
-        </aapt:attr>
-    </path>
-    <path
-        android:fillColor="#FFFFFF"
-        android:fillType="nonZero"
-        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
-        android:strokeWidth="1"
-        android:strokeColor="#00000000" />
-</vector>

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

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

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

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

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

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

BIN
demo/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
demo/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
demo/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
demo/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
demo/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
demo/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 0 - 10
demo/src/main/res/values-night/themes.xml

@@ -1,10 +0,0 @@
-<resources xmlns:tools="http://schemas.android.com/tools">
-    <!-- Base application theme. -->
-    <style name="Theme.YYXXSupportSdk" parent="Theme.AppCompat.Light.DarkActionBar">
-        <!-- Primary brand color. -->
-        <item name="colorPrimary">@color/purple_200</item>
-        <item name="colorPrimaryDark">@color/purple_700</item>
-        <item name="colorAccent">@color/teal_200</item>
-        <!-- Customize your theme here. -->
-    </style>
-</resources>

+ 1 - 1
demo/src/main/res/values/strings.xml

@@ -1,3 +1,3 @@
 <resources>
-    <string name="app_name">Demo</string>
+    <string name="app_name">YYXXSupportSdk</string>
 </resources>

+ 5 - 0
demo/src/main/res/values/styles.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="android:Theme.Light.NoTitleBar.Fullscreen" />
+</resources>

+ 0 - 10
demo/src/main/res/values/themes.xml

@@ -1,10 +0,0 @@
-<resources xmlns:tools="http://schemas.android.com/tools">
-    <!-- Base application theme. -->
-    <style name="Theme.YYXXSupportSdk" parent="Theme.AppCompat.Light.DarkActionBar">
-        <!-- Primary brand color. -->
-        <item name="colorPrimary">@color/purple_500</item>
-        <item name="colorPrimaryDark">@color/purple_700</item>
-        <item name="colorAccent">@color/teal_200</item>
-        <!-- Customize your theme here. -->
-    </style>
-</resources>

+ 8 - 1
library_support/build.gradle

@@ -35,7 +35,14 @@ android {
     dexOptions {
         preDexLibraries = false
     }
+
+    //api23以上使用 httpClient
+    useLibrary 'org.apache.http.legacy'
 }
 
 dependencies {
-}
+    compileOnly files('libs/android-support-v4.jar')
+//    implementation 'com.android.support:support-annotations:28.0.0'
+}
+
+apply from: 'buildJar.gradle'

+ 17 - 0
library_support/buildJar.gradle

@@ -0,0 +1,17 @@
+def SDK_BASE_NAME = 'yyxx_support'
+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)

+ 0 - 0
library_support/consumer-rules.pro


BIN
library_support/libs/android-support-v4.jar


+ 100 - 1
library_support/proguard-rules.pro

@@ -18,4 +18,103 @@
 
 # If you keep the line number information, uncomment this to
 # hide the original source file name.
-#-renamesourcefileattribute SourceFile
+#-renamesourcefileattribute SourceFile
+
+# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改
+-optimizationpasses 7
+# 混合时不使用大小写混合,混合后的类名为小写
+-dontusemixedcaseclassnames
+# 指定不去忽略非公共库的类
+-dontskipnonpubliclibraryclasses
+-dontoptimize
+# 这句话能够使我们的项目混淆后产生映射文件
+# 包含有类名->混淆后类名的映射关系
+-verbose
+-ignorewarnings
+# 指定不去忽略非公共库的类成员
+-dontskipnonpubliclibraryclassmembers
+# 指定混淆是采用的算法,后面的参数是一个过滤器
+# 这个过滤器是谷歌推荐的算法,一般不做更改
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+# 保留java与js交互注解
+-keepattributes *Annotation*
+-keepattributes *JavascriptInterface*
+
+# 保留内部类
+-keepattributes Exceptions,InnerClasses
+
+# 保留泛型
+-keepattributes Signature
+
+-keepnames class * implements java.io.Serializable
+-keepclassmembers class * implements java.io.Serializable {
+   static final long serialVersionUID;
+   private static final java.io.ObjectStreamField[] serialPersistentFields;
+   !static !transient <fields>;
+   private void writeObject(java.io.ObjectOutputStream);
+   private void readObject(java.io.ObjectInputStream);
+   java.lang.Object writeReplace();
+   java.lang.Object readResolve();
+}
+
+-keepclassmembers class **.R$* {
+    public static <fields>;
+}
+-keep class **.R$* {
+ *;
+}
+
+-keep public class * extends android.app.Activity{
+	public <fields>;
+	public <methods>;
+}
+-keep public class * extends android.app.Application{
+	public <fields>;
+	public <methods>;
+}
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+
+
+-keepclassmembers enum * {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+-keepclasseswithmembers class * {
+	public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+	public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclasseswithmembernames class *{
+	native <methods>;
+}
+
+-keep class * implements android.os.Parcelable {
+  public static final android.os.Parcelable$Creator *;
+}
+
+-keepclasseswithmembers class * {
+    ... *JNI*(...);
+}
+
+-keepclasseswithmembernames class * {
+	... *JRI*(...);
+}
+
+-keep class **JNI* {*;}
+
+-keep class android.support.annotation.GuardedBy{*;}
+-keep class cn.yyxx.support.AppUtils{ public <fields>;public <methods>;}
+-keep class cn.yyxx.support.BeanUtils{ public <fields>;public <methods>;}
+-keep class cn.yyxx.support.HostModelUtils{ public <fields>;public <methods>;}
+-keep class cn.yyxx.support.ResUtils{ public <fields>;public <methods>;}
+-keep class cn.yyxx.support.hawkeye.**{ public <fields>; public <methods>;}
+-keep class cn.yyxx.support.volley.**{  public <fields>; public <methods>;}

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

@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 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 android.support.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the annotated method or field can only be accessed when holding the referenced lock.
+ * <p>
+ * Example:
+ * <pre>
+ * final Object objectLock = new Object();
+ *
+ * {@literal @}GuardedBy("objectLock")
+ * volatile Object object;
+ *
+ * Object getObject() {
+ *     synchronized (objectLock) {
+ *         if (object == null) {
+ *             object = new Object();
+ *         }
+ *     }
+ *     return object;
+ * }</pre>
+ */
+@Target({ ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.CLASS)
+public @interface GuardedBy {
+    String value();
+}

+ 98 - 0
library_support/src/main/java/cn/yyxx/support/AppUtils.java

@@ -0,0 +1,98 @@
+package cn.yyxx.support;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+
+/**
+ * @author #Suyghur.
+ * Created on 4/22/21
+ */
+public class AppUtils {
+
+    private AppUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    /**
+     * 获取应用程序名称
+     */
+    public static String getAppName(Context context) {
+        if (context == null) {
+            return "";
+        }
+        try {
+            PackageManager packageManager = context.getPackageManager();
+            PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+            int labelRes = packageInfo.applicationInfo.labelRes;
+            return context.getResources().getString(labelRes);
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    /**
+     * 获取程序包名
+     */
+    public static String getPackageName(Context context) {
+        if (context == null) {
+            return null;
+        }
+        String packgename = "";
+        try {
+            packgename = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).packageName;
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return packgename;
+    }
+
+    /**
+     * 获得程序版本号
+     */
+    public static int getVersionCode(Context context) {
+        if (context == null) {
+            return 0;
+        }
+        try {
+            return 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";
+        }
+        try {
+            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return "1";
+    }
+
+    /**
+     * 判断是否已安装
+     */
+    public static boolean isPackageInstalled(Context context, String pkgName) {
+        try {
+            if (TextUtils.isEmpty(pkgName)) {
+                return false;
+            } else {
+                context.getPackageManager().getPackageInfo(pkgName.trim(), PackageManager.GET_GIDS);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+        return true;
+    }
+}

+ 35 - 0
library_support/src/main/java/cn/yyxx/support/BeanUtils.java

@@ -0,0 +1,35 @@
+package cn.yyxx.support;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+/**
+ * @author #Suyghur.
+ * Created on 2021/04/22
+ */
+public class BeanUtils {
+
+    public BeanUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    public static <T> T deepClone(T obj) {
+        try {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream oos = new ObjectOutputStream(bos);
+            oos.writeObject(obj);
+            //将流序列化成对象
+            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+            ObjectInputStream ois = new ObjectInputStream(bis);
+            return (T) ois.readObject();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

+ 32 - 0
library_support/src/main/java/cn/yyxx/support/HostModelUtils.java

@@ -0,0 +1,32 @@
+package cn.yyxx.support;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * @author #Suyghur.
+ * Created on 2021/04/22
+ */
+public class HostModelUtils {
+    private HostModelUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    //主机环境:1.dev;2.test;3.online
+    public static final int ENV_DEV = 1;
+    public static final int ENV_TEST = 2;
+    public static final int ENV_ONLINE = 3;
+
+    public static void setHostModel(Context context, int ipModel) {
+        SharedPreferences sp = context.getSharedPreferences("yyxx_host_model", Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sp.edit();
+        editor.putInt("host_mode", ipModel);
+        editor.commit();
+    }
+
+    public static int getHostModel(Context context) {
+        SharedPreferences sp = context.getSharedPreferences("yyxx_host_model", Context.MODE_PRIVATE);
+        //默认线上环境
+        return sp.getInt("host_model", 3);
+    }
+}

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

@@ -0,0 +1,18 @@
+package cn.yyxx.support;
+
+import android.content.Context;
+
+/**
+ * @author #Suyghur.
+ * Created on 2021/04/22
+ */
+public class ResUtils {
+
+    private ResUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    public static int getResId(Context context, String name, String type) {
+        return context.getResources().getIdentifier(name, type, context.getPackageName());
+    }
+}

+ 97 - 0
library_support/src/main/java/cn/yyxx/support/hawkeye/LogUtils.java

@@ -0,0 +1,97 @@
+package cn.yyxx.support.hawkeye;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.lang.reflect.Array;
+
+/**
+ * @author #Suyghur.
+ * Created on 4/22/21
+ */
+public class LogUtils {
+
+    public static boolean DEBUG = true;
+    public static Handler handler = null;
+
+    private static final String TAG = "yyxx_support";
+
+    public static void d(Object object) {
+        d(TAG, object);
+    }
+
+    public static void d(String tag, Object object) {
+        if (DEBUG) {
+            print(Log.DEBUG, tag, object);
+        }
+    }
+
+    public static void i(Object object) {
+        i(TAG, object);
+    }
+
+    public static void i(String tag, Object object) {
+        print(Log.INFO, tag, object);
+    }
+
+    public static void e(Object object) {
+        e(TAG, object);
+    }
+
+    public static void e(String tag, Object object) {
+        print(Log.ERROR, tag, object);
+    }
+
+    private static void print(int level, String tag, Object obj) {
+        String msg;
+        if (obj == null) {
+            msg = "null";
+        } else {
+            Class<?> clz = obj.getClass();
+            if (clz.isArray()) {
+                StringBuilder sb = new StringBuilder(clz.getSimpleName());
+                sb.append(" [ ");
+                int len = Array.getLength(obj);
+                for (int i = 0; i < len; i++) {
+                    if (i != 0 && i != len - 1) {
+                        sb.append(", ");
+                    }
+                    Object tmp = Array.get(obj, i);
+                    sb.append(tmp);
+                }
+                sb.append(" ] ");
+                msg = sb.toString();
+            } else {
+                msg = obj + "";
+            }
+        }
+
+        switch (level) {
+            case Log.DEBUG:
+                Log.d(tag, msg);
+                break;
+            case Log.INFO:
+                Log.i(tag, msg);
+                break;
+            case Log.ERROR:
+                Log.e(tag, msg);
+                break;
+        }
+    }
+
+    public static void logHandler(Handler handler, String msg) {
+        if (handler != null) {
+            Message message = Message.obtain();
+            message.what = 10001;
+            message.obj = msg;
+            handler.sendMessage(message);
+        }
+    }
+
+    public static void logHandler(String msg) {
+        if (handler != null) {
+            logHandler(handler, msg);
+        }
+    }
+}

+ 64 - 0
library_support/src/main/java/cn/yyxx/support/volley/VolleySingleton.java

@@ -0,0 +1,64 @@
+package cn.yyxx.support.volley;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.LruCache;
+
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.RequestQueue;
+import cn.yyxx.support.volley.source.toolbox.ImageLoader;
+import cn.yyxx.support.volley.source.toolbox.Volley;
+
+/**
+ * @author #Suyghur.
+ * Created on 2021/04/22
+ */
+public class VolleySingleton {
+
+    private volatile static VolleySingleton mInstance;
+    private RequestQueue requestQueue;
+    private final ImageLoader imageLoader;
+
+    private VolleySingleton(Context context) {
+        requestQueue = getRequestQueue(context);
+        imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
+            private final LruCache<String, Bitmap> cache = new LruCache<>(20);
+
+            @Override
+            public Bitmap getBitmap(String url) {
+                return cache.get(url);
+            }
+
+            @Override
+            public void putBitmap(String url, Bitmap bitmap) {
+                cache.put(url, bitmap);
+            }
+        });
+    }
+
+    public static VolleySingleton getInstance(Context context) {
+        if (mInstance == null) {
+            synchronized (VolleySingleton.class) {
+                if (mInstance == null) {
+                    mInstance = new VolleySingleton(context);
+                }
+            }
+        }
+        return mInstance;
+    }
+
+    public RequestQueue getRequestQueue(Context context) {
+        if (requestQueue == null) {
+            requestQueue = Volley.newRequestQueue(context);
+        }
+        return requestQueue;
+    }
+
+    public <T> void addToRequestQueue(Context context, Request<T> request) {
+        getRequestQueue(context).add(request);
+    }
+
+    public ImageLoader getImageLoader() {
+        return imageLoader;
+    }
+}

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

@@ -0,0 +1,61 @@
+/*
+ * 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();
+    }
+}

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

@@ -0,0 +1,133 @@
+/*
+ * 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 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
+     */
+    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();
+        }
+    }
+}

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

@@ -0,0 +1,318 @@
+/*
+ * 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 android.support.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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);
+    }
+
+    /**
+     * 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 (!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);
+        }
+    }
+
+    private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {
+
+        /**
+         * Staging area for requests that already have a duplicate request in flight.
+         *
+         * <ul>
+         *   <li>containsKey(cacheKey) indicates that there is a request in flight for the given
+         *       cache key.
+         *   <li>get(cacheKey) returns waiting requests for the given cache key. The in flight
+         *       request is <em>not</em> contained in that list. Is null if no requests are staged.
+         * </ul>
+         */
+        private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();
+
+        private final CacheDispatcher mCacheDispatcher;
+
+        WaitingRequestManager(CacheDispatcher cacheDispatcher) {
+            mCacheDispatcher = cacheDispatcher;
+        }
+
+        /**
+         * Request received a valid response that can be used by other waiting requests.
+         */
+        @Override
+        public void onResponseReceived(Request<?> request, Response<?> response) {
+            if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
+                onNoUsableResponseReceived(request);
+                return;
+            }
+            String cacheKey = request.getCacheKey();
+            List<Request<?>> waitingRequests;
+            synchronized (this) {
+                waitingRequests = mWaitingRequests.remove(cacheKey);
+            }
+            if (waitingRequests != null) {
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.", waitingRequests.size(), cacheKey);
+                }
+                // Process all queued up requests.
+                for (Request<?> waiting : waitingRequests) {
+                    mCacheDispatcher.mDelivery.postResponse(waiting, response);
+                }
+            }
+        }
+
+        /**
+         * No valid response received from network, release waiting requests.
+         */
+        @Override
+        public synchronized void onNoUsableResponseReceived(Request<?> request) {
+            String cacheKey = request.getCacheKey();
+            List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
+            if (waitingRequests != null && !waitingRequests.isEmpty()) {
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.v("%d waiting requests for cacheKey=%s; resend to network", waitingRequests.size(), cacheKey);
+                }
+                Request<?> nextInLine = waitingRequests.remove(0);
+                mWaitingRequests.put(cacheKey, waitingRequests);
+                nextInLine.setNetworkRequestCompleteListener(this);
+                try {
+                    mCacheDispatcher.mNetworkQueue.put(nextInLine);
+                } catch (InterruptedException iex) {
+                    VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
+                    // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
+                    Thread.currentThread().interrupt();
+                    // Quit the current CacheDispatcher thread.
+                    mCacheDispatcher.quit();
+                }
+            }
+        }
+
+        /**
+         * For cacheable requests, if a request for the same cache key is already in flight, add it
+         * to a queue to wait for that in-flight request to finish.
+         *
+         * @return whether the request was queued. If false, we should continue issuing the request
+         * over the network. If true, we should put the request on hold to be processed when the
+         * in-flight request finishes.
+         */
+        private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
+            String cacheKey = request.getCacheKey();
+            // Insert request into stage if there's already a request with the same cache key
+            // in flight.
+            if (mWaitingRequests.containsKey(cacheKey)) {
+                // There is already a request in flight. Queue up.
+                List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
+                if (stagedRequests == null) {
+                    stagedRequests = new ArrayList<>();
+                }
+                request.addMarker("waiting-for-response");
+                stagedRequests.add(request);
+                mWaitingRequests.put(cacheKey, stagedRequests);
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
+                }
+                return true;
+            } else {
+                // Insert 'null' queue for this cacheKey, indicating there is now a request in
+                // flight.
+                mWaitingRequests.put(cacheKey, null);
+                request.setNetworkRequestCompleteListener(this);
+                if (VolleyLog.DEBUG) {
+                    VolleyLog.d("new request, sending to network %s", cacheKey);
+                }
+                return false;
+            }
+        }
+    }
+}

+ 34 - 0
library_support/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_support/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;
+    }
+}

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

@@ -0,0 +1,127 @@
+/*
+ * 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();
+            }
+        }
+    }
+}

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

@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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.text.TextUtils;
+
+/**
+ * An HTTP header.
+ */
+public final class Header {
+    private final String mName;
+    private final String mValue;
+
+    public Header(String name, String value) {
+        mName = name;
+        mValue = value;
+    }
+
+    public final String getName() {
+        return mName;
+    }
+
+    public final String getValue() {
+        return mValue;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Header header = (Header) o;
+
+        return TextUtils.equals(mName, header.mName) && TextUtils.equals(mValue, header.mValue);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mName.hashCode();
+        result = 31 * result + mValue.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Header[name=" + mName + ",value=" + mValue + "]";
+    }
+}

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

@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * An interface for performing requests.
+ */
+public interface Network {
+    /**
+     * Performs the specified request.
+     *
+     * @param request Request to process
+     * @return A {@link NetworkResponse} with data and caching metadata; will never be null
+     * @throws VolleyError on errors
+     */
+    NetworkResponse performRequest(Request<?> request) throws VolleyError;
+}

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

@@ -0,0 +1,188 @@
+/*
+ * 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.annotation.TargetApi;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Provides a thread for performing network dispatch from a queue of requests.
+ *
+ * <p>Requests added to the specified queue are processed from the network via a specified {@link
+ * Network} interface. Responses are committed to cache, if eligible, using a specified {@link
+ * Cache} interface. Valid responses and errors are posted back to the caller via a {@link
+ * ResponseDelivery}.
+ */
+public class NetworkDispatcher extends Thread {
+
+    /**
+     * The queue of requests to service.
+     */
+    private final BlockingQueue<Request<?>> mQueue;
+    /**
+     * The network interface for processing requests.
+     */
+    private final Network mNetwork;
+    /**
+     * The cache to write to.
+     */
+    private final Cache mCache;
+    /**
+     * For posting responses and errors.
+     */
+    private final ResponseDelivery mDelivery;
+    /**
+     * Used for telling us to die.
+     */
+    private volatile boolean mQuit = false;
+
+    /**
+     * Creates a new network dispatcher thread. You must call {@link #start()} in order to begin
+     * processing.
+     *
+     * @param queue    Queue of incoming requests for triage
+     * @param network  Network interface to use for performing requests
+     * @param cache    Cache interface to use for writing responses to cache
+     * @param delivery Delivery interface to use for posting responses
+     */
+    public NetworkDispatcher(
+            BlockingQueue<Request<?>> queue,
+            Network network,
+            Cache cache,
+            ResponseDelivery delivery) {
+        mQueue = queue;
+        mNetwork = network;
+        mCache = cache;
+        mDelivery = 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();
+    }
+
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private void addTrafficStatsTag(Request<?> request) {
+        // Tag the request (if API >= 14)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
+        }
+    }
+
+    @Override
+    public void run() {
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        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 NetworkDispatcher 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 {
+        // Take a request from the queue.
+        Request<?> request = mQueue.take();
+        processRequest(request);
+    }
+
+    @VisibleForTesting
+    void processRequest(Request<?> request) {
+        long startTimeMs = SystemClock.elapsedRealtime();
+        request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED);
+        try {
+            request.addMarker("network-queue-take");
+
+            // If the request was cancelled already, do not perform the
+            // network request.
+            if (request.isCanceled()) {
+                request.finish("network-discard-cancelled");
+                request.notifyListenerResponseNotUsable();
+                return;
+            }
+
+            addTrafficStatsTag(request);
+
+            // Perform the network request.
+            NetworkResponse networkResponse = mNetwork.performRequest(request);
+            request.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 && request.hasHadResponseDelivered()) {
+                request.finish("not-modified");
+                request.notifyListenerResponseNotUsable();
+                return;
+            }
+
+            // Parse the response here on the worker thread.
+            Response<?> response = request.parseNetworkResponse(networkResponse);
+            request.addMarker("network-parse-complete");
+
+            // Write to cache if applicable.
+            // TODO: Only update cache metadata instead of entire record for 304s.
+            if (request.shouldCache() && response.cacheEntry != null) {
+                mCache.put(request.getCacheKey(), response.cacheEntry);
+                request.addMarker("network-cache-written");
+            }
+
+            // Post the response back.
+            request.markDelivered();
+            mDelivery.postResponse(request, response);
+            request.notifyListenerResponseReceived(response);
+        } catch (VolleyError volleyError) {
+            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+            parseAndDeliverNetworkError(request, volleyError);
+            request.notifyListenerResponseNotUsable();
+        } catch (Exception e) {
+            VolleyLog.e(e, "Unhandled exception %s", e.toString());
+            VolleyError volleyError = new VolleyError(e);
+            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
+            mDelivery.postError(request, volleyError);
+            request.notifyListenerResponseNotUsable();
+        } finally {
+            request.sendEvent(RequestQueue.RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED);
+        }
+    }
+
+    private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
+        error = request.parseNetworkError(error);
+        mDelivery.postError(request, error);
+    }
+}

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

@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Indicates that there was a network error when performing a Volley request.
+ */
+@SuppressWarnings("serial")
+public class NetworkError extends VolleyError {
+    public NetworkError() {
+        super();
+    }
+
+    public NetworkError(Throwable cause) {
+        super(cause);
+    }
+
+    public NetworkError(NetworkResponse networkResponse) {
+        super(networkResponse);
+    }
+}

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

@@ -0,0 +1,204 @@
+/*
+ * 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 java.net.HttpURLConnection;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Data and headers returned from {@link Network#performRequest(Request)}.
+ */
+public class NetworkResponse {
+
+    /**
+     * Creates a new network response.
+     *
+     * @param statusCode    the HTTP status code
+     * @param data          Response body
+     * @param headers       Headers returned with this response, or null for none
+     * @param notModified   True if the server returned a 304 and the data was already in cache
+     * @param networkTimeMs Round-trip network time to receive network response
+     * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+     * cannot handle server responses containing multiple headers with the same name. This
+     * constructor may be removed in a future release of Volley.
+     */
+    @Deprecated
+    public NetworkResponse(
+            int statusCode,
+            byte[] data,
+            Map<String, String> headers,
+            boolean notModified,
+            long networkTimeMs) {
+        this(statusCode, data, headers, toAllHeaderList(headers), notModified, networkTimeMs);
+    }
+
+    /**
+     * Creates a new network response.
+     *
+     * @param statusCode    the HTTP status code
+     * @param data          Response body
+     * @param notModified   True if the server returned a 304 and the data was already in cache
+     * @param networkTimeMs Round-trip network time to receive network response
+     * @param allHeaders    All headers returned with this response, or null for none
+     */
+    public NetworkResponse(
+            int statusCode,
+            byte[] data,
+            boolean notModified,
+            long networkTimeMs,
+            List<Header> allHeaders) {
+        this(statusCode, data, toHeaderMap(allHeaders), allHeaders, notModified, networkTimeMs);
+    }
+
+    /**
+     * Creates a new network response.
+     *
+     * @param statusCode  the HTTP status code
+     * @param data        Response body
+     * @param headers     Headers returned with this response, or null for none
+     * @param notModified True if the server returned a 304 and the data was already in cache
+     * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+     * cannot handle server responses containing multiple headers with the same name. This
+     * constructor may be removed in a future release of Volley.
+     */
+    @Deprecated
+    public NetworkResponse(
+            int statusCode, byte[] data, Map<String, String> headers, boolean notModified) {
+        this(statusCode, data, headers, notModified, /* networkTimeMs= */ 0);
+    }
+
+    /**
+     * Creates a new network response for an OK response with no headers.
+     *
+     * @param data Response body
+     */
+    public NetworkResponse(byte[] data) {
+        this(
+                HttpURLConnection.HTTP_OK,
+                data,
+                /* notModified= */ false,
+                /* networkTimeMs= */ 0,
+                Collections.<Header>emptyList());
+    }
+
+    /**
+     * Creates a new network response for an OK response.
+     *
+     * @param data    Response body
+     * @param headers Headers returned with this response, or null for none
+     * @deprecated see {@link #NetworkResponse(int, byte[], boolean, long, List)}. This constructor
+     * cannot handle server responses containing multiple headers with the same name. This
+     * constructor may be removed in a future release of Volley.
+     */
+    @Deprecated
+    public NetworkResponse(byte[] data, Map<String, String> headers) {
+        this(
+                HttpURLConnection.HTTP_OK,
+                data,
+                headers,
+                /* notModified= */ false,
+                /* networkTimeMs= */ 0);
+    }
+
+    private NetworkResponse(
+            int statusCode,
+            byte[] data,
+            Map<String, String> headers,
+            List<Header> allHeaders,
+            boolean notModified,
+            long networkTimeMs) {
+        this.statusCode = statusCode;
+        this.data = data;
+        this.headers = headers;
+        if (allHeaders == null) {
+            this.allHeaders = null;
+        } else {
+            this.allHeaders = Collections.unmodifiableList(allHeaders);
+        }
+        this.notModified = notModified;
+        this.networkTimeMs = networkTimeMs;
+    }
+
+    /**
+     * The HTTP status code.
+     */
+    public final int statusCode;
+
+    /**
+     * Raw data from this response.
+     */
+    public final byte[] data;
+
+    /**
+     * Response headers.
+     *
+     * <p>This map is case-insensitive. It 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 last one. Use {@link #allHeaders} to inspect all headers returned
+     * by the server.
+     */
+    public final Map<String, String> headers;
+
+    /**
+     * All response headers. Must not be mutated directly.
+     */
+    public final List<Header> allHeaders;
+
+    /**
+     * True if the server returned a 304 (Not Modified).
+     */
+    public final boolean notModified;
+
+    /**
+     * Network roundtrip time in milliseconds.
+     */
+    public final long networkTimeMs;
+
+    private static Map<String, String> toHeaderMap(List<Header> allHeaders) {
+        if (allHeaders == null) {
+            return null;
+        }
+        if (allHeaders.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        // Later elements in the list take precedence.
+        for (Header header : allHeaders) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    private static List<Header> toAllHeaderList(Map<String, String> headers) {
+        if (headers == null) {
+            return null;
+        }
+        if (headers.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Header> allHeaders = new ArrayList<>(headers.size());
+        for (Map.Entry<String, String> header : headers.entrySet()) {
+            allHeaders.add(new Header(header.getKey(), header.getValue()));
+        }
+        return allHeaders;
+    }
+}

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

@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Error indicating that no connection could be established when performing a Volley request.
+ */
+@SuppressWarnings("serial")
+public class NoConnectionError extends NetworkError {
+    public NoConnectionError() {
+        super();
+    }
+
+    public NoConnectionError(Throwable reason) {
+        super(reason);
+    }
+}

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

@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Indicates that the server's response could not be parsed.
+ */
+@SuppressWarnings("serial")
+public class ParseError extends VolleyError {
+    public ParseError() {
+    }
+
+    public ParseError(NetworkResponse networkResponse) {
+        super(networkResponse);
+    }
+
+    public ParseError(Throwable cause) {
+        super(cause);
+    }
+}

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

@@ -0,0 +1,768 @@
+/*
+ * 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.net.TrafficStats;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.CallSuper;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import cn.yyxx.support.volley.source.VolleyLog.MarkerLog;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Base class for all network requests.
+ *
+ * @param <T> The type of parsed response this request expects.
+ */
+public abstract class Request<T> implements Comparable<Request<T>> {
+
+    /**
+     * Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}.
+     */
+    private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
+
+    /**
+     * Supported request methods.
+     */
+    public interface Method {
+        int DEPRECATED_GET_OR_POST = -1;
+        int GET = 0;
+        int POST = 1;
+        int PUT = 2;
+        int DELETE = 3;
+        int HEAD = 4;
+        int OPTIONS = 5;
+        int TRACE = 6;
+        int PATCH = 7;
+    }
+
+    /**
+     * Callback to notify when the network request returns.
+     */
+    /* package */ interface NetworkRequestCompleteListener {
+
+        /**
+         * Callback when a network response has been received.
+         */
+        void onResponseReceived(Request<?> request, Response<?> response);
+
+        /**
+         * Callback when request returns from network without valid response.
+         */
+        void onNoUsableResponseReceived(Request<?> request);
+    }
+
+    /**
+     * An event log tracing the lifetime of this request; for debugging.
+     */
+    private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
+
+    /**
+     * Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS,
+     * TRACE, and PATCH.
+     */
+    private final int mMethod;
+
+    /**
+     * URL of this request.
+     */
+    private final String mUrl;
+
+    /**
+     * Default tag for {@link TrafficStats}.
+     */
+    private final int mDefaultTrafficStatsTag;
+
+    /**
+     * Lock to guard state which can be mutated after a request is added to the queue.
+     */
+    private final Object mLock = new Object();
+
+    /**
+     * Listener interface for errors.
+     */
+    @Nullable
+    @GuardedBy("mLock")
+    private Response.ErrorListener mErrorListener;
+
+    /**
+     * Sequence number of this request, used to enforce FIFO ordering.
+     */
+    private Integer mSequence;
+
+    /**
+     * The request queue this request is associated with.
+     */
+    private RequestQueue mRequestQueue;
+
+    /**
+     * Whether or not responses to this request should be cached.
+     */
+    // TODO(#190): Turn this off by default for anything other than GET requests.
+    private boolean mShouldCache = true;
+
+    /**
+     * Whether or not this request has been canceled.
+     */
+    @GuardedBy("mLock")
+    private boolean mCanceled = false;
+
+    /**
+     * Whether or not a response has been delivered for this request yet.
+     */
+    @GuardedBy("mLock")
+    private boolean mResponseDelivered = false;
+
+    /**
+     * Whether the request should be retried in the event of an HTTP 5xx (server) error.
+     */
+    private boolean mShouldRetryServerErrors = false;
+
+    /**
+     * The retry policy for this request.
+     */
+    private RetryPolicy mRetryPolicy;
+
+    /**
+     * When a request can be retrieved from cache but must be refreshed from the network, the cache
+     * entry will be stored here so that in the event of a "Not Modified" response, we can be sure
+     * it hasn't been evicted from cache.
+     */
+    private Cache.Entry mCacheEntry = null;
+
+    /**
+     * An opaque token tagging this request; used for bulk cancellation.
+     */
+    private Object mTag;
+
+    /**
+     * Listener that will be notified when a response has been delivered.
+     */
+    @GuardedBy("mLock")
+    private NetworkRequestCompleteListener mRequestCompleteListener;
+
+    /**
+     * Creates a new request with the given URL and error listener. Note that the normal response
+     * listener is not provided here as delivery of responses is provided by subclasses, who have a
+     * better idea of how to deliver an already-parsed response.
+     *
+     * @deprecated Use {@link #Request(int, String, Response.ErrorListener)}.
+     */
+    @Deprecated
+    public Request(String url, Response.ErrorListener listener) {
+        this(Method.DEPRECATED_GET_OR_POST, url, listener);
+    }
+
+    /**
+     * Creates a new request with the given method (one of the values from {@link Method}), URL, and
+     * error listener. Note that the normal response listener is not provided here as delivery of
+     * responses is provided by subclasses, who have a better idea of how to deliver an
+     * already-parsed response.
+     */
+    public Request(int method, String url, @Nullable Response.ErrorListener listener) {
+        mMethod = method;
+        mUrl = url;
+        mErrorListener = listener;
+        setRetryPolicy(new DefaultRetryPolicy());
+
+        mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
+    }
+
+    /**
+     * Return the method for this request. Can be one of the values in {@link Method}.
+     */
+    public int getMethod() {
+        return mMethod;
+    }
+
+    /**
+     * Set a tag on this request. Can be used to cancel all requests with this tag by {@link
+     * RequestQueue#cancelAll(Object)}.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public Request<?> setTag(Object tag) {
+        mTag = tag;
+        return this;
+    }
+
+    /**
+     * Returns this request's tag.
+     *
+     * @see Request#setTag(Object)
+     */
+    public Object getTag() {
+        return mTag;
+    }
+
+    /**
+     * @return this request's {@link Response.ErrorListener}.
+     */
+    @Nullable
+    public Response.ErrorListener getErrorListener() {
+        synchronized (mLock) {
+            return mErrorListener;
+        }
+    }
+
+    /**
+     * @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)}
+     */
+    public int getTrafficStatsTag() {
+        return mDefaultTrafficStatsTag;
+    }
+
+    /**
+     * @return The hashcode of the URL's host component, or 0 if there is none.
+     */
+    private static int findDefaultTrafficStatsTag(String url) {
+        if (!TextUtils.isEmpty(url)) {
+            Uri uri = Uri.parse(url);
+            if (uri != null) {
+                String host = uri.getHost();
+                if (host != null) {
+                    return host.hashCode();
+                }
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Sets the retry policy for this request.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
+        mRetryPolicy = retryPolicy;
+        return this;
+    }
+
+    /**
+     * Adds an event to this request's event log; for debugging.
+     */
+    public void addMarker(String tag) {
+        if (MarkerLog.ENABLED) {
+            mEventLog.add(tag, Thread.currentThread().getId());
+        }
+    }
+
+    /**
+     * Notifies the request queue that this request has finished (successfully or with error).
+     *
+     * <p>Also dumps all events from this request's event log; for debugging.
+     */
+    void finish(final String tag) {
+        if (mRequestQueue != null) {
+            mRequestQueue.finish(this);
+        }
+        if (MarkerLog.ENABLED) {
+            final long threadId = Thread.currentThread().getId();
+            if (Looper.myLooper() != Looper.getMainLooper()) {
+                // If we finish marking off of the main thread, we need to
+                // actually do it on the main thread to ensure correct ordering.
+                Handler mainThread = new Handler(Looper.getMainLooper());
+                mainThread.post(
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                mEventLog.add(tag, threadId);
+                                mEventLog.finish(Request.this.toString());
+                            }
+                        });
+                return;
+            }
+
+            mEventLog.add(tag, threadId);
+            mEventLog.finish(this.toString());
+        }
+    }
+
+    void sendEvent(@RequestQueue.RequestEvent int event) {
+        if (mRequestQueue != null) {
+            mRequestQueue.sendRequestEvent(this, event);
+        }
+    }
+
+    /**
+     * Associates this request with the given queue. The request queue will be notified when this
+     * request has finished.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public Request<?> setRequestQueue(RequestQueue requestQueue) {
+        mRequestQueue = requestQueue;
+        return this;
+    }
+
+    /**
+     * Sets the sequence number of this request. Used by {@link RequestQueue}.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public final Request<?> setSequence(int sequence) {
+        mSequence = sequence;
+        return this;
+    }
+
+    /**
+     * Returns the sequence number of this request.
+     */
+    public final int getSequence() {
+        if (mSequence == null) {
+            throw new IllegalStateException("getSequence called before setSequence");
+        }
+        return mSequence;
+    }
+
+    /**
+     * Returns the URL of this request.
+     */
+    public String getUrl() {
+        return mUrl;
+    }
+
+    /**
+     * Returns the cache key for this request. By default, this is the URL.
+     */
+    public String getCacheKey() {
+        String url = getUrl();
+        // If this is a GET request, just use the URL as the key.
+        // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches
+        // legacy behavior where all methods had the same cache key. We can't determine which method
+        // will be used because doing so requires calling getPostBody() which is expensive and may
+        // throw AuthFailureError.
+        // TODO(#190): Remove support for non-GET methods.
+        int method = getMethod();
+        if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) {
+            return url;
+        }
+        return Integer.toString(method) + '-' + url;
+    }
+
+    /**
+     * Annotates this request with an entry retrieved for it from cache. Used for cache coherency
+     * support.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public Request<?> setCacheEntry(Cache.Entry entry) {
+        mCacheEntry = entry;
+        return this;
+    }
+
+    /**
+     * Returns the annotated cache entry, or null if there isn't one.
+     */
+    public Cache.Entry getCacheEntry() {
+        return mCacheEntry;
+    }
+
+    /**
+     * Mark this request as canceled.
+     *
+     * <p>No callback will be delivered as long as either:
+     *
+     * <ul>
+     *   <li>This method is called on the same thread as the {@link ResponseDelivery} is running on.
+     *       By default, this is the main thread.
+     *   <li>The request subclass being used overrides cancel() and ensures that it does not invoke
+     *       the listener in {@link #deliverResponse} after cancel() has been called in a
+     *       thread-safe manner.
+     * </ul>
+     *
+     * <p>There are no guarantees if both of these conditions aren't met.
+     */
+    @CallSuper
+    public void cancel() {
+        synchronized (mLock) {
+            mCanceled = true;
+            mErrorListener = null;
+        }
+    }
+
+    /**
+     * Returns true if this request has been canceled.
+     */
+    public boolean isCanceled() {
+        synchronized (mLock) {
+            return mCanceled;
+        }
+    }
+
+    /**
+     * Returns a list of extra HTTP headers to go along with this request. Can throw {@link
+     * AuthFailureError} as authentication may be required to provide these values.
+     *
+     * @throws AuthFailureError In the event of auth failure
+     */
+    public Map<String, String> getHeaders() throws AuthFailureError {
+        return Collections.emptyMap();
+    }
+
+    /**
+     * Returns a Map of POST parameters to be used for this request, or null if a simple GET should
+     * be used. Can throw {@link AuthFailureError} as authentication may be required to provide
+     * these values.
+     *
+     * <p>Note that only one of getPostParams() and getPostBody() can return a non-null value.
+     *
+     * @throws AuthFailureError In the event of auth failure
+     * @deprecated Use {@link #getParams()} instead.
+     */
+    @Deprecated
+    protected Map<String, String> getPostParams() throws AuthFailureError {
+        return getParams();
+    }
+
+    /**
+     * Returns which encoding should be used when converting POST parameters returned by {@link
+     * #getPostParams()} into a raw POST body.
+     *
+     * <p>This controls both encodings:
+     *
+     * <ol>
+     *   <li>The string encoding used when converting parameter names and values into bytes prior to
+     *       URL encoding them.
+     *   <li>The string encoding used when converting the URL encoded parameters into a raw byte
+     *       array.
+     * </ol>
+     *
+     * @deprecated Use {@link #getParamsEncoding()} instead.
+     */
+    @Deprecated
+    protected String getPostParamsEncoding() {
+        return getParamsEncoding();
+    }
+
+    /**
+     * @deprecated Use {@link #getBodyContentType()} instead.
+     */
+    @Deprecated
+    public String getPostBodyContentType() {
+        return getBodyContentType();
+    }
+
+    /**
+     * Returns the raw POST body to be sent.
+     *
+     * @throws AuthFailureError In the event of auth failure
+     * @deprecated Use {@link #getBody()} instead.
+     */
+    @Deprecated
+    public byte[] getPostBody() throws AuthFailureError {
+        // Note: For compatibility with legacy clients of volley, this implementation must remain
+        // here instead of simply calling the getBody() function because this function must
+        // call getPostParams() and getPostParamsEncoding() since legacy clients would have
+        // overridden these two member functions for POST requests.
+        Map<String, String> postParams = getPostParams();
+        if (postParams != null && postParams.size() > 0) {
+            return encodeParameters(postParams, getPostParamsEncoding());
+        }
+        return null;
+    }
+
+    /**
+     * Returns a Map of parameters to be used for a POST or PUT request. Can throw {@link
+     * AuthFailureError} as authentication may be required to provide these values.
+     *
+     * <p>Note that you can directly override {@link #getBody()} for custom data.
+     *
+     * @throws AuthFailureError in the event of auth failure
+     */
+    protected Map<String, String> getParams() throws AuthFailureError {
+        return null;
+    }
+
+    /**
+     * Returns which encoding should be used when converting POST or PUT parameters returned by
+     * {@link #getParams()} into a raw POST or PUT body.
+     *
+     * <p>This controls both encodings:
+     *
+     * <ol>
+     *   <li>The string encoding used when converting parameter names and values into bytes prior to
+     *       URL encoding them.
+     *   <li>The string encoding used when converting the URL encoded parameters into a raw byte
+     *       array.
+     * </ol>
+     */
+    protected String getParamsEncoding() {
+        return DEFAULT_PARAMS_ENCODING;
+    }
+
+    /**
+     * Returns the content type of the POST or PUT body.
+     */
+    public String getBodyContentType() {
+        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
+    }
+
+    /**
+     * Returns the raw POST or PUT body to be sent.
+     *
+     * <p>By default, the body consists of the request parameters in
+     * application/x-www-form-urlencoded format. When overriding this method, consider overriding
+     * {@link #getBodyContentType()} as well to match the new body format.
+     *
+     * @throws AuthFailureError in the event of auth failure
+     */
+    public byte[] getBody() throws AuthFailureError {
+        Map<String, String> params = getParams();
+        if (params != null && params.size() > 0) {
+            return encodeParameters(params, getParamsEncoding());
+        }
+        return null;
+    }
+
+    /**
+     * Converts <code>params</code> into an application/x-www-form-urlencoded encoded string.
+     */
+    private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
+        StringBuilder encodedParams = new StringBuilder();
+        try {
+            for (Map.Entry<String, String> entry : params.entrySet()) {
+                if (entry.getKey() == null || entry.getValue() == null) {
+                    throw new IllegalArgumentException(
+                            String.format(
+                                    "Request#getParams() or Request#getPostParams() returned a map "
+                                            + "containing a null key or value: (%s, %s). All keys "
+                                            + "and values must be non-null.",
+                                    entry.getKey(), entry.getValue()));
+                }
+                encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
+                encodedParams.append('=');
+                encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
+                encodedParams.append('&');
+            }
+            return encodedParams.toString().getBytes(paramsEncoding);
+        } catch (UnsupportedEncodingException uee) {
+            throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
+        }
+    }
+
+    /**
+     * Set whether or not responses to this request should be cached.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public final Request<?> setShouldCache(boolean shouldCache) {
+        mShouldCache = shouldCache;
+        return this;
+    }
+
+    /**
+     * Returns true if responses to this request should be cached.
+     */
+    public final boolean shouldCache() {
+        return mShouldCache;
+    }
+
+    /**
+     * Sets whether or not the request should be retried in the event of an HTTP 5xx (server) error.
+     *
+     * @return This Request object to allow for chaining.
+     */
+    public final Request<?> setShouldRetryServerErrors(boolean shouldRetryServerErrors) {
+        mShouldRetryServerErrors = shouldRetryServerErrors;
+        return this;
+    }
+
+    /**
+     * Returns true if this request should be retried in the event of an HTTP 5xx (server) error.
+     */
+    public final boolean shouldRetryServerErrors() {
+        return mShouldRetryServerErrors;
+    }
+
+    /**
+     * Priority values. Requests will be processed from higher priorities to lower priorities, in
+     * FIFO order.
+     */
+    public enum Priority {
+        LOW,
+        NORMAL,
+        HIGH,
+        IMMEDIATE
+    }
+
+    /**
+     * Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
+     */
+    public Priority getPriority() {
+        return Priority.NORMAL;
+    }
+
+    /**
+     * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed per
+     * retry attempt if a backoff is specified via backoffTimeout()). If there are no retry attempts
+     * remaining, this will cause delivery of a {@link TimeoutError} error.
+     */
+    public final int getTimeoutMs() {
+        return getRetryPolicy().getCurrentTimeout();
+    }
+
+    /**
+     * Returns the retry policy that should be used for this request.
+     */
+    public RetryPolicy getRetryPolicy() {
+        return mRetryPolicy;
+    }
+
+    /**
+     * Mark this request as having a response delivered on it. This can be used later in the
+     * request's lifetime for suppressing identical responses.
+     */
+    public void markDelivered() {
+        synchronized (mLock) {
+            mResponseDelivered = true;
+        }
+    }
+
+    /**
+     * Returns true if this request has had a response delivered for it.
+     */
+    public boolean hasHadResponseDelivered() {
+        synchronized (mLock) {
+            return mResponseDelivered;
+        }
+    }
+
+    /**
+     * Subclasses must implement this to parse the raw network response and return an appropriate
+     * response type. This method will be called from a worker thread. The response will not be
+     * delivered if you return null.
+     *
+     * @param response Response from the network
+     * @return The parsed response, or null in the case of an error
+     */
+    protected abstract Response<T> parseNetworkResponse(NetworkResponse response);
+
+    /**
+     * Subclasses can override this method to parse 'networkError' and return a more specific error.
+     *
+     * <p>The default implementation just returns the passed 'networkError'.
+     *
+     * @param volleyError the error retrieved from the network
+     * @return an NetworkError augmented with additional information
+     */
+    protected VolleyError parseNetworkError(VolleyError volleyError) {
+        return volleyError;
+    }
+
+    /**
+     * Subclasses must implement this to perform delivery of the parsed response to their listeners.
+     * The given response is guaranteed to be non-null; responses that fail to parse are not
+     * delivered.
+     *
+     * @param response The parsed response returned by {@link
+     *                 #parseNetworkResponse(NetworkResponse)}
+     */
+    protected abstract void deliverResponse(T response);
+
+    /**
+     * Delivers error message to the ErrorListener that the Request was initialized with.
+     *
+     * @param error Error details
+     */
+    public void deliverError(VolleyError error) {
+        Response.ErrorListener listener;
+        synchronized (mLock) {
+            listener = mErrorListener;
+        }
+        if (listener != null) {
+            listener.onErrorResponse(error);
+        }
+    }
+
+    /**
+     * {@link NetworkRequestCompleteListener} that will receive callbacks when the request returns
+     * from the network.
+     */
+    /* package */ void setNetworkRequestCompleteListener(
+            NetworkRequestCompleteListener requestCompleteListener) {
+        synchronized (mLock) {
+            mRequestCompleteListener = requestCompleteListener;
+        }
+    }
+
+    /**
+     * Notify NetworkRequestCompleteListener that a valid response has been received which can be
+     * used for other, waiting requests.
+     *
+     * @param response received from the network
+     */
+    /* package */ void notifyListenerResponseReceived(Response<?> response) {
+        NetworkRequestCompleteListener listener;
+        synchronized (mLock) {
+            listener = mRequestCompleteListener;
+        }
+        if (listener != null) {
+            listener.onResponseReceived(this, response);
+        }
+    }
+
+    /**
+     * Notify NetworkRequestCompleteListener that the network request did not result in a response
+     * which can be used for other, waiting requests.
+     */
+    /* package */ void notifyListenerResponseNotUsable() {
+        NetworkRequestCompleteListener listener;
+        synchronized (mLock) {
+            listener = mRequestCompleteListener;
+        }
+        if (listener != null) {
+            listener.onNoUsableResponseReceived(this);
+        }
+    }
+
+    /**
+     * Our comparator sorts from high to low priority, and secondarily by sequence number to provide
+     * FIFO ordering.
+     */
+    @Override
+    public int compareTo(Request<T> other) {
+        Priority left = this.getPriority();
+        Priority right = other.getPriority();
+
+        // High-priority requests are "lesser" so they are sorted to the front.
+        // Equal priorities are sorted by sequence number to provide FIFO ordering.
+        return left == right ? this.mSequence - other.mSequence : right.ordinal() - left.ordinal();
+    }
+
+    @Override
+    public String toString() {
+        String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag());
+        return (isCanceled() ? "[X] " : "[ ] ")
+                + getUrl()
+                + " "
+                + trafficStatsTag
+                + " "
+                + getPriority()
+                + " "
+                + mSequence;
+    }
+}

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

@@ -0,0 +1,383 @@
+/*
+ * 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 android.os.Looper;
+import android.support.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A request dispatch queue with a thread pool of dispatchers.
+ *
+ * <p>Calling {@link #add(Request)} will enqueue the given Request for dispatch, resolving from
+ * either cache or network on a worker thread, and then delivering a parsed response on the main
+ * thread.
+ */
+public class RequestQueue {
+
+    /**
+     * Callback interface for completed requests.
+     */
+    // TODO: This should not be a generic class, because the request type can't be determined at
+    // compile time, so all calls to onRequestFinished are unsafe. However, changing this would be
+    // an API-breaking change. See also: https://github.com/google/volley/pull/109
+    @Deprecated // Use RequestEventListener instead.
+    public interface RequestFinishedListener<T> {
+        /**
+         * Called when a request has finished processing.
+         */
+        void onRequestFinished(Request<T> request);
+    }
+
+    /**
+     * Request event types the listeners {@link RequestEventListener} will be notified about.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            RequestEvent.REQUEST_QUEUED,
+            RequestEvent.REQUEST_CACHE_LOOKUP_STARTED,
+            RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED,
+            RequestEvent.REQUEST_NETWORK_DISPATCH_STARTED,
+            RequestEvent.REQUEST_NETWORK_DISPATCH_FINISHED,
+            RequestEvent.REQUEST_FINISHED
+    })
+    public @interface RequestEvent {
+        /**
+         * The request was added to the queue.
+         */
+        int REQUEST_QUEUED = 0;
+        /**
+         * Cache lookup started for the request.
+         */
+        int REQUEST_CACHE_LOOKUP_STARTED = 1;
+        /**
+         * Cache lookup finished for the request and cached response is delivered or request is
+         * queued for network dispatching.
+         */
+        int REQUEST_CACHE_LOOKUP_FINISHED = 2;
+        /**
+         * Network dispatch started for the request.
+         */
+        int REQUEST_NETWORK_DISPATCH_STARTED = 3;
+        /**
+         * The network dispatch finished for the request and response (if any) is delivered.
+         */
+        int REQUEST_NETWORK_DISPATCH_FINISHED = 4;
+        /**
+         * All the work associated with the request is finished and request is removed from all the
+         * queues.
+         */
+        int REQUEST_FINISHED = 5;
+    }
+
+    /**
+     * Callback interface for request life cycle events.
+     */
+    public interface RequestEventListener {
+        /**
+         * Called on every request lifecycle event. Can be called from different threads. The call
+         * is blocking request processing, so any processing should be kept at minimum or moved to
+         * another thread.
+         */
+        void onRequestEvent(Request<?> request, @RequestEvent int event);
+    }
+
+    /**
+     * Used for generating monotonically-increasing sequence numbers for requests.
+     */
+    private final AtomicInteger mSequenceGenerator = new AtomicInteger();
+
+    /**
+     * The set of all requests currently being processed by this RequestQueue. A Request will be in
+     * this set if it is waiting in any queue or currently being processed by any dispatcher.
+     */
+    private final Set<Request<?>> mCurrentRequests = new HashSet<>();
+
+    /**
+     * The cache triage queue.
+     */
+    private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<>();
+
+    /**
+     * The queue of requests that are actually going out to the network.
+     */
+    private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<>();
+
+    /**
+     * Number of network request dispatcher threads to start.
+     */
+    private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
+
+    /**
+     * Cache interface for retrieving and storing responses.
+     */
+    private final Cache mCache;
+
+    /**
+     * Network interface for performing requests.
+     */
+    private final Network mNetwork;
+
+    /**
+     * Response delivery mechanism.
+     */
+    private final ResponseDelivery mDelivery;
+
+    /**
+     * The network dispatchers.
+     */
+    private final NetworkDispatcher[] mDispatchers;
+
+    /**
+     * The cache dispatcher.
+     */
+    private CacheDispatcher mCacheDispatcher;
+
+    private final List<RequestFinishedListener> mFinishedListeners = new ArrayList<>();
+
+    /**
+     * Collection of listeners for request life cycle events.
+     */
+    private final List<RequestEventListener> mEventListeners = new ArrayList<>();
+
+    /**
+     * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+     *
+     * @param cache          A Cache to use for persisting responses to disk
+     * @param network        A Network interface for performing HTTP requests
+     * @param threadPoolSize Number of network dispatcher threads to create
+     * @param delivery       A ResponseDelivery interface for posting responses and errors
+     */
+    public RequestQueue(
+            Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) {
+        mCache = cache;
+        mNetwork = network;
+        mDispatchers = new NetworkDispatcher[threadPoolSize];
+        mDelivery = delivery;
+    }
+
+    /**
+     * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+     *
+     * @param cache          A Cache to use for persisting responses to disk
+     * @param network        A Network interface for performing HTTP requests
+     * @param threadPoolSize Number of network dispatcher threads to create
+     */
+    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
+        this(
+                cache,
+                network,
+                threadPoolSize,
+                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
+    }
+
+    /**
+     * Creates the worker pool. Processing will not begin until {@link #start()} is called.
+     *
+     * @param cache   A Cache to use for persisting responses to disk
+     * @param network A Network interface for performing HTTP requests
+     */
+    public RequestQueue(Cache cache, Network network) {
+        this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
+    }
+
+    /**
+     * Starts the dispatchers in this queue.
+     */
+    public void start() {
+        stop(); // Make sure any currently running dispatchers are stopped.
+        // Create the cache dispatcher and start it.
+        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
+        mCacheDispatcher.start();
+
+        // Create network dispatchers (and corresponding threads) up to the pool size.
+        for (int i = 0; i < mDispatchers.length; i++) {
+            NetworkDispatcher networkDispatcher =
+                    new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
+            mDispatchers[i] = networkDispatcher;
+            networkDispatcher.start();
+        }
+    }
+
+    /**
+     * Stops the cache and network dispatchers.
+     */
+    public void stop() {
+        if (mCacheDispatcher != null) {
+            mCacheDispatcher.quit();
+        }
+        for (final NetworkDispatcher mDispatcher : mDispatchers) {
+            if (mDispatcher != null) {
+                mDispatcher.quit();
+            }
+        }
+    }
+
+    /**
+     * Gets a sequence number.
+     */
+    public int getSequenceNumber() {
+        return mSequenceGenerator.incrementAndGet();
+    }
+
+    /**
+     * Gets the {@link Cache} instance being used.
+     */
+    public Cache getCache() {
+        return mCache;
+    }
+
+    /**
+     * A simple predicate or filter interface for Requests, for use by {@link
+     * RequestQueue#cancelAll(RequestFilter)}.
+     */
+    public interface RequestFilter {
+        boolean apply(Request<?> request);
+    }
+
+    /**
+     * Cancels all requests in this queue for which the given filter applies.
+     *
+     * @param filter The filtering function to use
+     */
+    public void cancelAll(RequestFilter filter) {
+        synchronized (mCurrentRequests) {
+            for (Request<?> request : mCurrentRequests) {
+                if (filter.apply(request)) {
+                    request.cancel();
+                }
+            }
+        }
+    }
+
+    /**
+     * Cancels all requests in this queue with the given tag. Tag must be non-null and equality is
+     * by identity.
+     */
+    public void cancelAll(final Object tag) {
+        if (tag == null) {
+            throw new IllegalArgumentException("Cannot cancelAll with a null tag");
+        }
+        cancelAll(new RequestFilter() {
+            @Override
+            public boolean apply(Request<?> request) {
+                return request.getTag() == tag;
+            }
+        });
+    }
+
+    /**
+     * Adds a Request to the dispatch queue.
+     *
+     * @param request The request to service
+     * @return The passed-in request
+     */
+    public <T> Request<T> add(Request<T> request) {
+        // Tag the request as belonging to this queue and add it to the set of current requests.
+        request.setRequestQueue(this);
+        synchronized (mCurrentRequests) {
+            mCurrentRequests.add(request);
+        }
+
+        // Process requests in the order they are added.
+        request.setSequence(getSequenceNumber());
+        request.addMarker("add-to-queue");
+        sendRequestEvent(request, RequestEvent.REQUEST_QUEUED);
+
+        // If the request is uncacheable, skip the cache queue and go straight to the network.
+        if (!request.shouldCache()) {
+            mNetworkQueue.add(request);
+            return request;
+        }
+        mCacheQueue.add(request);
+        return request;
+    }
+
+    /**
+     * Called from {@link Request#finish(String)}, indicating that processing of the given request
+     * has finished.
+     */
+    @SuppressWarnings("unchecked")
+    // see above note on RequestFinishedListener
+    <T> void finish(Request<T> request) {
+        // Remove from the set of requests currently being processed.
+        synchronized (mCurrentRequests) {
+            mCurrentRequests.remove(request);
+        }
+        synchronized (mFinishedListeners) {
+            for (RequestFinishedListener<T> listener : mFinishedListeners) {
+                listener.onRequestFinished(request);
+            }
+        }
+        sendRequestEvent(request, RequestEvent.REQUEST_FINISHED);
+    }
+
+    /**
+     * Sends a request life cycle event to the listeners.
+     */
+    void sendRequestEvent(Request<?> request, @RequestEvent int event) {
+        synchronized (mEventListeners) {
+            for (RequestEventListener listener : mEventListeners) {
+                listener.onRequestEvent(request, event);
+            }
+        }
+    }
+
+    /**
+     * Add a listener for request life cycle events.
+     */
+    public void addRequestEventListener(RequestEventListener listener) {
+        synchronized (mEventListeners) {
+            mEventListeners.add(listener);
+        }
+    }
+
+    /**
+     * Remove a listener for request life cycle events.
+     */
+    public void removeRequestEventListener(RequestEventListener listener) {
+        synchronized (mEventListeners) {
+            mEventListeners.remove(listener);
+        }
+    }
+
+    @Deprecated // Use RequestEventListener instead.
+    public <T> void addRequestFinishedListener(RequestFinishedListener<T> listener) {
+        synchronized (mFinishedListeners) {
+            mFinishedListeners.add(listener);
+        }
+    }
+
+    /**
+     * Remove a RequestFinishedListener. Has no effect if listener was not previously added.
+     */
+    @Deprecated // Use RequestEventListener instead.
+    public <T> void removeRequestFinishedListener(RequestFinishedListener<T> listener) {
+        synchronized (mFinishedListeners) {
+            mFinishedListeners.remove(listener);
+        }
+    }
+}

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

@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+/**
+ * Encapsulates a parsed response for delivery.
+ *
+ * @param <T> Parsed type of this response
+ */
+public class Response<T> {
+
+    /**
+     * Callback interface for delivering parsed responses.
+     */
+    public interface Listener<T> {
+        /**
+         * Called when a response is received.
+         */
+        void onResponse(T response);
+    }
+
+    /**
+     * Callback interface for delivering error responses.
+     */
+    public interface ErrorListener {
+        /**
+         * Callback method that an error has been occurred with the provided error code and optional
+         * user-readable message.
+         */
+        void onErrorResponse(VolleyError error);
+    }
+
+    /**
+     * Returns a successful response containing the parsed result.
+     */
+    public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
+        return new Response<>(result, cacheEntry);
+    }
+
+    /**
+     * Returns a failed response containing the given error code and an optional localized message
+     * displayed to the user.
+     */
+    public static <T> Response<T> error(VolleyError error) {
+        return new Response<>(error);
+    }
+
+    /**
+     * Parsed response, or null in the case of error.
+     */
+    public final T result;
+
+    /**
+     * Cache metadata for this response, or null in the case of error.
+     */
+    public final Cache.Entry cacheEntry;
+
+    /**
+     * Detailed error information if <code>errorCode != OK</code>.
+     */
+    public final VolleyError error;
+
+    /**
+     * True if this response was a soft-expired one and a second one MAY be coming.
+     */
+    public boolean intermediate = false;
+
+    /**
+     * Returns whether this response is considered successful.
+     */
+    public boolean isSuccess() {
+        return error == null;
+    }
+
+    private Response(T result, Cache.Entry cacheEntry) {
+        this.result = result;
+        this.cacheEntry = cacheEntry;
+        this.error = null;
+    }
+
+    private Response(VolleyError error) {
+        this.result = null;
+        this.cacheEntry = null;
+        this.error = error;
+    }
+}

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

@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+public interface ResponseDelivery {
+    /**
+     * Parses a response from the network or cache and delivers it.
+     */
+    void postResponse(Request<?> request, Response<?> response);
+
+    /**
+     * Parses a response from the network or cache and delivers it. The provided Runnable will be
+     * executed after delivery.
+     */
+    void postResponse(Request<?> request, Response<?> response, Runnable runnable);
+
+    /**
+     * Posts an error for the given request.
+     */
+    void postError(Request<?> request, VolleyError error);
+}

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

@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+/**
+ * Retry policy for a request.
+ *
+ * <p>A retry policy can control two parameters:
+ *
+ * <ul>
+ *   <li>The number of tries. This can be a simple counter or more complex logic based on the type
+ *       of error passed to {@link #retry(VolleyError)}, although {@link #getCurrentRetryCount()}
+ *       should always return the current retry count for logging purposes.
+ *   <li>The request timeout for each try, via {@link #getCurrentTimeout()}. In the common case that
+ *       a request times out before the response has been received from the server, retrying again
+ *       with a longer timeout can increase the likelihood of success (at the expense of causing the
+ *       user to wait longer, especially if the request still fails).
+ * </ul>
+ *
+ * <p>Note that currently, retries triggered by a retry policy are attempted immediately in sequence
+ * with no delay between them (although the time between tries may increase if the requests are
+ * timing out and {@link #getCurrentTimeout()} is returning increasing values).
+ *
+ * <p>By default, Volley uses {@link DefaultRetryPolicy}.
+ */
+public interface RetryPolicy {
+
+    /**
+     * Returns the current timeout (used for logging).
+     */
+    int getCurrentTimeout();
+
+    /**
+     * Returns the current retry count (used for logging).
+     */
+    int getCurrentRetryCount();
+
+    /**
+     * Prepares for the next retry by applying a backoff to the timeout.
+     *
+     * @param error The error code of the last attempt.
+     * @throws VolleyError In the event that the retry could not be performed (for example if we ran
+     *                     out of attempts), the passed in error is thrown.
+     */
+    void retry(VolleyError error) throws VolleyError;
+}

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

@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Indicates that the server responded with an error response.
+ */
+@SuppressWarnings("serial")
+public class ServerError extends VolleyError {
+    public ServerError(NetworkResponse networkResponse) {
+        super(networkResponse);
+    }
+
+    public ServerError() {
+        super();
+    }
+}

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

@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Indicates that the connection or the socket timed out.
+ */
+@SuppressWarnings("serial")
+public class TimeoutError extends VolleyError {
+}

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

@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * Exception style class encapsulating Volley errors
+ */
+@SuppressWarnings("serial")
+public class VolleyError extends Exception {
+    public final NetworkResponse networkResponse;
+    private long networkTimeMs;
+
+    public VolleyError() {
+        networkResponse = null;
+    }
+
+    public VolleyError(NetworkResponse response) {
+        networkResponse = response;
+    }
+
+    public VolleyError(String exceptionMessage) {
+        super(exceptionMessage);
+        networkResponse = null;
+    }
+
+    public VolleyError(String exceptionMessage, Throwable reason) {
+        super(exceptionMessage, reason);
+        networkResponse = null;
+    }
+
+    public VolleyError(Throwable cause) {
+        super(cause);
+        networkResponse = null;
+    }
+
+    /* package */
+    void setNetworkTimeMs(long networkTimeMs) {
+        this.networkTimeMs = networkTimeMs;
+    }
+
+    public long getNetworkTimeMs() {
+        return networkTimeMs;
+    }
+}

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

@@ -0,0 +1,191 @@
+/*
+ * 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.SystemClock;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Logging helper class.
+ *
+ * <p>to see Volley logs call:<br>
+ * {@code <android-sdk>/platform-tools/adb shell setprop log.tag.Volley VERBOSE}
+ */
+public class VolleyLog {
+    public static String TAG = "Volley";
+
+    public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+
+    /**
+     * {@link Class#getName()} uses reflection and calling it on a potentially hot code path may
+     * have some cost. To minimize this cost we fetch class name once here and use it later.
+     */
+    private static final String CLASS_NAME = VolleyLog.class.getName();
+
+    /**
+     * Customize the log tag for your application, so that other apps using Volley don't mix their
+     * logs with yours. <br>
+     * Enable the log property for your tag before starting your app: <br>
+     * {@code adb shell setprop log.tag.&lt;tag&gt;}
+     */
+    public static void setTag(String tag) {
+        d("Changing log tag to %s", tag);
+        TAG = tag;
+
+        // Reinitialize the DEBUG "constant"
+        DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+    }
+
+    public static void v(String format, Object... args) {
+        if (DEBUG) {
+            Log.v(TAG, buildMessage(format, args));
+        }
+    }
+
+    public static void d(String format, Object... args) {
+        Log.d(TAG, buildMessage(format, args));
+    }
+
+    public static void e(String format, Object... args) {
+        Log.e(TAG, buildMessage(format, args));
+    }
+
+    public static void e(Throwable tr, String format, Object... args) {
+        Log.e(TAG, buildMessage(format, args), tr);
+    }
+
+    public static void wtf(String format, Object... args) {
+        Log.wtf(TAG, buildMessage(format, args));
+    }
+
+    public static void wtf(Throwable tr, String format, Object... args) {
+        Log.wtf(TAG, buildMessage(format, args), tr);
+    }
+
+    /**
+     * Formats the caller's provided message and prepends useful info like calling thread ID and
+     * method name.
+     */
+    private static String buildMessage(String format, Object... args) {
+        String msg = (args == null) ? format : String.format(Locale.US, format, args);
+        StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();
+
+        String caller = "<unknown>";
+        // Walk up the stack looking for the first caller outside of VolleyLog.
+        // It will be at least two frames up, so start there.
+        for (int i = 2; i < trace.length; i++) {
+            String clazz = trace[i].getClassName();
+            if (!clazz.equals(VolleyLog.CLASS_NAME)) {
+                String callingClass = trace[i].getClassName();
+                callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
+                callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);
+
+                caller = callingClass + "." + trace[i].getMethodName();
+                break;
+            }
+        }
+        return String.format(Locale.US, "[%d] %s: %s", Thread.currentThread().getId(), caller, msg);
+    }
+
+    /**
+     * A simple event log with records containing a name, thread ID, and timestamp.
+     */
+    static class MarkerLog {
+        public static final boolean ENABLED = VolleyLog.DEBUG;
+
+        /**
+         * Minimum duration from first marker to last in an marker log to warrant logging.
+         */
+        private static final long MIN_DURATION_FOR_LOGGING_MS = 0;
+
+        private static class Marker {
+            public final String name;
+            public final long thread;
+            public final long time;
+
+            public Marker(String name, long thread, long time) {
+                this.name = name;
+                this.thread = thread;
+                this.time = time;
+            }
+        }
+
+        private final List<Marker> mMarkers = new ArrayList<>();
+        private boolean mFinished = false;
+
+        /**
+         * Adds a marker to this log with the specified name.
+         */
+        public synchronized void add(String name, long threadId) {
+            if (mFinished) {
+                throw new IllegalStateException("Marker added to finished log");
+            }
+
+            mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime()));
+        }
+
+        /**
+         * Closes the log, dumping it to logcat if the time difference between the first and last
+         * markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}.
+         *
+         * @param header Header string to print above the marker log.
+         */
+        public synchronized void finish(String header) {
+            mFinished = true;
+
+            long duration = getTotalDuration();
+            if (duration <= MIN_DURATION_FOR_LOGGING_MS) {
+                return;
+            }
+
+            long prevTime = mMarkers.get(0).time;
+            d("(%-4d ms) %s", duration, header);
+            for (Marker marker : mMarkers) {
+                long thisTime = marker.time;
+                d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name);
+                prevTime = thisTime;
+            }
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            // Catch requests that have been collected (and hence end-of-lifed)
+            // but had no debugging output printed for them.
+            if (!mFinished) {
+                finish("Request on the loose");
+                e("Marker log finalized without finish() - uncaught exit point for request");
+            }
+        }
+
+        /**
+         * Returns the time difference between the first and last events in this log.
+         */
+        private long getTotalDuration() {
+            if (mMarkers.size() == 0) {
+                return 0;
+            }
+
+            long first = mMarkers.get(0).time;
+            long last = mMarkers.get(mMarkers.size() - 1).time;
+            return last - first;
+        }
+    }
+}

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

@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.toolbox;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+import cn.yyxx.support.volley.source.Header;
+import cn.yyxx.support.volley.source.Request;
+
+import org.apache.http.conn.ConnectTimeoutException;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}.
+ *
+ * <p>{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time,
+ * allowing it to have one implementation based atop {@link BaseHttpStack}.
+ */
+@SuppressWarnings("deprecation")
+class AdaptedHttpStack extends BaseHttpStack {
+
+    private final HttpStack mHttpStack;
+
+    AdaptedHttpStack(HttpStack httpStack) {
+        mHttpStack = httpStack;
+    }
+
+    @Override
+    public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError {
+        org.apache.http.HttpResponse apacheResp;
+        try {
+            apacheResp = mHttpStack.performRequest(request, additionalHeaders);
+        } catch (ConnectTimeoutException e) {
+            // BasicNetwork won't know that this exception should be retried like a timeout, since
+            // it's an Apache-specific error, so wrap it in a standard timeout exception.
+            throw new SocketTimeoutException(e.getMessage());
+        }
+
+        int statusCode = apacheResp.getStatusLine().getStatusCode();
+
+        org.apache.http.Header[] headers = apacheResp.getAllHeaders();
+        List<Header> headerList = new ArrayList<>(headers.length);
+        for (org.apache.http.Header header : headers) {
+            headerList.add(new Header(header.getName(), header.getValue()));
+        }
+
+        if (apacheResp.getEntity() == null) {
+            return new HttpResponse(statusCode, headerList);
+        }
+
+        long contentLength = apacheResp.getEntity().getContentLength();
+        if ((int) contentLength != contentLength) {
+            throw new IOException("Response too large: " + contentLength);
+        }
+
+        return new HttpResponse(
+                statusCode,
+                headerList,
+                (int) apacheResp.getEntity().getContentLength(),
+                apacheResp.getEntity().getContent());
+    }
+}

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

@@ -0,0 +1,128 @@
+/*
+ * 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.toolbox;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+
+/**
+ * An Authenticator that uses {@link AccountManager} to get auth tokens of a specified type for a
+ * specified account.
+ */
+// TODO: Update this to account for runtime permissions
+@SuppressLint("MissingPermission")
+public class AndroidAuthenticator implements Authenticator {
+    private final AccountManager mAccountManager;
+    private final Account mAccount;
+    private final String mAuthTokenType;
+    private final boolean mNotifyAuthFailure;
+
+    /**
+     * Creates a new authenticator.
+     *
+     * @param context       Context for accessing AccountManager
+     * @param account       Account to authenticate as
+     * @param authTokenType Auth token type passed to AccountManager
+     */
+    public AndroidAuthenticator(Context context, Account account, String authTokenType) {
+        this(context, account, authTokenType, /* notifyAuthFailure= */ false);
+    }
+
+    /**
+     * Creates a new authenticator.
+     *
+     * @param context           Context for accessing AccountManager
+     * @param account           Account to authenticate as
+     * @param authTokenType     Auth token type passed to AccountManager
+     * @param notifyAuthFailure Whether to raise a notification upon auth failure
+     */
+    public AndroidAuthenticator(
+            Context context, Account account, String authTokenType, boolean notifyAuthFailure) {
+        this(AccountManager.get(context), account, authTokenType, notifyAuthFailure);
+    }
+
+    @VisibleForTesting
+    AndroidAuthenticator(
+            AccountManager accountManager,
+            Account account,
+            String authTokenType,
+            boolean notifyAuthFailure) {
+        mAccountManager = accountManager;
+        mAccount = account;
+        mAuthTokenType = authTokenType;
+        mNotifyAuthFailure = notifyAuthFailure;
+    }
+
+    /**
+     * Returns the Account being used by this authenticator.
+     */
+    public Account getAccount() {
+        return mAccount;
+    }
+
+    /**
+     * Returns the Auth Token Type used by this authenticator.
+     */
+    public String getAuthTokenType() {
+        return mAuthTokenType;
+    }
+
+    // TODO: Figure out what to do about notifyAuthFailure
+    @SuppressWarnings("deprecation")
+    @Override
+    public String getAuthToken() throws AuthFailureError {
+        AccountManagerFuture<Bundle> future =
+                mAccountManager.getAuthToken(
+                        mAccount,
+                        mAuthTokenType,
+                        mNotifyAuthFailure,
+                        /* callback= */ null,
+                        /* handler= */ null);
+        Bundle result;
+        try {
+            result = future.getResult();
+        } catch (Exception e) {
+            throw new AuthFailureError("Error while retrieving auth token", e);
+        }
+        String authToken = null;
+        if (future.isDone() && !future.isCancelled()) {
+            if (result.containsKey(AccountManager.KEY_INTENT)) {
+                Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+                throw new AuthFailureError(intent);
+            }
+            authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+        }
+        if (authToken == null) {
+            throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType);
+        }
+
+        return authToken;
+    }
+
+    @Override
+    public void invalidateAuthToken(String authToken) {
+        mAccountManager.invalidateAuthToken(mAccount.type, authToken);
+    }
+}

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

@@ -0,0 +1,36 @@
+/*
+ * 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.toolbox;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+
+/**
+ * An interface for interacting with auth tokens.
+ */
+public interface Authenticator {
+    /**
+     * Synchronously retrieves an auth token.
+     *
+     * @throws AuthFailureError If authentication did not succeed
+     */
+    String getAuthToken() throws AuthFailureError;
+
+    /**
+     * Invalidates the provided auth token.
+     */
+    void invalidateAuthToken(String authToken);
+}

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

@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 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.toolbox;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+import cn.yyxx.support.volley.source.Header;
+import cn.yyxx.support.volley.source.Request;
+
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An HTTP stack abstraction.
+ */
+@SuppressWarnings("deprecation")
+// for HttpStack
+public abstract class BaseHttpStack implements HttpStack {
+
+    /**
+     * Performs an HTTP request with the given parameters.
+     *
+     * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+     * and the Content-Type header is set to request.getPostBodyContentType().
+     *
+     * @param request           the request to perform
+     * @param additionalHeaders additional headers to be sent together with {@link
+     *                          Request#getHeaders()}
+     * @return the {@link HttpResponse}
+     * @throws SocketTimeoutException if the request times out
+     * @throws IOException            if another I/O error occurs during the request
+     * @throws AuthFailureError       if an authentication failure occurs during the request
+     */
+    public abstract HttpResponse executeRequest(
+            Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError;
+
+    /**
+     * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated
+     * Apache HTTP library. Nothing in Volley's own source calls this method. However, since
+     * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation
+     * in case legacy client apps are dependent on that field. This method may be removed in a
+     * future release of Volley.
+     */
+    @Deprecated
+    @Override
+    public final org.apache.http.HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError {
+        HttpResponse response = executeRequest(request, additionalHeaders);
+
+        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+        StatusLine statusLine = new BasicStatusLine(protocolVersion, response.getStatusCode(), /* reasonPhrase= */ "");
+        BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine);
+
+        List<org.apache.http.Header> headers = new ArrayList<>();
+        for (Header header : response.getHeaders()) {
+            headers.add(new BasicHeader(header.getName(), header.getValue()));
+        }
+        apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[0]));
+
+        InputStream responseStream = response.getContent();
+        if (responseStream != null) {
+            BasicHttpEntity entity = new BasicHttpEntity();
+            entity.setContent(responseStream);
+            entity.setContentLength(response.getContentLength());
+            apacheResponse.setEntity(entity);
+        }
+
+        return apacheResponse;
+    }
+}

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

@@ -0,0 +1,380 @@
+/*
+ * 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.toolbox;
+
+import android.os.SystemClock;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+import cn.yyxx.support.volley.source.Cache;
+import cn.yyxx.support.volley.source.Cache.Entry;
+import cn.yyxx.support.volley.source.ClientError;
+import cn.yyxx.support.volley.source.Header;
+import cn.yyxx.support.volley.source.Network;
+import cn.yyxx.support.volley.source.NetworkError;
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.NoConnectionError;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.RetryPolicy;
+import cn.yyxx.support.volley.source.ServerError;
+import cn.yyxx.support.volley.source.TimeoutError;
+import cn.yyxx.support.volley.source.VolleyError;
+import cn.yyxx.support.volley.source.VolleyLog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * A network performing Volley requests over an {@link HttpStack}.
+ */
+public class BasicNetwork implements Network {
+    protected static final boolean DEBUG = VolleyLog.DEBUG;
+
+    private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
+
+    private static final int DEFAULT_POOL_SIZE = 4096;
+
+    /**
+     * @deprecated Should never have been exposed in the API. This field may be removed in a future
+     * release of Volley.
+     */
+    @Deprecated
+    protected final HttpStack mHttpStack;
+
+    private final BaseHttpStack mBaseHttpStack;
+
+    protected final ByteArrayPool mPool;
+
+    /**
+     * @param httpStack HTTP stack to be used
+     * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache
+     * HTTP. This method may be removed in a future release of Volley.
+     */
+    @Deprecated
+    public BasicNetwork(HttpStack httpStack) {
+        // If a pool isn't passed in, then build a small default pool that will give us a lot of
+        // benefit and not use too much memory.
+        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+    }
+
+    /**
+     * @param httpStack HTTP stack to be used
+     * @param pool      a buffer pool that improves GC performance in copy operations
+     * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid
+     * depending on Apache HTTP. This method may be removed in a future release of Volley.
+     */
+    @Deprecated
+    public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
+        mHttpStack = httpStack;
+        mBaseHttpStack = new AdaptedHttpStack(httpStack);
+        mPool = pool;
+    }
+
+    /**
+     * @param httpStack HTTP stack to be used
+     */
+    public BasicNetwork(BaseHttpStack httpStack) {
+        // If a pool isn't passed in, then build a small default pool that will give us a lot of
+        // benefit and not use too much memory.
+        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+    }
+
+    /**
+     * @param httpStack HTTP stack to be used
+     * @param pool      a buffer pool that improves GC performance in copy operations
+     */
+    public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) {
+        mBaseHttpStack = httpStack;
+        // Populate mHttpStack for backwards compatibility, since it is a protected field. However,
+        // we won't use it directly here, so clients which don't access it directly won't need to
+        // depend on Apache HTTP.
+        mHttpStack = httpStack;
+        mPool = pool;
+    }
+
+    @Override
+    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
+        long requestStart = SystemClock.elapsedRealtime();
+        while (true) {
+            HttpResponse httpResponse = null;
+            byte[] responseContents = null;
+            List<Header> responseHeaders = Collections.emptyList();
+            try {
+                // Gather headers.
+                Map<String, String> additionalRequestHeaders =
+                        getCacheHeaders(request.getCacheEntry());
+                httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
+                int statusCode = httpResponse.getStatusCode();
+
+                responseHeaders = httpResponse.getHeaders();
+                // Handle cache validation.
+                if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
+                    Entry entry = request.getCacheEntry();
+                    if (entry == null) {
+                        return new NetworkResponse(
+                                HttpURLConnection.HTTP_NOT_MODIFIED,
+                                /* data= */ null,
+                                /* notModified= */ true,
+                                SystemClock.elapsedRealtime() - requestStart,
+                                responseHeaders);
+                    }
+                    // Combine cached and response headers so the response will be complete.
+                    List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
+                    return new NetworkResponse(
+                            HttpURLConnection.HTTP_NOT_MODIFIED,
+                            entry.data,
+                            /* notModified= */ true,
+                            SystemClock.elapsedRealtime() - requestStart,
+                            combinedHeaders);
+                }
+
+                // Some responses such as 204s do not have content.  We must check.
+                InputStream inputStream = httpResponse.getContent();
+                if (inputStream != null) {
+                    responseContents =
+                            inputStreamToBytes(inputStream, httpResponse.getContentLength());
+                } else {
+                    // Add 0 byte response as a way of honestly representing a
+                    // no-content request.
+                    responseContents = new byte[0];
+                }
+
+                // if the request is slow, log it.
+                long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
+                logSlowRequests(requestLifetime, request, responseContents, statusCode);
+
+                if (statusCode < 200 || statusCode > 299) {
+                    throw new IOException();
+                }
+                return new NetworkResponse(
+                        statusCode,
+                        responseContents,
+                        /* notModified= */ false,
+                        SystemClock.elapsedRealtime() - requestStart,
+                        responseHeaders);
+            } catch (SocketTimeoutException e) {
+                attemptRetryOnException("socket", request, new TimeoutError());
+            } catch (MalformedURLException e) {
+                throw new RuntimeException("Bad URL " + request.getUrl(), e);
+            } catch (IOException e) {
+                int statusCode;
+                if (httpResponse != null) {
+                    statusCode = httpResponse.getStatusCode();
+                } else {
+                    throw new NoConnectionError(e);
+                }
+                VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
+                NetworkResponse networkResponse;
+                if (responseContents != null) {
+                    networkResponse =
+                            new NetworkResponse(
+                                    statusCode,
+                                    responseContents,
+                                    /* notModified= */ false,
+                                    SystemClock.elapsedRealtime() - requestStart,
+                                    responseHeaders);
+                    if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
+                            || statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
+                        attemptRetryOnException(
+                                "auth", request, new AuthFailureError(networkResponse));
+                    } else if (statusCode >= 400 && statusCode <= 499) {
+                        // Don't retry other client errors.
+                        throw new ClientError(networkResponse);
+                    } else if (statusCode >= 500 && statusCode <= 599) {
+                        if (request.shouldRetryServerErrors()) {
+                            attemptRetryOnException(
+                                    "server", request, new ServerError(networkResponse));
+                        } else {
+                            throw new ServerError(networkResponse);
+                        }
+                    } else {
+                        // 3xx? No reason to retry.
+                        throw new ServerError(networkResponse);
+                    }
+                } else {
+                    attemptRetryOnException("network", request, new NetworkError());
+                }
+            }
+        }
+    }
+
+    /**
+     * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
+     */
+    private void logSlowRequests(
+            long requestLifetime, Request<?> request, byte[] responseContents, int statusCode) {
+        if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
+            VolleyLog.d(
+                    "HTTP response for request=<%s> [lifetime=%d], [size=%s], "
+                            + "[rc=%d], [retryCount=%s]",
+                    request,
+                    requestLifetime,
+                    responseContents != null ? responseContents.length : "null",
+                    statusCode,
+                    request.getRetryPolicy().getCurrentRetryCount());
+        }
+    }
+
+    /**
+     * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
+     * request's retry policy, a timeout exception is thrown.
+     *
+     * @param request The request to use.
+     */
+    private static void attemptRetryOnException(
+            String logPrefix, Request<?> request, VolleyError exception) throws VolleyError {
+        RetryPolicy retryPolicy = request.getRetryPolicy();
+        int oldTimeout = request.getTimeoutMs();
+
+        try {
+            retryPolicy.retry(exception);
+        } catch (VolleyError e) {
+            request.addMarker(
+                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
+            throw e;
+        }
+        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
+    }
+
+    private Map<String, String> getCacheHeaders(Cache.Entry entry) {
+        // If there's no cache entry, we're done.
+        if (entry == null) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, String> headers = new HashMap<>();
+
+        if (entry.etag != null) {
+            headers.put("If-None-Match", entry.etag);
+        }
+
+        if (entry.lastModified > 0) {
+            headers.put(
+                    "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
+        }
+
+        return headers;
+    }
+
+    protected void logError(String what, String url, long start) {
+        long now = SystemClock.elapsedRealtime();
+        VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
+    }
+
+    /**
+     * Reads the contents of an InputStream into a byte[].
+     */
+    private byte[] inputStreamToBytes(InputStream in, int contentLength)
+            throws IOException, ServerError {
+        PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, contentLength);
+        byte[] buffer = null;
+        try {
+            if (in == null) {
+                throw new ServerError();
+            }
+            buffer = mPool.getBuf(1024);
+            int count;
+            while ((count = in.read(buffer)) != -1) {
+                bytes.write(buffer, 0, count);
+            }
+            return bytes.toByteArray();
+        } finally {
+            try {
+                // Close the InputStream and release the resources by "consuming the content".
+                if (in != null) {
+                    in.close();
+                }
+            } catch (IOException e) {
+                // This can happen if there was an exception above that left the stream in
+                // an invalid state.
+                VolleyLog.v("Error occurred when closing InputStream");
+            }
+            mPool.returnBuf(buffer);
+            bytes.close();
+        }
+    }
+
+    /**
+     * Converts Headers[] to Map&lt;String, String&gt;.
+     *
+     * @deprecated Should never have been exposed in the API. This method may be removed in a future
+     * release of Volley.
+     */
+    @Deprecated
+    protected static Map<String, String> convertHeaders(Header[] headers) {
+        Map<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (int i = 0; i < headers.length; i++) {
+            result.put(headers[i].getName(), headers[i].getValue());
+        }
+        return result;
+    }
+
+    /**
+     * Combine cache headers with network response headers for an HTTP 304 response.
+     *
+     * <p>An HTTP 304 response does not have all header fields. We have to use the header fields
+     * from the cache entry plus the new ones from the response. See also:
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+     *
+     * @param responseHeaders Headers from the network response.
+     * @param entry           The cached response.
+     * @return The combined list of headers.
+     */
+    private static List<Header> combineHeaders(List<Header> responseHeaders, Entry entry) {
+        // First, create a case-insensitive set of header names from the network
+        // response.
+        Set<String> headerNamesFromNetworkResponse = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        if (!responseHeaders.isEmpty()) {
+            for (Header header : responseHeaders) {
+                headerNamesFromNetworkResponse.add(header.getName());
+            }
+        }
+
+        // Second, add headers from the cache entry to the network response as long as
+        // they didn't appear in the network response, which should take precedence.
+        List<Header> combinedHeaders = new ArrayList<>(responseHeaders);
+        if (entry.allResponseHeaders != null) {
+            if (!entry.allResponseHeaders.isEmpty()) {
+                for (Header header : entry.allResponseHeaders) {
+                    if (!headerNamesFromNetworkResponse.contains(header.getName())) {
+                        combinedHeaders.add(header);
+                    }
+                }
+            }
+        } else {
+            // Legacy caches only have entry.responseHeaders.
+            if (!entry.responseHeaders.isEmpty()) {
+                for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) {
+                    if (!headerNamesFromNetworkResponse.contains(header.getKey())) {
+                        combinedHeaders.add(new Header(header.getKey(), header.getValue()));
+                    }
+                }
+            }
+        }
+        return combinedHeaders;
+    }
+}

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

@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2012 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.toolbox;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to supply
+ * those buffers to consumers who need to use them for a short period of time and then dispose of
+ * them. Simply creating and disposing such buffers in the conventional manner can considerable heap
+ * churn and garbage collection delays on Android, which lacks good management of short-lived heap
+ * objects. It may be advantageous to trade off some memory in the form of a permanently allocated
+ * pool of buffers in order to gain heap performance improvements; that is what this class does.
+ *
+ * <p>A good candidate user for this class is something like an I/O system that uses large temporary
+ * <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants the
+ * buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks off
+ * of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into account
+ * and also to maximize the odds of being able to reuse a recycled buffer, this class is free to
+ * return buffers larger than the requested size. The caller needs to be able to gracefully deal
+ * with getting buffers any size over the minimum.
+ *
+ * <p>If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
+ * class will allocate a new buffer and return it.
+ *
+ * <p>This class has no special ownership of buffers it creates; the caller is free to take a buffer
+ * it receives from this pool, use it permanently, and never return it to the pool; additionally, it
+ * is not harmful to return to this pool a buffer that was allocated elsewhere, provided there are
+ * no other lingering references to it.
+ *
+ * <p>This class ensures that the total size of the buffers in its recycling pool never exceeds a
+ * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
+ * least-recently-used buffers are disposed.
+ */
+public class ByteArrayPool {
+    /**
+     * The buffer pool, arranged both by last use and by buffer size
+     */
+    private final List<byte[]> mBuffersByLastUse = new ArrayList<>();
+
+    private final List<byte[]> mBuffersBySize = new ArrayList<>(64);
+
+    /**
+     * The total size of the buffers in the pool
+     */
+    private int mCurrentSize = 0;
+
+    /**
+     * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
+     * under this limit.
+     */
+    private final int mSizeLimit;
+
+    /**
+     * Compares buffers by size
+     */
+    protected static final Comparator<byte[]> BUF_COMPARATOR =
+            new Comparator<byte[]>() {
+                @Override
+                public int compare(byte[] lhs, byte[] rhs) {
+                    return lhs.length - rhs.length;
+                }
+            };
+
+    /**
+     * @param sizeLimit the maximum size of the pool, in bytes
+     */
+    public ByteArrayPool(int sizeLimit) {
+        mSizeLimit = sizeLimit;
+    }
+
+    /**
+     * Returns a buffer from the pool if one is available in the requested size, or allocates a new
+     * one if a pooled one is not available.
+     *
+     * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
+     *            larger.
+     * @return a byte[] buffer is always returned.
+     */
+    public synchronized byte[] getBuf(int len) {
+        for (int i = 0; i < mBuffersBySize.size(); i++) {
+            byte[] buf = mBuffersBySize.get(i);
+            if (buf.length >= len) {
+                mCurrentSize -= buf.length;
+                mBuffersBySize.remove(i);
+                mBuffersByLastUse.remove(buf);
+                return buf;
+            }
+        }
+        return new byte[len];
+    }
+
+    /**
+     * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
+     * size.
+     *
+     * @param buf the buffer to return to the pool.
+     */
+    public synchronized void returnBuf(byte[] buf) {
+        if (buf == null || buf.length > mSizeLimit) {
+            return;
+        }
+        mBuffersByLastUse.add(buf);
+        int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
+        if (pos < 0) {
+            pos = -pos - 1;
+        }
+        mBuffersBySize.add(pos, buf);
+        mCurrentSize += buf.length;
+        trim();
+    }
+
+    /**
+     * Removes buffers from the pool until it is under its size limit.
+     */
+    private synchronized void trim() {
+        while (mCurrentSize > mSizeLimit) {
+            byte[] buf = mBuffersByLastUse.remove(0);
+            mBuffersBySize.remove(buf);
+            mCurrentSize -= buf.length;
+        }
+    }
+}

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

@@ -0,0 +1,70 @@
+/*
+ * 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.toolbox;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import cn.yyxx.support.volley.source.Cache;
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Response;
+
+/**
+ * A synthetic request used for clearing the cache.
+ */
+public class ClearCacheRequest extends Request<Object> {
+    private final Cache mCache;
+    private final Runnable mCallback;
+
+    /**
+     * Creates a synthetic request for clearing the cache.
+     *
+     * @param cache    Cache to clear
+     * @param callback Callback to make on the main thread once the cache is clear, or null for none
+     */
+    public ClearCacheRequest(Cache cache, Runnable callback) {
+        super(Method.GET, null, null);
+        mCache = cache;
+        mCallback = callback;
+    }
+
+    @Override
+    public boolean isCanceled() {
+        // This is a little bit of a hack, but hey, why not.
+        mCache.clear();
+        if (mCallback != null) {
+            Handler handler = new Handler(Looper.getMainLooper());
+            handler.postAtFrontOfQueue(mCallback);
+        }
+        return true;
+    }
+
+    @Override
+    public Priority getPriority() {
+        return Priority.IMMEDIATE;
+    }
+
+    @Override
+    protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+        return null;
+    }
+
+    @Override
+    protected void deliverResponse(Object response) {
+    }
+}

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

@@ -0,0 +1,683 @@
+/*
+ * 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.toolbox;
+
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+
+import cn.yyxx.support.volley.source.Cache;
+import cn.yyxx.support.volley.source.Header;
+import cn.yyxx.support.volley.source.VolleyLog;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Cache implementation that caches files directly onto the hard disk in the specified directory.
+ * The default disk usage size is 5MB, but is configurable.
+ *
+ * <p>This cache supports the {@link Entry#allResponseHeaders} headers field.
+ */
+public class DiskBasedCache implements Cache {
+
+    /**
+     * Map of the Key, CacheHeader pairs
+     */
+    private final Map<String, CacheHeader> mEntries = new LinkedHashMap<>(16, .75f, true);
+
+    /**
+     * Total amount of space currently used by the cache in bytes.
+     */
+    private long mTotalSize = 0;
+
+    /**
+     * The root directory to use for the cache.
+     */
+    private final File mRootDirectory;
+
+    /**
+     * The maximum size of the cache in bytes.
+     */
+    private final int mMaxCacheSizeInBytes;
+
+    /**
+     * Default maximum disk usage in bytes.
+     */
+    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
+
+    /**
+     * High water mark percentage for the cache
+     */
+    @VisibleForTesting
+    static final float HYSTERESIS_FACTOR = 0.9f;
+
+    /**
+     * Magic number for current version of cache file format.
+     */
+    private static final int CACHE_MAGIC = 0x20150306;
+
+    /**
+     * Constructs an instance of the DiskBasedCache at the specified directory.
+     *
+     * @param rootDirectory       The root directory of the cache.
+     * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may
+     *                            briefly exceed this size on disk when writing a new entry that pushes it over the limit
+     *                            until the ensuing pruning completes.
+     */
+    public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
+        mRootDirectory = rootDirectory;
+        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
+    }
+
+    /**
+     * Constructs an instance of the DiskBasedCache at the specified directory using the default
+     * maximum cache size of 5MB.
+     *
+     * @param rootDirectory The root directory of the cache.
+     */
+    public DiskBasedCache(File rootDirectory) {
+        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
+    }
+
+    /**
+     * Clears the cache. Deletes all cached files from disk.
+     */
+    @Override
+    public synchronized void clear() {
+        File[] files = mRootDirectory.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                file.delete();
+            }
+        }
+        mEntries.clear();
+        mTotalSize = 0;
+        VolleyLog.d("Cache cleared.");
+    }
+
+    /**
+     * Returns the cache entry with the specified key if it exists, null otherwise.
+     */
+    @Override
+    public synchronized Entry get(String key) {
+        CacheHeader entry = mEntries.get(key);
+        // if the entry does not exist, return.
+        if (entry == null) {
+            return null;
+        }
+        File file = getFileForKey(key);
+        try {
+            CountingInputStream cis =
+                    new CountingInputStream(
+                            new BufferedInputStream(createInputStream(file)), file.length());
+            try {
+                CacheHeader entryOnDisk = CacheHeader.readHeader(cis);
+                if (!TextUtils.equals(key, entryOnDisk.key)) {
+                    // File was shared by two keys and now holds data for a different entry!
+                    VolleyLog.d(
+                            "%s: key=%s, found=%s", file.getAbsolutePath(), key, entryOnDisk.key);
+                    // Remove key whose contents on disk have been replaced.
+                    removeEntry(key);
+                    return null;
+                }
+                byte[] data = streamToBytes(cis, cis.bytesRemaining());
+                return entry.toCacheEntry(data);
+            } finally {
+                // Any IOException thrown here is handled by the below catch block by design.
+                //noinspection ThrowFromFinallyBlock
+                cis.close();
+            }
+        } catch (IOException e) {
+            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
+            remove(key);
+            return null;
+        }
+    }
+
+    /**
+     * Initializes the DiskBasedCache by scanning for all files currently in the specified root
+     * directory. Creates the root directory if necessary.
+     */
+    @Override
+    public synchronized void initialize() {
+        if (!mRootDirectory.exists()) {
+            if (!mRootDirectory.mkdirs()) {
+                VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
+            }
+            return;
+        }
+        File[] files = mRootDirectory.listFiles();
+        if (files == null) {
+            return;
+        }
+        for (File file : files) {
+            try {
+                long entrySize = file.length();
+                CountingInputStream cis =
+                        new CountingInputStream(
+                                new BufferedInputStream(createInputStream(file)), entrySize);
+                try {
+                    CacheHeader entry = CacheHeader.readHeader(cis);
+                    entry.size = entrySize;
+                    putEntry(entry.key, entry);
+                } finally {
+                    // Any IOException thrown here is handled by the below catch block by design.
+                    //noinspection ThrowFromFinallyBlock
+                    cis.close();
+                }
+            } catch (IOException e) {
+                //noinspection ResultOfMethodCallIgnored
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Invalidates an entry in the cache.
+     *
+     * @param key        Cache key
+     * @param fullExpire True to fully expire the entry, false to soft expire
+     */
+    @Override
+    public synchronized void invalidate(String key, boolean fullExpire) {
+        Entry entry = get(key);
+        if (entry != null) {
+            entry.softTtl = 0;
+            if (fullExpire) {
+                entry.ttl = 0;
+            }
+            put(key, entry);
+        }
+    }
+
+    /**
+     * Puts the entry with the specified key into the cache.
+     */
+    @Override
+    public synchronized void put(String key, Entry entry) {
+        // If adding this entry would trigger a prune, but pruning would cause the new entry to be
+        // deleted, then skip writing the entry in the first place, as this is just churn.
+        // Note that we don't include the cache header overhead in this calculation for simplicity,
+        // so putting entries which are just below the threshold may still cause this churn.
+        if (mTotalSize + entry.data.length > mMaxCacheSizeInBytes
+                && entry.data.length > mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
+            return;
+        }
+        File file = getFileForKey(key);
+        try {
+            BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
+            CacheHeader e = new CacheHeader(key, entry);
+            boolean success = e.writeHeader(fos);
+            if (!success) {
+                fos.close();
+                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
+                throw new IOException();
+            }
+            fos.write(entry.data);
+            fos.close();
+            e.size = file.length();
+            putEntry(key, e);
+            pruneIfNeeded();
+            return;
+        } catch (IOException e) {
+        }
+        boolean deleted = file.delete();
+        if (!deleted) {
+            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
+        }
+    }
+
+    /**
+     * Removes the specified key from the cache if it exists.
+     */
+    @Override
+    public synchronized void remove(String key) {
+        boolean deleted = getFileForKey(key).delete();
+        removeEntry(key);
+        if (!deleted) {
+            VolleyLog.d(
+                    "Could not delete cache entry for key=%s, filename=%s",
+                    key, getFilenameForKey(key));
+        }
+    }
+
+    /**
+     * Creates a pseudo-unique filename for the specified cache key.
+     *
+     * @param key The key to generate a file name for.
+     * @return A pseudo-unique filename.
+     */
+    private String getFilenameForKey(String key) {
+        int firstHalfLength = key.length() / 2;
+        String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
+        localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
+        return localFilename;
+    }
+
+    /**
+     * Returns a file object for the given cache key.
+     */
+    public File getFileForKey(String key) {
+        return new File(mRootDirectory, getFilenameForKey(key));
+    }
+
+    /**
+     * Prunes the cache to fit the maximum size.
+     */
+    private void pruneIfNeeded() {
+        if (mTotalSize < mMaxCacheSizeInBytes) {
+            return;
+        }
+        if (VolleyLog.DEBUG) {
+            VolleyLog.v("Pruning old cache entries.");
+        }
+
+        long before = mTotalSize;
+        int prunedFiles = 0;
+        long startTime = SystemClock.elapsedRealtime();
+
+        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<String, CacheHeader> entry = iterator.next();
+            CacheHeader e = entry.getValue();
+            boolean deleted = getFileForKey(e.key).delete();
+            if (deleted) {
+                mTotalSize -= e.size;
+            } else {
+                VolleyLog.d(
+                        "Could not delete cache entry for key=%s, filename=%s",
+                        e.key, getFilenameForKey(e.key));
+            }
+            iterator.remove();
+            prunedFiles++;
+
+            if (mTotalSize < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
+                break;
+            }
+        }
+
+        if (VolleyLog.DEBUG) {
+            VolleyLog.v(
+                    "pruned %d files, %d bytes, %d ms",
+                    prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
+        }
+    }
+
+    /**
+     * Puts the entry with the specified key into the cache.
+     *
+     * @param key   The key to identify the entry by.
+     * @param entry The entry to cache.
+     */
+    private void putEntry(String key, CacheHeader entry) {
+        if (!mEntries.containsKey(key)) {
+            mTotalSize += entry.size;
+        } else {
+            CacheHeader oldEntry = mEntries.get(key);
+            mTotalSize += (entry.size - oldEntry.size);
+        }
+        mEntries.put(key, entry);
+    }
+
+    /**
+     * Removes the entry identified by 'key' from the cache.
+     */
+    private void removeEntry(String key) {
+        CacheHeader removed = mEntries.remove(key);
+        if (removed != null) {
+            mTotalSize -= removed.size;
+        }
+    }
+
+    /**
+     * Reads length bytes from CountingInputStream into byte array.
+     *
+     * @param cis    input stream
+     * @param length number of bytes to read
+     * @throws IOException if fails to read all bytes
+     */
+    @VisibleForTesting
+    static byte[] streamToBytes(CountingInputStream cis, long length) throws IOException {
+        long maxLength = cis.bytesRemaining();
+        // Length cannot be negative or greater than bytes remaining, and must not overflow int.
+        if (length < 0 || length > maxLength || (int) length != length) {
+            throw new IOException("streamToBytes length=" + length + ", maxLength=" + maxLength);
+        }
+        byte[] bytes = new byte[(int) length];
+        new DataInputStream(cis).readFully(bytes);
+        return bytes;
+    }
+
+    @VisibleForTesting
+    InputStream createInputStream(File file) throws FileNotFoundException {
+        return new FileInputStream(file);
+    }
+
+    @VisibleForTesting
+    OutputStream createOutputStream(File file) throws FileNotFoundException {
+        return new FileOutputStream(file);
+    }
+
+    /**
+     * Handles holding onto the cache headers for an entry.
+     */
+    @VisibleForTesting
+    static class CacheHeader {
+        /**
+         * The size of the data identified by this CacheHeader on disk (both header and data).
+         *
+         * <p>Must be set by the caller after it has been calculated.
+         *
+         * <p>This is not serialized to disk.
+         */
+        long size;
+
+        /**
+         * The key that identifies the cache entry.
+         */
+        final String key;
+
+        /**
+         * ETag for cache coherence.
+         */
+        final String etag;
+
+        /**
+         * Date of this response as reported by the server.
+         */
+        final long serverDate;
+
+        /**
+         * The last modified date for the requested object.
+         */
+        final long lastModified;
+
+        /**
+         * TTL for this record.
+         */
+        final long ttl;
+
+        /**
+         * Soft TTL for this record.
+         */
+        final long softTtl;
+
+        /**
+         * Headers from the response resulting in this cache entry.
+         */
+        final List<Header> allResponseHeaders;
+
+        private CacheHeader(
+                String key,
+                String etag,
+                long serverDate,
+                long lastModified,
+                long ttl,
+                long softTtl,
+                List<Header> allResponseHeaders) {
+            this.key = key;
+            this.etag = "".equals(etag) ? null : etag;
+            this.serverDate = serverDate;
+            this.lastModified = lastModified;
+            this.ttl = ttl;
+            this.softTtl = softTtl;
+            this.allResponseHeaders = allResponseHeaders;
+        }
+
+        /**
+         * Instantiates a new CacheHeader object.
+         *
+         * @param key   The key that identifies the cache entry
+         * @param entry The cache entry.
+         */
+        CacheHeader(String key, Entry entry) {
+            this(
+                    key,
+                    entry.etag,
+                    entry.serverDate,
+                    entry.lastModified,
+                    entry.ttl,
+                    entry.softTtl,
+                    getAllResponseHeaders(entry));
+        }
+
+        private static List<Header> getAllResponseHeaders(Entry entry) {
+            // If the entry contains all the response headers, use that field directly.
+            if (entry.allResponseHeaders != null) {
+                return entry.allResponseHeaders;
+            }
+
+            // Legacy fallback - copy headers from the map.
+            return HttpHeaderParser.toAllHeaderList(entry.responseHeaders);
+        }
+
+        /**
+         * Reads the header from a CountingInputStream and returns a CacheHeader object.
+         *
+         * @param is The InputStream to read from.
+         * @throws IOException if fails to read header
+         */
+        static CacheHeader readHeader(CountingInputStream is) throws IOException {
+            int magic = readInt(is);
+            if (magic != CACHE_MAGIC) {
+                // don't bother deleting, it'll get pruned eventually
+                throw new IOException();
+            }
+            String key = readString(is);
+            String etag = readString(is);
+            long serverDate = readLong(is);
+            long lastModified = readLong(is);
+            long ttl = readLong(is);
+            long softTtl = readLong(is);
+            List<Header> allResponseHeaders = readHeaderList(is);
+            return new CacheHeader(
+                    key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders);
+        }
+
+        /**
+         * Creates a cache entry for the specified data.
+         */
+        Entry toCacheEntry(byte[] data) {
+            Entry e = new Entry();
+            e.data = data;
+            e.etag = etag;
+            e.serverDate = serverDate;
+            e.lastModified = lastModified;
+            e.ttl = ttl;
+            e.softTtl = softTtl;
+            e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders);
+            e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders);
+            return e;
+        }
+
+        /**
+         * Writes the contents of this CacheHeader to the specified OutputStream.
+         */
+        boolean writeHeader(OutputStream os) {
+            try {
+                writeInt(os, CACHE_MAGIC);
+                writeString(os, key);
+                writeString(os, etag == null ? "" : etag);
+                writeLong(os, serverDate);
+                writeLong(os, lastModified);
+                writeLong(os, ttl);
+                writeLong(os, softTtl);
+                writeHeaderList(allResponseHeaders, os);
+                os.flush();
+                return true;
+            } catch (IOException e) {
+                VolleyLog.d("%s", e.toString());
+                return false;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static class CountingInputStream extends FilterInputStream {
+        private final long length;
+        private long bytesRead;
+
+        CountingInputStream(InputStream in, long length) {
+            super(in);
+            this.length = length;
+        }
+
+        @Override
+        public int read() throws IOException {
+            int result = super.read();
+            if (result != -1) {
+                bytesRead++;
+            }
+            return result;
+        }
+
+        @Override
+        public int read(byte[] buffer, int offset, int count) throws IOException {
+            int result = super.read(buffer, offset, count);
+            if (result != -1) {
+                bytesRead += result;
+            }
+            return result;
+        }
+
+        @VisibleForTesting
+        long bytesRead() {
+            return bytesRead;
+        }
+
+        long bytesRemaining() {
+            return length - bytesRead;
+        }
+    }
+
+    /*
+     * Homebrewed simple serialization system used for reading and writing cache
+     * headers on disk. Once upon a time, this used the standard Java
+     * Object{Input,Output}Stream, but the default implementation relies heavily
+     * on reflection (even for standard types) and generates a ton of garbage.
+     *
+     * TODO: Replace by standard DataInput and DataOutput in next cache version.
+     */
+
+    /**
+     * Simple wrapper around {@link InputStream#read()} that throws EOFException instead of
+     * returning -1.
+     */
+    private static int read(InputStream is) throws IOException {
+        int b = is.read();
+        if (b == -1) {
+            throw new EOFException();
+        }
+        return b;
+    }
+
+    static void writeInt(OutputStream os, int n) throws IOException {
+        os.write((n >> 0) & 0xff);
+        os.write((n >> 8) & 0xff);
+        os.write((n >> 16) & 0xff);
+        os.write((n >> 24) & 0xff);
+    }
+
+    static int readInt(InputStream is) throws IOException {
+        int n = 0;
+        n |= (read(is) << 0);
+        n |= (read(is) << 8);
+        n |= (read(is) << 16);
+        n |= (read(is) << 24);
+        return n;
+    }
+
+    static void writeLong(OutputStream os, long n) throws IOException {
+        os.write((byte) (n >>> 0));
+        os.write((byte) (n >>> 8));
+        os.write((byte) (n >>> 16));
+        os.write((byte) (n >>> 24));
+        os.write((byte) (n >>> 32));
+        os.write((byte) (n >>> 40));
+        os.write((byte) (n >>> 48));
+        os.write((byte) (n >>> 56));
+    }
+
+    static long readLong(InputStream is) throws IOException {
+        long n = 0;
+        n |= ((read(is) & 0xFFL) << 0);
+        n |= ((read(is) & 0xFFL) << 8);
+        n |= ((read(is) & 0xFFL) << 16);
+        n |= ((read(is) & 0xFFL) << 24);
+        n |= ((read(is) & 0xFFL) << 32);
+        n |= ((read(is) & 0xFFL) << 40);
+        n |= ((read(is) & 0xFFL) << 48);
+        n |= ((read(is) & 0xFFL) << 56);
+        return n;
+    }
+
+    static void writeString(OutputStream os, String s) throws IOException {
+        byte[] b = s.getBytes("UTF-8");
+        writeLong(os, b.length);
+        os.write(b, 0, b.length);
+    }
+
+    static String readString(CountingInputStream cis) throws IOException {
+        long n = readLong(cis);
+        byte[] b = streamToBytes(cis, n);
+        return new String(b, "UTF-8");
+    }
+
+    static void writeHeaderList(List<Header> headers, OutputStream os) throws IOException {
+        if (headers != null) {
+            writeInt(os, headers.size());
+            for (Header header : headers) {
+                writeString(os, header.getName());
+                writeString(os, header.getValue());
+            }
+        } else {
+            writeInt(os, 0);
+        }
+    }
+
+    static List<Header> readHeaderList(CountingInputStream cis) throws IOException {
+        int size = readInt(cis);
+        if (size < 0) {
+            throw new IOException("readHeaderList size=" + size);
+        }
+        List<Header> result =
+                (size == 0) ? Collections.<Header>emptyList() : new ArrayList<Header>();
+        for (int i = 0; i < size; i++) {
+            String name = readString(cis).intern();
+            String value = readString(cis).intern();
+            result.add(new Header(name, value));
+        }
+        return result;
+    }
+}

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

@@ -0,0 +1,203 @@
+/*
+ * 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.toolbox;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Request.Method;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An HttpStack that performs request over an {@link HttpClient}.
+ *
+ * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another
+ * {@link BaseHttpStack} implementation.
+ */
+@Deprecated
+public class HttpClientStack implements HttpStack {
+    protected final HttpClient mClient;
+
+    private static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+    public HttpClientStack(HttpClient client) {
+        mClient = client;
+    }
+
+    private static void setHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
+        for (String key : headers.keySet()) {
+            httpRequest.setHeader(key, headers.get(key));
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
+        List<NameValuePair> result = new ArrayList<>(postParams.size());
+        for (String key : postParams.keySet()) {
+            result.add(new BasicNameValuePair(key, postParams.get(key)));
+        }
+        return result;
+    }
+
+    @Override
+    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError {
+        HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
+        setHeaders(httpRequest, additionalHeaders);
+        // Request.getHeaders() takes precedence over the given additional (cache) headers) and any
+        // headers set by createHttpRequest (like the Content-Type header).
+        setHeaders(httpRequest, request.getHeaders());
+        onPrepareRequest(httpRequest);
+        HttpParams httpParams = httpRequest.getParams();
+        int timeoutMs = request.getTimeoutMs();
+        // TODO: Reevaluate this connection timeout based on more wide-scale
+        // data collection and possibly different for wifi vs. 3G.
+        HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
+        HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
+        return mClient.execute(httpRequest);
+    }
+
+    /**
+     * Creates the appropriate subclass of HttpUriRequest for passed in request.
+     */
+    @SuppressWarnings("deprecation")
+    /* protected */ static HttpUriRequest createHttpRequest(
+            Request<?> request, Map<String, String> additionalHeaders) throws AuthFailureError {
+        switch (request.getMethod()) {
+            case Method.DEPRECATED_GET_OR_POST: {
+                // This is the deprecated way that needs to be handled for backwards
+                // compatibility.
+                // If the request's post body is null, then the assumption is that the request
+                // is
+                // GET.  Otherwise, it is assumed that the request is a POST.
+                byte[] postBody = request.getPostBody();
+                if (postBody != null) {
+                    HttpPost postRequest = new HttpPost(request.getUrl());
+                    postRequest.addHeader(
+                            HEADER_CONTENT_TYPE, request.getPostBodyContentType());
+                    HttpEntity entity;
+                    entity = new ByteArrayEntity(postBody);
+                    postRequest.setEntity(entity);
+                    return postRequest;
+                } else {
+                    return new HttpGet(request.getUrl());
+                }
+            }
+            case Method.GET:
+                return new HttpGet(request.getUrl());
+            case Method.DELETE:
+                return new HttpDelete(request.getUrl());
+            case Method.POST: {
+                HttpPost postRequest = new HttpPost(request.getUrl());
+                postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+                setEntityIfNonEmptyBody(postRequest, request);
+                return postRequest;
+            }
+            case Method.PUT: {
+                HttpPut putRequest = new HttpPut(request.getUrl());
+                putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+                setEntityIfNonEmptyBody(putRequest, request);
+                return putRequest;
+            }
+            case Method.HEAD:
+                return new HttpHead(request.getUrl());
+            case Method.OPTIONS:
+                return new HttpOptions(request.getUrl());
+            case Method.TRACE:
+                return new HttpTrace(request.getUrl());
+            case Method.PATCH: {
+                HttpPatch patchRequest = new HttpPatch(request.getUrl());
+                patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
+                setEntityIfNonEmptyBody(patchRequest, request);
+                return patchRequest;
+            }
+            default:
+                throw new IllegalStateException("Unknown request method.");
+        }
+    }
+
+    private static void setEntityIfNonEmptyBody(
+            HttpEntityEnclosingRequestBase httpRequest, Request<?> request)
+            throws AuthFailureError {
+        byte[] body = request.getBody();
+        if (body != null) {
+            HttpEntity entity = new ByteArrayEntity(body);
+            httpRequest.setEntity(entity);
+        }
+    }
+
+    /**
+     * Called before the request is executed using the underlying HttpClient.
+     *
+     * <p>Overwrite in subclasses to augment the request.
+     */
+    protected void onPrepareRequest(HttpUriRequest request) throws IOException {
+        // Nothing.
+    }
+
+    /**
+     * The HttpPatch class does not exist in the Android framework, so this has been defined here.
+     */
+    public static final class HttpPatch extends HttpEntityEnclosingRequestBase {
+
+        public static final String METHOD_NAME = "PATCH";
+
+        public HttpPatch() {
+            super();
+        }
+
+        public HttpPatch(final URI uri) {
+            super();
+            setURI(uri);
+        }
+
+        /**
+         * @throws IllegalArgumentException if the uri is invalid.
+         */
+        public HttpPatch(final String uri) {
+            super();
+            setURI(URI.create(uri));
+        }
+
+        @Override
+        public String getMethod() {
+            return METHOD_NAME;
+        }
+    }
+}

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

@@ -0,0 +1,215 @@
+/*
+ * 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.toolbox;
+
+import cn.yyxx.support.volley.source.Cache;
+import cn.yyxx.support.volley.source.Header;
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.VolleyLog;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.TreeMap;
+
+/**
+ * Utility methods for parsing HTTP headers.
+ */
+public class HttpHeaderParser {
+
+    static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+    private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
+
+    private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+    /**
+     * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
+     *
+     * @param response The network response to parse headers from
+     * @return a cache entry for the given response, or null if the response is not cacheable.
+     */
+    public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
+        long now = System.currentTimeMillis();
+
+        Map<String, String> headers = response.headers;
+
+        long serverDate = 0;
+        long lastModified = 0;
+        long serverExpires = 0;
+        long softExpire = 0;
+        long finalExpire = 0;
+        long maxAge = 0;
+        long staleWhileRevalidate = 0;
+        boolean hasCacheControl = false;
+        boolean mustRevalidate = false;
+
+        String serverEtag = null;
+        String headerValue;
+
+        headerValue = headers.get("Date");
+        if (headerValue != null) {
+            serverDate = parseDateAsEpoch(headerValue);
+        }
+
+        headerValue = headers.get("Cache-Control");
+        if (headerValue != null) {
+            hasCacheControl = true;
+            String[] tokens = headerValue.split(",", 0);
+            for (int i = 0; i < tokens.length; i++) {
+                String token = tokens[i].trim();
+                if (token.equals("no-cache") || token.equals("no-store")) {
+                    return null;
+                } else if (token.startsWith("max-age=")) {
+                    try {
+                        maxAge = Long.parseLong(token.substring(8));
+                    } catch (Exception e) {
+                    }
+                } else if (token.startsWith("stale-while-revalidate=")) {
+                    try {
+                        staleWhileRevalidate = Long.parseLong(token.substring(23));
+                    } catch (Exception e) {
+                    }
+                } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
+                    mustRevalidate = true;
+                }
+            }
+        }
+
+        headerValue = headers.get("Expires");
+        if (headerValue != null) {
+            serverExpires = parseDateAsEpoch(headerValue);
+        }
+
+        headerValue = headers.get("Last-Modified");
+        if (headerValue != null) {
+            lastModified = parseDateAsEpoch(headerValue);
+        }
+
+        serverEtag = headers.get("ETag");
+
+        // Cache-Control takes precedence over an Expires header, even if both exist and Expires
+        // is more restrictive.
+        if (hasCacheControl) {
+            softExpire = now + maxAge * 1000;
+            finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000;
+        } else if (serverDate > 0 && serverExpires >= serverDate) {
+            // Default semantic for Expire header in HTTP specification is softExpire.
+            softExpire = now + (serverExpires - serverDate);
+            finalExpire = softExpire;
+        }
+
+        Cache.Entry entry = new Cache.Entry();
+        entry.data = response.data;
+        entry.etag = serverEtag;
+        entry.softTtl = softExpire;
+        entry.ttl = finalExpire;
+        entry.serverDate = serverDate;
+        entry.lastModified = lastModified;
+        entry.responseHeaders = headers;
+        entry.allResponseHeaders = response.allHeaders;
+
+        return entry;
+    }
+
+    /**
+     * Parse date in RFC1123 format, and return its value as epoch
+     */
+    public static long parseDateAsEpoch(String dateStr) {
+        try {
+            // Parse date in RFC1123 format if this header contains one
+            return newRfc1123Formatter().parse(dateStr).getTime();
+        } catch (ParseException e) {
+            // Date in invalid format, fallback to 0
+            VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr);
+            return 0;
+        }
+    }
+
+    /**
+     * Format an epoch date in RFC1123 format.
+     */
+    static String formatEpochAsRfc1123(long epoch) {
+        return newRfc1123Formatter().format(new Date(epoch));
+    }
+
+    private static SimpleDateFormat newRfc1123Formatter() {
+        SimpleDateFormat formatter = new SimpleDateFormat(RFC1123_FORMAT, Locale.US);
+        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+        return formatter;
+    }
+
+    /**
+     * Retrieve a charset from headers
+     *
+     * @param headers        An {@link Map} of headers
+     * @param defaultCharset Charset to return if none can be found
+     * @return Returns the charset specified in the Content-Type of this header, or the
+     * defaultCharset if none can be found.
+     */
+    public static String parseCharset(Map<String, String> headers, String defaultCharset) {
+        String contentType = headers.get(HEADER_CONTENT_TYPE);
+        if (contentType != null) {
+            String[] params = contentType.split(";", 0);
+            for (int i = 1; i < params.length; i++) {
+                String[] pair = params[i].trim().split("=", 0);
+                if (pair.length == 2) {
+                    if (pair[0].equals("charset")) {
+                        return pair[1];
+                    }
+                }
+            }
+        }
+
+        return defaultCharset;
+    }
+
+    /**
+     * Returns the charset specified in the Content-Type of this header, or the HTTP default
+     * (ISO-8859-1) if none can be found.
+     */
+    public static String parseCharset(Map<String, String> headers) {
+        return parseCharset(headers, DEFAULT_CONTENT_CHARSET);
+    }
+
+    // Note - these are copied from NetworkResponse to avoid making them public (as needed to access
+    // them from the .toolbox package), which would mean they'd become part of the Volley API.
+    // TODO: Consider obfuscating official releases so we can share utility methods between Volley
+    // and Toolbox without making them public APIs.
+
+    static Map<String, String> toHeaderMap(List<Header> allHeaders) {
+        Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        // Later elements in the list take precedence.
+        for (Header header : allHeaders) {
+            headers.put(header.getName(), header.getValue());
+        }
+        return headers;
+    }
+
+    static List<Header> toAllHeaderList(Map<String, String> headers) {
+        List<Header> allHeaders = new ArrayList<>(headers.size());
+        for (Map.Entry<String, String> header : headers.entrySet()) {
+            allHeaders.add(new Header(header.getKey(), header.getValue()));
+        }
+        return allHeaders;
+    }
+}

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

@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.toolbox;
+
+import cn.yyxx.support.volley.source.Header;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A response from an HTTP server.
+ */
+public final class HttpResponse {
+
+    private final int mStatusCode;
+    private final List<Header> mHeaders;
+    private final int mContentLength;
+    private final InputStream mContent;
+
+    /**
+     * Construct a new HttpResponse for an empty response body.
+     *
+     * @param statusCode the HTTP status code of the response
+     * @param headers    the response headers
+     */
+    public HttpResponse(int statusCode, List<Header> headers) {
+        this(statusCode, headers, /* contentLength= */ -1, /* content= */ null);
+    }
+
+    /**
+     * Construct a new HttpResponse.
+     *
+     * @param statusCode    the HTTP status code of the response
+     * @param headers       the response headers
+     * @param contentLength the length of the response content. Ignored if there is no content.
+     * @param content       an {@link InputStream} of the response content. May be null to indicate that
+     *                      the response has no content.
+     */
+    public HttpResponse(
+            int statusCode, List<Header> headers, int contentLength, InputStream content) {
+        mStatusCode = statusCode;
+        mHeaders = headers;
+        mContentLength = contentLength;
+        mContent = content;
+    }
+
+    /**
+     * Returns the HTTP status code of the response.
+     */
+    public final int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /**
+     * Returns the response headers. Must not be mutated directly.
+     */
+    public final List<Header> getHeaders() {
+        return Collections.unmodifiableList(mHeaders);
+    }
+
+    /**
+     * Returns the length of the content. Only valid if {@link #getContent} is non-null.
+     */
+    public final int getContentLength() {
+        return mContentLength;
+    }
+
+    /**
+     * Returns an {@link InputStream} of the response content. May be null to indicate that the
+     * response has no content.
+     */
+    public final InputStream getContent() {
+        return mContent;
+    }
+}

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

@@ -0,0 +1,48 @@
+/*
+ * 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.toolbox;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+import cn.yyxx.support.volley.source.Request;
+
+import org.apache.http.HttpResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * An HTTP stack abstraction.
+ *
+ * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library.
+ * Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future
+ * release of Volley.
+ */
+@Deprecated
+public interface HttpStack {
+    /**
+     * Performs an HTTP request with the given parameters.
+     *
+     * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+     * and the Content-Type header is set to request.getPostBodyContentType().
+     *
+     * @param request           the request to perform
+     * @param additionalHeaders additional headers to be sent together with {@link
+     *                          Request#getHeaders()}
+     * @return the HTTP response
+     */
+    HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError;
+}

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

@@ -0,0 +1,307 @@
+/*
+ * 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.toolbox;
+
+import android.support.annotation.VisibleForTesting;
+
+import cn.yyxx.support.volley.source.AuthFailureError;
+import cn.yyxx.support.volley.source.Header;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Request.Method;
+
+import java.io.DataOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * A {@link BaseHttpStack} based on {@link HttpURLConnection}.
+ */
+public class HurlStack extends BaseHttpStack {
+
+    private static final int HTTP_CONTINUE = 100;
+
+    /**
+     * An interface for transforming URLs before use.
+     */
+    public interface UrlRewriter {
+        /**
+         * Returns a URL to use instead of the provided one, or null to indicate this URL should not
+         * be used at all.
+         */
+        String rewriteUrl(String originalUrl);
+    }
+
+    private final UrlRewriter mUrlRewriter;
+    private final SSLSocketFactory mSslSocketFactory;
+
+    public HurlStack() {
+        this(/* urlRewriter = */ null);
+    }
+
+    /**
+     * @param urlRewriter Rewriter to use for request URLs
+     */
+    public HurlStack(UrlRewriter urlRewriter) {
+        this(urlRewriter, /* sslSocketFactory = */ null);
+    }
+
+    /**
+     * @param urlRewriter      Rewriter to use for request URLs
+     * @param sslSocketFactory SSL factory to use for HTTPS connections
+     */
+    public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
+        mUrlRewriter = urlRewriter;
+        mSslSocketFactory = sslSocketFactory;
+    }
+
+    @Override
+    public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
+            throws IOException, AuthFailureError {
+        String url = request.getUrl();
+        HashMap<String, String> map = new HashMap<>();
+        map.putAll(additionalHeaders);
+        // Request.getHeaders() takes precedence over the given additional (cache) headers).
+        map.putAll(request.getHeaders());
+        if (mUrlRewriter != null) {
+            String rewritten = mUrlRewriter.rewriteUrl(url);
+            if (rewritten == null) {
+                throw new IOException("URL blocked by rewriter: " + url);
+            }
+            url = rewritten;
+        }
+        URL parsedUrl = new URL(url);
+        HttpURLConnection connection = openConnection(parsedUrl, request);
+        boolean keepConnectionOpen = false;
+        try {
+            for (String headerName : map.keySet()) {
+                connection.setRequestProperty(headerName, map.get(headerName));
+            }
+            setConnectionParametersForRequest(connection, request);
+            // Initialize HttpResponse with data from the HttpURLConnection.
+            int responseCode = connection.getResponseCode();
+            if (responseCode == -1) {
+                // -1 is returned by getResponseCode() if the response code could not be retrieved.
+                // Signal to the caller that something was wrong with the connection.
+                throw new IOException("Could not retrieve response code from HttpUrlConnection.");
+            }
+
+            if (!hasResponseBody(request.getMethod(), responseCode)) {
+                return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
+            }
+
+            // Need to keep the connection open until the stream is consumed by the caller. Wrap the
+            // stream such that close() will disconnect the connection.
+            keepConnectionOpen = true;
+            return new HttpResponse(
+                    responseCode,
+                    convertHeaders(connection.getHeaderFields()),
+                    connection.getContentLength(),
+                    new UrlConnectionInputStream(connection));
+        } finally {
+            if (!keepConnectionOpen) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) {
+        List<Header> headerList = new ArrayList<>(responseHeaders.size());
+        for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) {
+            // HttpUrlConnection includes the status line as a header with a null key; omit it here
+            // since it's not really a header and the rest of Volley assumes non-null keys.
+            if (entry.getKey() != null) {
+                for (String value : entry.getValue()) {
+                    headerList.add(new Header(entry.getKey(), value));
+                }
+            }
+        }
+        return headerList;
+    }
+
+    /**
+     * Checks if a response message contains a body.
+     *
+     * @param requestMethod request method
+     * @param responseCode  response status code
+     * @return whether the response has a body
+     * @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a>
+     */
+    private static boolean hasResponseBody(int requestMethod, int responseCode) {
+        return requestMethod != Request.Method.HEAD
+                && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK)
+                && responseCode != HttpURLConnection.HTTP_NO_CONTENT
+                && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED;
+    }
+
+    /**
+     * Wrapper for a {@link HttpURLConnection}'s InputStream which disconnects the connection on
+     * stream close.
+     */
+    static class UrlConnectionInputStream extends FilterInputStream {
+        private final HttpURLConnection mConnection;
+
+        UrlConnectionInputStream(HttpURLConnection connection) {
+            super(inputStreamFromConnection(connection));
+            mConnection = connection;
+        }
+
+        @Override
+        public void close() throws IOException {
+            super.close();
+            mConnection.disconnect();
+        }
+    }
+
+    /**
+     * Initializes an {@link InputStream} from the given {@link HttpURLConnection}.
+     *
+     * @param connection
+     * @return an HttpEntity populated with data from <code>connection</code>.
+     */
+    private static InputStream inputStreamFromConnection(HttpURLConnection connection) {
+        InputStream inputStream;
+        try {
+            inputStream = connection.getInputStream();
+        } catch (IOException ioe) {
+            inputStream = connection.getErrorStream();
+        }
+        return inputStream;
+    }
+
+    /**
+     * Create an {@link HttpURLConnection} for the specified {@code url}.
+     */
+    protected HttpURLConnection createConnection(URL url) throws IOException {
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+        // Workaround for the M release HttpURLConnection not observing the
+        // HttpURLConnection.setFollowRedirects() property.
+        // https://code.google.com/p/android/issues/detail?id=194495
+        connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects());
+
+        return connection;
+    }
+
+    /**
+     * Opens an {@link HttpURLConnection} with parameters.
+     *
+     * @param url
+     * @return an open connection
+     * @throws IOException
+     */
+    private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
+        HttpURLConnection connection = createConnection(url);
+
+        int timeoutMs = request.getTimeoutMs();
+        connection.setConnectTimeout(timeoutMs);
+        connection.setReadTimeout(timeoutMs);
+        connection.setUseCaches(false);
+        connection.setDoInput(true);
+
+        // use caller-provided custom SslSocketFactory, if any, for HTTPS
+        if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
+            ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory);
+        }
+
+        return connection;
+    }
+
+    // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be
+    // checked against the existing properties in the connection and not overridden if already set.
+    @SuppressWarnings("deprecation")
+    /* package */ static void setConnectionParametersForRequest(
+            HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
+        switch (request.getMethod()) {
+            case Method.DEPRECATED_GET_OR_POST:
+                // This is the deprecated way that needs to be handled for backwards compatibility.
+                // If the request's post body is null, then the assumption is that the request is
+                // GET.  Otherwise, it is assumed that the request is a POST.
+                byte[] postBody = request.getPostBody();
+                if (postBody != null) {
+                    connection.setRequestMethod("POST");
+                    addBody(connection, request, postBody);
+                }
+                break;
+            case Method.GET:
+                // Not necessary to set the request method because connection defaults to GET but
+                // being explicit here.
+                connection.setRequestMethod("GET");
+                break;
+            case Method.DELETE:
+                connection.setRequestMethod("DELETE");
+                break;
+            case Method.POST:
+                connection.setRequestMethod("POST");
+                addBodyIfExists(connection, request);
+                break;
+            case Method.PUT:
+                connection.setRequestMethod("PUT");
+                addBodyIfExists(connection, request);
+                break;
+            case Method.HEAD:
+                connection.setRequestMethod("HEAD");
+                break;
+            case Method.OPTIONS:
+                connection.setRequestMethod("OPTIONS");
+                break;
+            case Method.TRACE:
+                connection.setRequestMethod("TRACE");
+                break;
+            case Method.PATCH:
+                connection.setRequestMethod("PATCH");
+                addBodyIfExists(connection, request);
+                break;
+            default:
+                throw new IllegalStateException("Unknown method type.");
+        }
+    }
+
+    private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
+            throws IOException, AuthFailureError {
+        byte[] body = request.getBody();
+        if (body != null) {
+            addBody(connection, request, body);
+        }
+    }
+
+    private static void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
+            throws IOException {
+        // Prepare output. There is no need to set Content-Length explicitly,
+        // since this is handled by HttpURLConnection using the size of the prepared
+        // output stream.
+        connection.setDoOutput(true);
+        // Set the content-type unless it was already set (by Request#getHeaders).
+        if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) {
+            connection.setRequestProperty(
+                    HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
+        }
+        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
+        out.write(body);
+        out.close();
+    }
+}

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

@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2013 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.toolbox;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.RequestQueue;
+import cn.yyxx.support.volley.source.Response.ErrorListener;
+import cn.yyxx.support.volley.source.Response.Listener;
+import cn.yyxx.support.volley.source.ResponseDelivery;
+import cn.yyxx.support.volley.source.VolleyError;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Helper that handles loading and caching images from remote URLs.
+ *
+ * <p>The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} and
+ * to pass in the default image listener provided by {@link ImageLoader#getImageListener(ImageView,
+ * int, int)}. Note that all function calls to this class must be made from the main thread, and all
+ * responses will be delivered to the main thread as well. Custom {@link ResponseDelivery}s which
+ * don't use the main thread are not supported.
+ */
+public class ImageLoader {
+    /**
+     * RequestQueue for dispatching ImageRequests onto.
+     */
+    private final RequestQueue mRequestQueue;
+
+    /**
+     * Amount of time to wait after first response arrives before delivering all responses.
+     */
+    private int mBatchResponseDelayMs = 100;
+
+    /**
+     * The cache implementation to be used as an L1 cache before calling into volley.
+     */
+    private final ImageCache mCache;
+
+    /**
+     * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so that we can
+     * coalesce multiple requests to the same URL into a single network request.
+     */
+    private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<>();
+
+    /**
+     * HashMap of the currently pending responses (waiting to be delivered).
+     */
+    private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<>();
+
+    /**
+     * Handler to the main thread.
+     */
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    /**
+     * Runnable for in-flight response delivery.
+     */
+    private Runnable mRunnable;
+
+    /**
+     * Simple cache adapter interface. If provided to the ImageLoader, it will be used as an L1
+     * cache before dispatch to Volley. Implementations must not block. Implementation with an
+     * LruCache is recommended.
+     */
+    public interface ImageCache {
+        Bitmap getBitmap(String url);
+
+        void putBitmap(String url, Bitmap bitmap);
+    }
+
+    /**
+     * Constructs a new ImageLoader.
+     *
+     * @param queue      The RequestQueue to use for making image requests.
+     * @param imageCache The cache to use as an L1 cache.
+     */
+    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
+        mRequestQueue = queue;
+        mCache = imageCache;
+    }
+
+    /**
+     * The default implementation of ImageListener which handles basic functionality of showing a
+     * default image until the network response is received, at which point it will switch to either
+     * the actual image or the error image.
+     *
+     * @param view              The imageView that the listener is associated with.
+     * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
+     * @param errorImageResId   Error image resource ID to use, or 0 if it doesn't exist.
+     */
+    public static ImageListener getImageListener(
+            final ImageView view, final int defaultImageResId, final int errorImageResId) {
+        return new ImageListener() {
+            @Override
+            public void onErrorResponse(VolleyError error) {
+                if (errorImageResId != 0) {
+                    view.setImageResource(errorImageResId);
+                }
+            }
+
+            @Override
+            public void onResponse(ImageContainer response, boolean isImmediate) {
+                if (response.getBitmap() != null) {
+                    view.setImageBitmap(response.getBitmap());
+                } else if (defaultImageResId != 0) {
+                    view.setImageResource(defaultImageResId);
+                }
+            }
+        };
+    }
+
+    /**
+     * Interface for the response handlers on image requests.
+     *
+     * <p>The call flow is this: 1. Upon being attached to a request, onResponse(response, true)
+     * will be invoked to reflect any cached data that was already available. If the data was
+     * available, response.getBitmap() will be non-null.
+     *
+     * <p>2. After a network response returns, only one of the following cases will happen: -
+     * onResponse(response, false) will be called if the image was loaded. or - onErrorResponse will
+     * be called if there was an error loading the image.
+     */
+    public interface ImageListener extends ErrorListener {
+        /**
+         * Listens for non-error changes to the loading of the image request.
+         *
+         * @param response    Holds all information pertaining to the request, as well as the bitmap
+         *                    (if it is loaded).
+         * @param isImmediate True if this was called during ImageLoader.get() variants. This can be
+         *                    used to differentiate between a cached image loading and a network image loading in
+         *                    order to, for example, run an animation to fade in network loaded images.
+         */
+        void onResponse(ImageContainer response, boolean isImmediate);
+    }
+
+    /**
+     * Checks if the item is available in the cache.
+     *
+     * @param requestUrl The url of the remote image
+     * @param maxWidth   The maximum width of the returned image.
+     * @param maxHeight  The maximum height of the returned image.
+     * @return True if the item exists in cache, false otherwise.
+     */
+    public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
+        return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
+    }
+
+    /**
+     * Checks if the item is available in the cache.
+     *
+     * <p>Must be called from the main thread.
+     *
+     * @param requestUrl The url of the remote image
+     * @param maxWidth   The maximum width of the returned image.
+     * @param maxHeight  The maximum height of the returned image.
+     * @param scaleType  The scaleType of the imageView.
+     * @return True if the item exists in cache, false otherwise.
+     */
+    @MainThread
+    public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
+        Threads.throwIfNotOnMainThread();
+
+        String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
+        return mCache.getBitmap(cacheKey) != null;
+    }
+
+    /**
+     * Returns an ImageContainer for the requested URL.
+     *
+     * <p>The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
+     * If the default was returned, the {@link ImageLoader} will be invoked when the request is
+     * fulfilled.
+     *
+     * @param requestUrl The URL of the image to be loaded.
+     */
+    public ImageContainer get(String requestUrl, final ImageListener listener) {
+        return get(requestUrl, listener, /* maxWidth= */ 0, /* maxHeight= */ 0);
+    }
+
+    /**
+     * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with {@code
+     * Scaletype == ScaleType.CENTER_INSIDE}.
+     */
+    public ImageContainer get(
+            String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) {
+        return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
+    }
+
+    /**
+     * Issues a bitmap request with the given URL if that image is not available in the cache, and
+     * returns a bitmap container that contains all of the data relating to the request (as well as
+     * the default image if the requested image is not available).
+     *
+     * <p>Must be called from the main thread.
+     *
+     * @param requestUrl    The url of the remote image
+     * @param imageListener The listener to call when the remote image is loaded
+     * @param maxWidth      The maximum width of the returned image.
+     * @param maxHeight     The maximum height of the returned image.
+     * @param scaleType     The ImageViews ScaleType used to calculate the needed image size.
+     * @return A container object that contains all of the properties of the request, as well as the
+     * currently available image (default if remote is not loaded).
+     */
+    @MainThread
+    public ImageContainer get(
+            String requestUrl,
+            ImageListener imageListener,
+            int maxWidth,
+            int maxHeight,
+            ScaleType scaleType) {
+
+        // only fulfill requests that were initiated from the main thread.
+        Threads.throwIfNotOnMainThread();
+
+        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
+
+        // Try to look up the request in the cache of remote images.
+        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
+        if (cachedBitmap != null) {
+            // Return the cached bitmap.
+            ImageContainer container =
+                    new ImageContainer(
+                            cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null);
+            imageListener.onResponse(container, true);
+            return container;
+        }
+
+        // The bitmap did not exist in the cache, fetch it!
+        ImageContainer imageContainer =
+                new ImageContainer(null, requestUrl, cacheKey, imageListener);
+
+        // Update the caller to let them know that they should use the default bitmap.
+        imageListener.onResponse(imageContainer, true);
+
+        // Check to see if a request is already in-flight or completed but pending batch delivery.
+        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
+        if (request == null) {
+            request = mBatchedResponses.get(cacheKey);
+        }
+        if (request != null) {
+            // If it is, add this request to the list of listeners.
+            request.addContainer(imageContainer);
+            return imageContainer;
+        }
+
+        // The request is not already in flight. Send the new request to the network and
+        // track it.
+        Request<Bitmap> newRequest =
+                makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
+
+        mRequestQueue.add(newRequest);
+        mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
+        return imageContainer;
+    }
+
+    protected Request<Bitmap> makeImageRequest(
+            String requestUrl,
+            int maxWidth,
+            int maxHeight,
+            ScaleType scaleType,
+            final String cacheKey) {
+        return new ImageRequest(
+                requestUrl,
+                new Listener<Bitmap>() {
+                    @Override
+                    public void onResponse(Bitmap response) {
+                        onGetImageSuccess(cacheKey, response);
+                    }
+                },
+                maxWidth,
+                maxHeight,
+                scaleType,
+                Config.RGB_565,
+                new ErrorListener() {
+                    @Override
+                    public void onErrorResponse(VolleyError error) {
+                        onGetImageError(cacheKey, error);
+                    }
+                });
+    }
+
+    /**
+     * Sets the amount of time to wait after the first response arrives before delivering all
+     * responses. Batching can be disabled entirely by passing in 0.
+     *
+     * @param newBatchedResponseDelayMs The time in milliseconds to wait.
+     */
+    public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
+        mBatchResponseDelayMs = newBatchedResponseDelayMs;
+    }
+
+    /**
+     * Handler for when an image was successfully loaded.
+     *
+     * @param cacheKey The cache key that is associated with the image request.
+     * @param response The bitmap that was returned from the network.
+     */
+    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
+        // cache the image that was fetched.
+        mCache.putBitmap(cacheKey, response);
+
+        // remove the request from the list of in-flight requests.
+        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
+
+        if (request != null) {
+            // Update the response bitmap.
+            request.mResponseBitmap = response;
+
+            // Send the batched response
+            batchResponse(cacheKey, request);
+        }
+    }
+
+    /**
+     * Handler for when an image failed to load.
+     *
+     * @param cacheKey The cache key that is associated with the image request.
+     */
+    protected void onGetImageError(String cacheKey, VolleyError error) {
+        // Notify the requesters that something failed via a null result.
+        // Remove this request from the list of in-flight requests.
+        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
+
+        if (request != null) {
+            // Set the error for this request
+            request.setError(error);
+
+            // Send the batched response
+            batchResponse(cacheKey, request);
+        }
+    }
+
+    /**
+     * Container object for all of the data surrounding an image request.
+     */
+    public class ImageContainer {
+        /**
+         * The most relevant bitmap for the container. If the image was in cache, the Holder to use
+         * for the final bitmap (the one that pairs to the requested URL).
+         */
+        private Bitmap mBitmap;
+
+        private final ImageListener mListener;
+
+        /**
+         * The cache key that was associated with the request
+         */
+        private final String mCacheKey;
+
+        /**
+         * The request URL that was specified
+         */
+        private final String mRequestUrl;
+
+        /**
+         * Constructs a BitmapContainer object.
+         *
+         * @param bitmap     The final bitmap (if it exists).
+         * @param requestUrl The requested URL for this container.
+         * @param cacheKey   The cache key that identifies the requested URL for this container.
+         */
+        public ImageContainer(
+                Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) {
+            mBitmap = bitmap;
+            mRequestUrl = requestUrl;
+            mCacheKey = cacheKey;
+            mListener = listener;
+        }
+
+        /**
+         * Releases interest in the in-flight request (and cancels it if no one else is listening).
+         *
+         * <p>Must be called from the main thread.
+         */
+        @MainThread
+        public void cancelRequest() {
+            Threads.throwIfNotOnMainThread();
+
+            if (mListener == null) {
+                return;
+            }
+
+            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
+            if (request != null) {
+                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
+                if (canceled) {
+                    mInFlightRequests.remove(mCacheKey);
+                }
+            } else {
+                // check to see if it is already batched for delivery.
+                request = mBatchedResponses.get(mCacheKey);
+                if (request != null) {
+                    request.removeContainerAndCancelIfNecessary(this);
+                    if (request.mContainers.size() == 0) {
+                        mBatchedResponses.remove(mCacheKey);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
+         */
+        public Bitmap getBitmap() {
+            return mBitmap;
+        }
+
+        /**
+         * Returns the requested URL for this container.
+         */
+        public String getRequestUrl() {
+            return mRequestUrl;
+        }
+    }
+
+    /**
+     * Wrapper class used to map a Request to the set of active ImageContainer objects that are
+     * interested in its results.
+     */
+    private static class BatchedImageRequest {
+        /**
+         * The request being tracked
+         */
+        private final Request<?> mRequest;
+
+        /**
+         * The result of the request being tracked by this item
+         */
+        private Bitmap mResponseBitmap;
+
+        /**
+         * Error if one occurred for this response
+         */
+        private VolleyError mError;
+
+        /**
+         * List of all of the active ImageContainers that are interested in the request
+         */
+        private final List<ImageContainer> mContainers = new ArrayList<>();
+
+        /**
+         * Constructs a new BatchedImageRequest object
+         *
+         * @param request   The request being tracked
+         * @param container The ImageContainer of the person who initiated the request.
+         */
+        public BatchedImageRequest(Request<?> request, ImageContainer container) {
+            mRequest = request;
+            mContainers.add(container);
+        }
+
+        /**
+         * Set the error for this response
+         */
+        public void setError(VolleyError error) {
+            mError = error;
+        }
+
+        /**
+         * Get the error for this response
+         */
+        public VolleyError getError() {
+            return mError;
+        }
+
+        /**
+         * Adds another ImageContainer to the list of those interested in the results of the
+         * request.
+         */
+        public void addContainer(ImageContainer container) {
+            mContainers.add(container);
+        }
+
+        /**
+         * Detaches the bitmap container from the request and cancels the request if no one is left
+         * listening.
+         *
+         * @param container The container to remove from the list
+         * @return True if the request was canceled, false otherwise.
+         */
+        public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
+            mContainers.remove(container);
+            if (mContainers.size() == 0) {
+                mRequest.cancel();
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Starts the runnable for batched delivery of responses if it is not already started.
+     *
+     * @param cacheKey The cacheKey of the response being delivered.
+     * @param request  The BatchedImageRequest to be delivered.
+     */
+    private void batchResponse(String cacheKey, BatchedImageRequest request) {
+        mBatchedResponses.put(cacheKey, request);
+        // If we don't already have a batch delivery runnable in flight, make a new one.
+        // Note that this will be used to deliver responses to all callers in mBatchedResponses.
+        if (mRunnable == null) {
+            mRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    for (BatchedImageRequest bir : mBatchedResponses.values()) {
+                        for (ImageContainer container : bir.mContainers) {
+                            // If one of the callers in the batched request canceled the
+                            // request
+                            // after the response was received but before it was delivered,
+                            // skip them.
+                            if (container.mListener == null) {
+                                continue;
+                            }
+                            if (bir.getError() == null) {
+                                container.mBitmap = bir.mResponseBitmap;
+                                container.mListener.onResponse(container, false);
+                            } else {
+                                container.mListener.onErrorResponse(bir.getError());
+                            }
+                        }
+                    }
+                    mBatchedResponses.clear();
+                    mRunnable = null;
+                }
+            };
+            // Post the runnable.
+            mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
+        }
+    }
+
+    /**
+     * Creates a cache key for use with the L1 cache.
+     *
+     * @param url       The URL of the request.
+     * @param maxWidth  The max-width of the output.
+     * @param maxHeight The max-height of the output.
+     * @param scaleType The scaleType of the imageView.
+     */
+    private static String getCacheKey(
+            String url, int maxWidth, int maxHeight, ScaleType scaleType) {
+        return new StringBuilder(url.length() + 12)
+                .append("#W")
+                .append(maxWidth)
+                .append("#H")
+                .append(maxHeight)
+                .append("#S")
+                .append(scaleType.ordinal())
+                .append(url)
+                .toString();
+    }
+}

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

@@ -0,0 +1,298 @@
+/*
+ * 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.toolbox;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.widget.ImageView.ScaleType;
+
+import cn.yyxx.support.volley.source.DefaultRetryPolicy;
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.ParseError;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Response;
+import cn.yyxx.support.volley.source.VolleyLog;
+
+/**
+ * A canned request for getting an image at a given URL and calling back with a decoded Bitmap.
+ */
+public class ImageRequest extends Request<Bitmap> {
+    /**
+     * Socket timeout in milliseconds for image requests
+     */
+    public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;
+
+    /**
+     * Default number of retries for image requests
+     */
+    public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
+
+    /**
+     * Default backoff multiplier for image requests
+     */
+    public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;
+
+    /**
+     * Lock to guard mListener as it is cleared on cancel() and read on delivery.
+     */
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    @Nullable
+    private Response.Listener<Bitmap> mListener;
+
+    private final Config mDecodeConfig;
+    private final int mMaxWidth;
+    private final int mMaxHeight;
+    private final ScaleType mScaleType;
+
+    /**
+     * Decoding lock so that we don't decode more than one image at a time (to avoid OOM's)
+     */
+    private static final Object sDecodeLock = new Object();
+
+    /**
+     * Creates a new image request, decoding to a maximum specified width and height. If both width
+     * and height are zero, the image will be decoded to its natural size. If one of the two is
+     * nonzero, that dimension will be clamped and the other one will be set to preserve the image's
+     * aspect ratio. If both width and height are nonzero, the image will be decoded to be fit in
+     * the rectangle of dimensions width x height while keeping its aspect ratio.
+     *
+     * @param url           URL of the image
+     * @param listener      Listener to receive the decoded bitmap
+     * @param maxWidth      Maximum width to decode this bitmap to, or zero for none
+     * @param maxHeight     Maximum height to decode this bitmap to, or zero for none
+     * @param scaleType     The ImageViews ScaleType used to calculate the needed image size.
+     * @param decodeConfig  Format to decode the bitmap to
+     * @param errorListener Error listener, or null to ignore errors
+     */
+    public ImageRequest(
+            String url,
+            Response.Listener<Bitmap> listener,
+            int maxWidth,
+            int maxHeight,
+            ScaleType scaleType,
+            Config decodeConfig,
+            @Nullable Response.ErrorListener errorListener) {
+        super(Method.GET, url, errorListener);
+        setRetryPolicy(
+                new DefaultRetryPolicy(
+                        DEFAULT_IMAGE_TIMEOUT_MS,
+                        DEFAULT_IMAGE_MAX_RETRIES,
+                        DEFAULT_IMAGE_BACKOFF_MULT));
+        mListener = listener;
+        mDecodeConfig = decodeConfig;
+        mMaxWidth = maxWidth;
+        mMaxHeight = maxHeight;
+        mScaleType = scaleType;
+    }
+
+    /**
+     * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to the
+     * normal constructor with {@code ScaleType.CENTER_INSIDE}.
+     */
+    @Deprecated
+    public ImageRequest(
+            String url,
+            Response.Listener<Bitmap> listener,
+            int maxWidth,
+            int maxHeight,
+            Config decodeConfig,
+            Response.ErrorListener errorListener) {
+        this(
+                url,
+                listener,
+                maxWidth,
+                maxHeight,
+                ScaleType.CENTER_INSIDE,
+                decodeConfig,
+                errorListener);
+    }
+
+    @Override
+    public Priority getPriority() {
+        return Priority.LOW;
+    }
+
+    /**
+     * Scales one side of a rectangle to fit aspect ratio.
+     *
+     * @param maxPrimary      Maximum size of the primary dimension (i.e. width for max width), or zero
+     *                        to maintain aspect ratio with secondary dimension
+     * @param maxSecondary    Maximum size of the secondary dimension, or zero to maintain aspect ratio
+     *                        with primary dimension
+     * @param actualPrimary   Actual size of the primary dimension
+     * @param actualSecondary Actual size of the secondary dimension
+     * @param scaleType       The ScaleType used to calculate the needed image size.
+     */
+    private static int getResizedDimension(
+            int maxPrimary,
+            int maxSecondary,
+            int actualPrimary,
+            int actualSecondary,
+            ScaleType scaleType) {
+
+        // If no dominant value at all, just return the actual.
+        if ((maxPrimary == 0) && (maxSecondary == 0)) {
+            return actualPrimary;
+        }
+
+        // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
+        if (scaleType == ScaleType.FIT_XY) {
+            if (maxPrimary == 0) {
+                return actualPrimary;
+            }
+            return maxPrimary;
+        }
+
+        // If primary is unspecified, scale primary to match secondary's scaling ratio.
+        if (maxPrimary == 0) {
+            double ratio = (double) maxSecondary / (double) actualSecondary;
+            return (int) (actualPrimary * ratio);
+        }
+
+        if (maxSecondary == 0) {
+            return maxPrimary;
+        }
+
+        double ratio = (double) actualSecondary / (double) actualPrimary;
+        int resized = maxPrimary;
+
+        // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
+        if (scaleType == ScaleType.CENTER_CROP) {
+            if ((resized * ratio) < maxSecondary) {
+                resized = (int) (maxSecondary / ratio);
+            }
+            return resized;
+        }
+
+        if ((resized * ratio) > maxSecondary) {
+            resized = (int) (maxSecondary / ratio);
+        }
+        return resized;
+    }
+
+    @Override
+    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
+        // Serialize all decode on a global lock to reduce concurrent heap usage.
+        synchronized (sDecodeLock) {
+            try {
+                return doParse(response);
+            } catch (OutOfMemoryError e) {
+                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
+                return Response.error(new ParseError(e));
+            }
+        }
+    }
+
+    /**
+     * The real guts of parseNetworkResponse. Broken out for readability.
+     */
+    private Response<Bitmap> doParse(NetworkResponse response) {
+        byte[] data = response.data;
+        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
+        Bitmap bitmap = null;
+        if (mMaxWidth == 0 && mMaxHeight == 0) {
+            decodeOptions.inPreferredConfig = mDecodeConfig;
+            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+        } else {
+            // If we have to resize this image, first get the natural bounds.
+            decodeOptions.inJustDecodeBounds = true;
+            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+            int actualWidth = decodeOptions.outWidth;
+            int actualHeight = decodeOptions.outHeight;
+
+            // Then compute the dimensions we would ideally like to decode to.
+            int desiredWidth =
+                    getResizedDimension(
+                            mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType);
+            int desiredHeight =
+                    getResizedDimension(
+                            mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType);
+
+            // Decode to the nearest power of two scaling factor.
+            decodeOptions.inJustDecodeBounds = false;
+            // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
+            // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
+            decodeOptions.inSampleSize =
+                    findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
+            Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
+
+            // If necessary, scale down to the maximal acceptable size.
+            if (tempBitmap != null
+                    && (tempBitmap.getWidth() > desiredWidth
+                    || tempBitmap.getHeight() > desiredHeight)) {
+                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true);
+                tempBitmap.recycle();
+            } else {
+                bitmap = tempBitmap;
+            }
+        }
+
+        if (bitmap == null) {
+            return Response.error(new ParseError(response));
+        } else {
+            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
+        }
+    }
+
+    @Override
+    public void cancel() {
+        super.cancel();
+        synchronized (mLock) {
+            mListener = null;
+        }
+    }
+
+    @Override
+    protected void deliverResponse(Bitmap response) {
+        Response.Listener<Bitmap> listener;
+        synchronized (mLock) {
+            listener = mListener;
+        }
+        if (listener != null) {
+            listener.onResponse(response);
+        }
+    }
+
+    /**
+     * Returns the largest power-of-two divisor for use in downscaling a bitmap that will not result
+     * in the scaling past the desired dimensions.
+     *
+     * @param actualWidth   Actual width of the bitmap
+     * @param actualHeight  Actual height of the bitmap
+     * @param desiredWidth  Desired width of the bitmap
+     * @param desiredHeight Desired height of the bitmap
+     */
+    @VisibleForTesting
+    static int findBestSampleSize(
+            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
+        double wr = (double) actualWidth / desiredWidth;
+        double hr = (double) actualHeight / desiredHeight;
+        double ratio = Math.min(wr, hr);
+        float n = 1.0f;
+        while ((n * 2) <= ratio) {
+            n *= 2;
+        }
+
+        return (int) n;
+    }
+}

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

@@ -0,0 +1,83 @@
+/*
+ * 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.toolbox;
+
+import android.support.annotation.Nullable;
+
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.ParseError;
+import cn.yyxx.support.volley.source.Response;
+import cn.yyxx.support.volley.source.Response.ErrorListener;
+import cn.yyxx.support.volley.source.Response.Listener;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a {@link JSONArray} response body at a given URL.
+ */
+public class JsonArrayRequest extends JsonRequest<JSONArray> {
+
+    /**
+     * Creates a new request.
+     *
+     * @param url           URL to fetch the JSON from
+     * @param listener      Listener to receive the JSON response
+     * @param errorListener Error listener, or null to ignore errors.
+     */
+    public JsonArrayRequest(String url, Listener<JSONArray> listener, @Nullable ErrorListener errorListener) {
+        super(Method.GET, url, null, listener, errorListener);
+    }
+
+    /**
+     * Creates a new request.
+     *
+     * @param method        the HTTP method to use
+     * @param url           URL to fetch the JSON from
+     * @param jsonRequest   A {@link JSONArray} to post with the request. Null indicates no parameters
+     *                      will be posted along with request.
+     * @param listener      Listener to receive the JSON response
+     * @param errorListener Error listener, or null to ignore errors.
+     */
+    public JsonArrayRequest(
+            int method,
+            String url,
+            @Nullable JSONArray jsonRequest,
+            Listener<JSONArray> listener,
+            @Nullable ErrorListener errorListener) {
+        super(
+                method,
+                url,
+                (jsonRequest == null) ? null : jsonRequest.toString(),
+                listener,
+                errorListener);
+    }
+
+    @Override
+    protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
+        try {
+            String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
+            return Response.success(new JSONArray(jsonString), HttpHeaderParser.parseCacheHeaders(response));
+        } catch (UnsupportedEncodingException e) {
+            return Response.error(new ParseError(e));
+        } catch (JSONException je) {
+            return Response.error(new ParseError(je));
+        }
+    }
+}

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

@@ -0,0 +1,96 @@
+/*
+ * 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.toolbox;
+
+import android.support.annotation.Nullable;
+
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.ParseError;
+import cn.yyxx.support.volley.source.Response;
+import cn.yyxx.support.volley.source.Response.ErrorListener;
+import cn.yyxx.support.volley.source.Response.Listener;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
+ * optional {@link JSONObject} to be passed in as part of the request body.
+ */
+public class JsonObjectRequest extends JsonRequest<JSONObject> {
+
+    /**
+     * Creates a new request.
+     *
+     * @param method        the HTTP method to use
+     * @param url           URL to fetch the JSON from
+     * @param jsonRequest   A {@link JSONObject} to post with the request. Null indicates no
+     *                      parameters will be posted along with request.
+     * @param listener      Listener to receive the JSON response
+     * @param errorListener Error listener, or null to ignore errors.
+     */
+    public JsonObjectRequest(
+            int method,
+            String url,
+            @Nullable JSONObject jsonRequest,
+            Listener<JSONObject> listener,
+            @Nullable ErrorListener errorListener) {
+        super(
+                method,
+                url,
+                (jsonRequest == null) ? null : jsonRequest.toString(),
+                listener,
+                errorListener);
+    }
+
+    /**
+     * Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is <code>null
+     * </code> , <code>POST</code> otherwise.
+     *
+     * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
+     */
+    public JsonObjectRequest(
+            String url,
+            @Nullable JSONObject jsonRequest,
+            Listener<JSONObject> listener,
+            @Nullable ErrorListener errorListener) {
+        this(
+                jsonRequest == null ? Method.GET : Method.POST,
+                url,
+                jsonRequest,
+                listener,
+                errorListener);
+    }
+
+    @Override
+    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
+        try {
+            String jsonString =
+                    new String(
+                            response.data,
+                            HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
+            return Response.success(
+                    new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response));
+        } catch (UnsupportedEncodingException e) {
+            return Response.error(new ParseError(e));
+        } catch (JSONException je) {
+            return Response.error(new ParseError(je));
+        }
+    }
+}

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

@@ -0,0 +1,129 @@
+/*
+ * 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.toolbox;
+
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Response;
+import cn.yyxx.support.volley.source.Response.ErrorListener;
+import cn.yyxx.support.volley.source.Response.Listener;
+import cn.yyxx.support.volley.source.VolleyLog;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A request for retrieving a T type response body at a given URL that also optionally sends along a
+ * JSON body in the request specified.
+ *
+ * @param <T> JSON type of response expected
+ */
+public abstract class JsonRequest<T> extends Request<T> {
+    /** Default charset for JSON request. */
+    protected static final String PROTOCOL_CHARSET = "utf-8";
+
+    /** Content type for request. */
+    private static final String PROTOCOL_CONTENT_TYPE =
+            String.format("application/json; charset=%s", PROTOCOL_CHARSET);
+
+    /** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
+    private final Object mLock = new Object();
+
+    @Nullable
+    @GuardedBy("mLock")
+    private Listener<T> mListener;
+
+    @Nullable private final String mRequestBody;
+
+    /**
+     * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
+     * or {@link #getPostParams()} is overridden (which defaults to POST).
+     *
+     * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
+     */
+    @Deprecated
+    public JsonRequest(
+            String url, String requestBody, Listener<T> listener, ErrorListener errorListener) {
+        this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
+    }
+
+    public JsonRequest(
+            int method,
+            String url,
+            @Nullable String requestBody,
+            Listener<T> listener,
+            @Nullable ErrorListener errorListener) {
+        super(method, url, errorListener);
+        mListener = listener;
+        mRequestBody = requestBody;
+    }
+
+    @Override
+    public void cancel() {
+        super.cancel();
+        synchronized (mLock) {
+            mListener = null;
+        }
+    }
+
+    @Override
+    protected void deliverResponse(T response) {
+        Response.Listener<T> listener;
+        synchronized (mLock) {
+            listener = mListener;
+        }
+        if (listener != null) {
+            listener.onResponse(response);
+        }
+    }
+
+    @Override
+    protected abstract Response<T> parseNetworkResponse(NetworkResponse response);
+
+    /** @deprecated Use {@link #getBodyContentType()}. */
+    @Deprecated
+    @Override
+    public String getPostBodyContentType() {
+        return getBodyContentType();
+    }
+
+    /** @deprecated Use {@link #getBody()}. */
+    @Deprecated
+    @Override
+    public byte[] getPostBody() {
+        return getBody();
+    }
+
+    @Override
+    public String getBodyContentType() {
+        return PROTOCOL_CONTENT_TYPE;
+    }
+
+    @Override
+    public byte[] getBody() {
+        try {
+            return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
+        } catch (UnsupportedEncodingException uee) {
+            VolleyLog.wtf(
+                    "Unsupported Encoding while trying to get the bytes of %s using %s",
+                    mRequestBody, PROTOCOL_CHARSET);
+            return null;
+        }
+    }
+}

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

@@ -0,0 +1,290 @@
+/**
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.toolbox;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView;
+
+import cn.yyxx.support.volley.source.VolleyError;
+import cn.yyxx.support.volley.source.toolbox.ImageLoader.ImageContainer;
+import cn.yyxx.support.volley.source.toolbox.ImageLoader.ImageListener;
+
+/**
+ * Handles fetching an image from a URL as well as the life-cycle of the associated request.
+ */
+public class NetworkImageView extends ImageView {
+    /**
+     * The URL of the network image to load
+     */
+    private String mUrl;
+
+    /**
+     * Resource ID of the image to be used as a placeholder until the network image is loaded. Won't
+     * be set at the same time as mDefaultImageBitmap.
+     */
+    private int mDefaultImageId;
+
+    /**
+     * Bitmap of the image to be used as a placeholder until the network image is loaded. Won't be
+     * set at the same time as mDefaultImageId.
+     */
+    @Nullable
+    Bitmap mDefaultImageBitmap;
+
+    /**
+     * Resource ID of the image to be used if the network response fails. Won't be set at the same
+     * time as mErrorImageBitmap.
+     */
+    private int mErrorImageId;
+
+    /**
+     * Bitmap of the image to be used if the network response fails. Won't be set at the same time
+     * as mErrorImageId.
+     */
+    @Nullable
+    private Bitmap mErrorImageBitmap;
+
+    /**
+     * Local copy of the ImageLoader.
+     */
+    private ImageLoader mImageLoader;
+
+    /**
+     * Current ImageContainer. (either in-flight or finished)
+     */
+    private ImageContainer mImageContainer;
+
+    public NetworkImageView(Context context) {
+        this(context, null);
+    }
+
+    public NetworkImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    /**
+     * Sets URL of the image that should be loaded into this view. Note that calling this will
+     * immediately either set the cached image (if available) or the default image specified by
+     * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
+     *
+     * <p>NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} or {@link
+     * NetworkImageView#setDefaultImageBitmap} and {@link NetworkImageView#setErrorImageResId(int)}
+     * or {@link NetworkImageView#setErrorImageBitmap(Bitmap)} should be called prior to calling
+     * this function.
+     *
+     * <p>Must be called from the main thread.
+     *
+     * @param url         The URL that should be loaded into this ImageView.
+     * @param imageLoader ImageLoader that will be used to make the request.
+     */
+    @MainThread
+    public void setImageUrl(String url, ImageLoader imageLoader) {
+        Threads.throwIfNotOnMainThread();
+        mUrl = url;
+        mImageLoader = imageLoader;
+        // The URL has potentially changed. See if we need to load it.
+        loadImageIfNecessary(/* isInLayoutPass= */ false);
+    }
+
+    /**
+     * Sets the default image resource ID to be used for this view until the attempt to load it
+     * completes.
+     *
+     * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageBitmap}.
+     */
+    public void setDefaultImageResId(int defaultImage) {
+        mDefaultImageBitmap = null;
+        mDefaultImageId = defaultImage;
+    }
+
+    /**
+     * Sets the default image bitmap to be used for this view until the attempt to load it
+     * completes.
+     *
+     * <p>This will clear anything set by {@link NetworkImageView#setDefaultImageResId}.
+     */
+    public void setDefaultImageBitmap(Bitmap defaultImage) {
+        mDefaultImageId = 0;
+        mDefaultImageBitmap = defaultImage;
+    }
+
+    /**
+     * Sets the error image resource ID to be used for this view in the event that the image
+     * requested fails to load.
+     *
+     * <p>This will clear anything set by {@link NetworkImageView#setErrorImageBitmap}.
+     */
+    public void setErrorImageResId(int errorImage) {
+        mErrorImageBitmap = null;
+        mErrorImageId = errorImage;
+    }
+
+    /**
+     * Sets the error image bitmap to be used for this view in the event that the image requested
+     * fails to load.
+     *
+     * <p>This will clear anything set by {@link NetworkImageView#setErrorImageResId}.
+     */
+    public void setErrorImageBitmap(Bitmap errorImage) {
+        mErrorImageId = 0;
+        mErrorImageBitmap = errorImage;
+    }
+
+    /**
+     * Loads the image for the view if it isn't already loaded.
+     *
+     * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
+     */
+    void loadImageIfNecessary(final boolean isInLayoutPass) {
+        int width = getWidth();
+        int height = getHeight();
+        ScaleType scaleType = getScaleType();
+
+        boolean wrapWidth = false, wrapHeight = false;
+        if (getLayoutParams() != null) {
+            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
+            wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
+        }
+
+        // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
+        // view, hold off on loading the image.
+        boolean isFullyWrapContent = wrapWidth && wrapHeight;
+        if (width == 0 && height == 0 && !isFullyWrapContent) {
+            return;
+        }
+
+        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
+        // currently loaded image.
+        if (TextUtils.isEmpty(mUrl)) {
+            if (mImageContainer != null) {
+                mImageContainer.cancelRequest();
+                mImageContainer = null;
+            }
+            setDefaultImageOrNull();
+            return;
+        }
+
+        // if there was an old request in this view, check if it needs to be canceled.
+        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
+            if (mImageContainer.getRequestUrl().equals(mUrl)) {
+                // if the request is from the same URL, return.
+                return;
+            } else {
+                // if there is a pre-existing request, cancel it if it's fetching a different URL.
+                mImageContainer.cancelRequest();
+                setDefaultImageOrNull();
+            }
+        }
+
+        // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
+        int maxWidth = wrapWidth ? 0 : width;
+        int maxHeight = wrapHeight ? 0 : height;
+
+        // The pre-existing content of this view didn't match the current URL. Load the new image
+        // from the network.
+
+        // update the ImageContainer to be the new bitmap container.
+        mImageContainer =
+                mImageLoader.get(
+                        mUrl,
+                        new ImageListener() {
+                            @Override
+                            public void onErrorResponse(VolleyError error) {
+                                if (mErrorImageId != 0) {
+                                    setImageResource(mErrorImageId);
+                                } else if (mErrorImageBitmap != null) {
+                                    setImageBitmap(mErrorImageBitmap);
+                                }
+                            }
+
+                            @Override
+                            public void onResponse(
+                                    final ImageContainer response, boolean isImmediate) {
+                                // If this was an immediate response that was delivered inside of a
+                                // layout
+                                // pass do not set the image immediately as it will trigger a
+                                // requestLayout
+                                // inside of a layout. Instead, defer setting the image by posting
+                                // back to
+                                // the main thread.
+                                if (isImmediate && isInLayoutPass) {
+                                    post(
+                                            new Runnable() {
+                                                @Override
+                                                public void run() {
+                                                    onResponse(response, /* isImmediate= */ false);
+                                                }
+                                            });
+                                    return;
+                                }
+
+                                if (response.getBitmap() != null) {
+                                    setImageBitmap(response.getBitmap());
+                                } else if (mDefaultImageId != 0) {
+                                    setImageResource(mDefaultImageId);
+                                } else if (mDefaultImageBitmap != null) {
+                                    setImageBitmap(mDefaultImageBitmap);
+                                }
+                            }
+                        },
+                        maxWidth,
+                        maxHeight,
+                        scaleType);
+    }
+
+    private void setDefaultImageOrNull() {
+        if (mDefaultImageId != 0) {
+            setImageResource(mDefaultImageId);
+        } else if (mDefaultImageBitmap != null) {
+            setImageBitmap(mDefaultImageBitmap);
+        } else {
+            setImageBitmap(null);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        loadImageIfNecessary(/* isInLayoutPass= */ true);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        if (mImageContainer != null) {
+            // If the view was bound to an image request, cancel it and clear
+            // out the image from the view.
+            mImageContainer.cancelRequest();
+            setImageBitmap(null);
+            // also clear out the container so we can reload the image if necessary.
+            mImageContainer = null;
+        }
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        invalidate();
+    }
+}

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

@@ -0,0 +1,49 @@
+/*
+ * 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.toolbox;
+
+import cn.yyxx.support.volley.source.Cache;
+
+/**
+ * A cache that doesn't.
+ */
+public class NoCache implements Cache {
+    @Override
+    public void clear() {
+    }
+
+    @Override
+    public Entry get(String key) {
+        return null;
+    }
+
+    @Override
+    public void put(String key, Entry entry) {
+    }
+
+    @Override
+    public void invalidate(String key, boolean fullExpire) {
+    }
+
+    @Override
+    public void remove(String key) {
+    }
+
+    @Override
+    public void initialize() {
+    }
+}

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

@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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.toolbox;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * A variation of {@link ByteArrayOutputStream} that uses a pool of byte[] buffers instead
+ * of always allocating them fresh, saving on heap churn.
+ */
+public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
+    /**
+     * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
+     * the default size to which the underlying byte array is initialized.
+     */
+    private static final int DEFAULT_SIZE = 256;
+
+    private final ByteArrayPool mPool;
+
+    /**
+     * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
+     * to this instance, the underlying byte array will expand.
+     */
+    public PoolingByteArrayOutputStream(ByteArrayPool pool) {
+        this(pool, DEFAULT_SIZE);
+    }
+
+    /**
+     * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
+     * more than {@code size} bytes are written to this instance, the underlying byte array will
+     * expand.
+     *
+     * @param size initial size for the underlying byte array. The value will be pinned to a default
+     *             minimum size.
+     */
+    public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
+        mPool = pool;
+        buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
+    }
+
+    @Override
+    public void close() throws IOException {
+        mPool.returnBuf(buf);
+        buf = null;
+        super.close();
+    }
+
+    @Override
+    public void finalize() {
+        mPool.returnBuf(buf);
+    }
+
+    /**
+     * Ensures there is enough space in the buffer for the given number of additional bytes.
+     */
+    @SuppressWarnings("UnsafeFinalization")
+    private void expand(int i) {
+        /* Can the buffer handle @i more bytes, if not expand it */
+        if (count + i <= buf.length) {
+            return;
+        }
+        byte[] newbuf = mPool.getBuf((count + i) * 2);
+        System.arraycopy(buf, 0, newbuf, 0, count);
+        mPool.returnBuf(buf);
+        buf = newbuf;
+    }
+
+    @Override
+    public synchronized void write(byte[] buffer, int offset, int len) {
+        expand(len);
+        super.write(buffer, offset, len);
+    }
+
+    @Override
+    public synchronized void write(int oneByte) {
+        expand(1);
+        super.write(oneByte);
+    }
+}

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

@@ -0,0 +1,162 @@
+/*
+ * 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.toolbox;
+
+import android.os.SystemClock;
+
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Response;
+import cn.yyxx.support.volley.source.VolleyError;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A Future that represents a Volley request.
+ *
+ * <p>Used by providing as your response and error listeners. For example:
+ *
+ * <pre>
+ * RequestFuture&lt;JSONObject&gt; future = RequestFuture.newFuture();
+ * MyRequest request = new MyRequest(URL, future, future);
+ *
+ * // If you want to be able to cancel the request:
+ * future.setRequest(requestQueue.add(request));
+ *
+ * // Otherwise:
+ * requestQueue.add(request);
+ *
+ * try {
+ *   JSONObject response = future.get();
+ *   // do something with response
+ * } catch (InterruptedException e) {
+ *   // handle the error
+ * } catch (ExecutionException e) {
+ *   // handle the error
+ * }
+ * </pre>
+ *
+ * @param <T> The type of parsed response this future expects.
+ */
+public class RequestFuture<T> implements Future<T>, Response.Listener<T>, Response.ErrorListener {
+    private Request<?> mRequest;
+    private boolean mResultReceived = false;
+    private T mResult;
+    private VolleyError mException;
+
+    public static <E> RequestFuture<E> newFuture() {
+        return new RequestFuture<>();
+    }
+
+    private RequestFuture() {
+    }
+
+    public void setRequest(Request<?> request) {
+        mRequest = request;
+    }
+
+    @Override
+    public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+        if (mRequest == null) {
+            return false;
+        }
+
+        if (!isDone()) {
+            mRequest.cancel();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public T get() throws InterruptedException, ExecutionException {
+        try {
+            return doGet(/* timeoutMs= */ null);
+        } catch (TimeoutException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    @Override
+    public T get(long timeout, TimeUnit unit)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit));
+    }
+
+    private synchronized T doGet(Long timeoutMs)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        if (mException != null) {
+            throw new ExecutionException(mException);
+        }
+
+        if (mResultReceived) {
+            return mResult;
+        }
+
+        if (timeoutMs == null) {
+            while (!isDone()) {
+                wait(0);
+            }
+        } else if (timeoutMs > 0) {
+            long nowMs = SystemClock.uptimeMillis();
+            long deadlineMs = nowMs + timeoutMs;
+            while (!isDone() && nowMs < deadlineMs) {
+                wait(deadlineMs - nowMs);
+                nowMs = SystemClock.uptimeMillis();
+            }
+        }
+
+        if (mException != null) {
+            throw new ExecutionException(mException);
+        }
+
+        if (!mResultReceived) {
+            throw new TimeoutException();
+        }
+
+        return mResult;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        if (mRequest == null) {
+            return false;
+        }
+        return mRequest.isCanceled();
+    }
+
+    @Override
+    public synchronized boolean isDone() {
+        return mResultReceived || mException != null || isCancelled();
+    }
+
+    @Override
+    public synchronized void onResponse(T response) {
+        mResultReceived = true;
+        mResult = response;
+        notifyAll();
+    }
+
+    @Override
+    public synchronized void onErrorResponse(VolleyError error) {
+        mException = error;
+        notifyAll();
+    }
+}

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

@@ -0,0 +1,106 @@
+/*
+ * 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.toolbox;
+
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+
+import cn.yyxx.support.volley.source.NetworkResponse;
+import cn.yyxx.support.volley.source.Request;
+import cn.yyxx.support.volley.source.Response;
+import cn.yyxx.support.volley.source.Response.ErrorListener;
+import cn.yyxx.support.volley.source.Response.Listener;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A canned request for retrieving the response body at a given URL as a String.
+ */
+public class StringRequest extends Request<String> {
+
+    /**
+     * Lock to guard mListener as it is cleared on cancel() and read on delivery.
+     */
+    private final Object mLock = new Object();
+
+    @Nullable
+    @GuardedBy("mLock")
+    private Listener<String> mListener;
+
+    /**
+     * Creates a new request with the given method.
+     *
+     * @param method        the request {@link Method} to use
+     * @param url           URL to fetch the string at
+     * @param listener      Listener to receive the String response
+     * @param errorListener Error listener, or null to ignore errors
+     */
+    public StringRequest(
+            int method,
+            String url,
+            Listener<String> listener,
+            @Nullable ErrorListener errorListener) {
+        super(method, url, errorListener);
+        mListener = listener;
+    }
+
+    /**
+     * Creates a new GET request.
+     *
+     * @param url           URL to fetch the string at
+     * @param listener      Listener to receive the String response
+     * @param errorListener Error listener, or null to ignore errors
+     */
+    public StringRequest(
+            String url, Listener<String> listener, @Nullable ErrorListener errorListener) {
+        this(Method.GET, url, listener, errorListener);
+    }
+
+    @Override
+    public void cancel() {
+        super.cancel();
+        synchronized (mLock) {
+            mListener = null;
+        }
+    }
+
+    @Override
+    protected void deliverResponse(String response) {
+        Response.Listener<String> listener;
+        synchronized (mLock) {
+            listener = mListener;
+        }
+        if (listener != null) {
+            listener.onResponse(response);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("DefaultCharset")
+    protected Response<String> parseNetworkResponse(NetworkResponse response) {
+        String parsed;
+        try {
+            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+        } catch (UnsupportedEncodingException e) {
+            // Since minSdkVersion = 8, we can't call
+            // new String(response.data, Charset.defaultCharset())
+            // So suppress the warning instead.
+            parsed = new String(response.data);
+        }
+        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
+    }
+}

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

@@ -0,0 +1,14 @@
+package cn.yyxx.support.volley.source.toolbox;
+
+import android.os.Looper;
+
+final class Threads {
+    private Threads() {
+    }
+
+    static void throwIfNotOnMainThread() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw new IllegalStateException("Must be invoked from the main thread.");
+        }
+    }
+}

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

@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2012 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.toolbox;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.http.AndroidHttpClient;
+import android.os.Build;
+
+import cn.yyxx.support.volley.source.Network;
+import cn.yyxx.support.volley.source.RequestQueue;
+
+import java.io.File;
+
+public class Volley {
+
+    /**
+     * Default on-disk cache directory.
+     */
+    private static final String DEFAULT_CACHE_DIR = "volley";
+
+    /**
+     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+     *
+     * @param context A {@link Context} to use for creating the cache dir.
+     * @param stack   A {@link BaseHttpStack} to use for the network, or null for default.
+     * @return A started {@link RequestQueue} instance.
+     */
+    public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
+        BasicNetwork network;
+        if (stack == null) {
+            if (Build.VERSION.SDK_INT >= 9) {
+                network = new BasicNetwork(new HurlStack());
+            } else {
+                // Prior to Gingerbread, HttpUrlConnection was unreliable.
+                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
+                // At some point in the future we'll move our minSdkVersion past Froyo and can
+                // delete this fallback (along with all Apache HTTP code).
+                String userAgent = "volley/0";
+                try {
+                    String packageName = context.getPackageName();
+                    PackageInfo info = context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
+                    userAgent = packageName + "/" + info.versionCode;
+                } catch (NameNotFoundException e) {
+                    e.printStackTrace();
+                }
+
+                network = new BasicNetwork(new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
+            }
+        } else {
+            network = new BasicNetwork(stack);
+        }
+
+        return newRequestQueue(context, network);
+    }
+
+    /**
+     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+     *
+     * @param context A {@link Context} to use for creating the cache dir.
+     * @param stack   An {@link HttpStack} to use for the network, or null for default.
+     * @return A started {@link RequestQueue} instance.
+     * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending
+     * on Apache HTTP. This method may be removed in a future release of Volley.
+     */
+    @Deprecated
+    @SuppressWarnings("deprecation")
+    public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
+        if (stack == null) {
+            return newRequestQueue(context, (BaseHttpStack) null);
+        }
+        return newRequestQueue(context, new BasicNetwork(stack));
+    }
+
+    private static RequestQueue newRequestQueue(Context context, Network network) {
+        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
+        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
+        queue.start();
+        return queue;
+    }
+
+    /**
+     * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+     *
+     * @param context A {@link Context} to use for creating the cache dir.
+     * @return A started {@link RequestQueue} instance.
+     */
+    public static RequestQueue newRequestQueue(Context context) {
+        return newRequestQueue(context, (BaseHttpStack) null);
+    }
+}