Преглед на файлове

尝试添加Aria到本地;修改aria默认的user-agent

zengjiebin преди 8 години
родител
ревизия
1d2e698d50
променени са 100 файла, в които са добавени 11499 реда и са изтрити 0 реда
  1. 2 0
      .idea/gradle.xml
  2. 3 0
      Aria/.gitignore
  3. 12 0
      Aria/bintray-release.gradle
  4. 31 0
      Aria/build.gradle
  5. 143 0
      Aria/jcenter.gradle
  6. 17 0
      Aria/proguard-rules.pro
  7. 30 0
      Aria/src/androidTest/java/com/arialyy/downloadutil/ApplicationTest.java
  8. 10 0
      Aria/src/main/AndroidManifest.xml
  9. 123 0
      Aria/src/main/java/com/arialyy/aria/core/Aria.java
  10. 500 0
      Aria/src/main/java/com/arialyy/aria/core/AriaManager.java
  11. 363 0
      Aria/src/main/java/com/arialyy/aria/core/ConfigHelper.java
  12. 558 0
      Aria/src/main/java/com/arialyy/aria/core/Configuration.java
  13. 83 0
      Aria/src/main/java/com/arialyy/aria/core/FtpUrlEntity.java
  14. 139 0
      Aria/src/main/java/com/arialyy/aria/core/WidgetLiftManager.java
  15. 36 0
      Aria/src/main/java/com/arialyy/aria/core/command/AbsCmd.java
  16. 33 0
      Aria/src/main/java/com/arialyy/aria/core/command/AbsCmdFactory.java
  17. 40 0
      Aria/src/main/java/com/arialyy/aria/core/command/ICmd.java
  18. 73 0
      Aria/src/main/java/com/arialyy/aria/core/command/group/AbsGroupCmd.java
  19. 37 0
      Aria/src/main/java/com/arialyy/aria/core/command/group/GroupCancelCmd.java
  20. 78 0
      Aria/src/main/java/com/arialyy/aria/core/command/group/GroupCmdFactory.java
  21. 37 0
      Aria/src/main/java/com/arialyy/aria/core/command/group/GroupStartCmd.java
  22. 37 0
      Aria/src/main/java/com/arialyy/aria/core/command/group/GroupStopCmd.java
  23. 185 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/AbsNormalCmd.java
  24. 45 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/AddCmd.java
  25. 42 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/CancelAllCmd.java
  26. 52 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/CancelCmd.java
  27. 63 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/HighestPriorityCmd.java
  28. 111 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/NormalCmdFactory.java
  29. 149 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/ResumeAllCmd.java
  30. 167 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/StartCmd.java
  31. 20 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/StopAllCmd.java
  32. 51 0
      Aria/src/main/java/com/arialyy/aria/core/command/normal/StopCmd.java
  33. 514 0
      Aria/src/main/java/com/arialyy/aria/core/common/AbsFileer.java
  34. 298 0
      Aria/src/main/java/com/arialyy/aria/core/common/AbsFtpInfoThread.java
  35. 141 0
      Aria/src/main/java/com/arialyy/aria/core/common/AbsFtpThreadTask.java
  36. 265 0
      Aria/src/main/java/com/arialyy/aria/core/common/AbsThreadTask.java
  37. 31 0
      Aria/src/main/java/com/arialyy/aria/core/common/CompleteInfo.java
  38. 66 0
      Aria/src/main/java/com/arialyy/aria/core/common/IUtil.java
  39. 32 0
      Aria/src/main/java/com/arialyy/aria/core/common/OnFileInfoCallback.java
  40. 107 0
      Aria/src/main/java/com/arialyy/aria/core/common/ProxyHelper.java
  41. 31 0
      Aria/src/main/java/com/arialyy/aria/core/common/QueueMod.java
  42. 45 0
      Aria/src/main/java/com/arialyy/aria/core/common/RecordWrapper.java
  43. 31 0
      Aria/src/main/java/com/arialyy/aria/core/common/RequestEnum.java
  44. 75 0
      Aria/src/main/java/com/arialyy/aria/core/common/StateConstance.java
  45. 25 0
      Aria/src/main/java/com/arialyy/aria/core/common/SubThreadConfig.java
  46. 82 0
      Aria/src/main/java/com/arialyy/aria/core/common/TaskRecord.java
  47. 50 0
      Aria/src/main/java/com/arialyy/aria/core/common/ThreadRecord.java
  48. 66 0
      Aria/src/main/java/com/arialyy/aria/core/delegate/FtpDelegate.java
  49. 139 0
      Aria/src/main/java/com/arialyy/aria/core/delegate/HttpHeaderDelegate.java
  50. 97 0
      Aria/src/main/java/com/arialyy/aria/core/download/AbsDownloadTarget.java
  51. 180 0
      Aria/src/main/java/com/arialyy/aria/core/download/BaseDListener.java
  52. 165 0
      Aria/src/main/java/com/arialyy/aria/core/download/BaseGroupTarget.java
  53. 182 0
      Aria/src/main/java/com/arialyy/aria/core/download/BaseNormalTarget.java
  54. 166 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadEntity.java
  55. 79 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupEntity.java
  56. 95 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupListener.java
  57. 251 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupTarget.java
  58. 124 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupTask.java
  59. 63 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupTaskEntity.java
  60. 35 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadListener.java
  61. 423 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadReceiver.java
  62. 105 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadTarget.java
  63. 183 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadTask.java
  64. 117 0
      Aria/src/main/java/com/arialyy/aria/core/download/DownloadTaskEntity.java
  65. 104 0
      Aria/src/main/java/com/arialyy/aria/core/download/FtpDirDownloadTarget.java
  66. 92 0
      Aria/src/main/java/com/arialyy/aria/core/download/FtpDownloadTarget.java
  67. 527 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/AbsGroupUtil.java
  68. 167 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/ConnectionHelp.java
  69. 149 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/DownloadGroupUtil.java
  70. 103 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/Downloader.java
  71. 87 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpDirDownloadUtil.java
  72. 93 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpDirInfoThread.java
  73. 46 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpFileInfoThread.java
  74. 159 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpThreadTask.java
  75. 245 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/HttpFileInfoThread.java
  76. 195 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/HttpThreadTask.java
  77. 68 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/IDownloadGroupListener.java
  78. 133 0
      Aria/src/main/java/com/arialyy/aria/core/download/downloader/SimpleDownloadUtil.java
  79. 44 0
      Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DGEWrapper.java
  80. 44 0
      Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DGSTEWrapper.java
  81. 65 0
      Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DGTEWrapper.java
  82. 46 0
      Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DTEWrapper.java
  83. 205 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsEntity.java
  84. 98 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsGroupEntity.java
  85. 65 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsGroupTask.java
  86. 26 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsGroupTaskEntity.java
  87. 108 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsNormalEntity.java
  88. 43 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsNormalTask.java
  89. 23 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsNormalTaskEntity.java
  90. 34 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsReceiver.java
  91. 274 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsTarget.java
  92. 186 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsTask.java
  93. 248 0
      Aria/src/main/java/com/arialyy/aria/core/inf/AbsTaskEntity.java
  94. 34 0
      Aria/src/main/java/com/arialyy/aria/core/inf/GroupSendParams.java
  95. 35 0
      Aria/src/main/java/com/arialyy/aria/core/inf/IDownloadListener.java
  96. 60 0
      Aria/src/main/java/com/arialyy/aria/core/inf/IEntity.java
  97. 65 0
      Aria/src/main/java/com/arialyy/aria/core/inf/IEventListener.java
  98. 43 0
      Aria/src/main/java/com/arialyy/aria/core/inf/IFtpTarget.java
  99. 52 0
      Aria/src/main/java/com/arialyy/aria/core/inf/IHttpHeaderTarget.java
  100. 0 0
      Aria/src/main/java/com/arialyy/aria/core/inf/IReceiver.java

+ 2 - 0
.idea/gradle.xml

@@ -8,6 +8,8 @@
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/Aria" />
+            <option value="$PROJECT_DIR$/AriaAnnotations" />
             <option value="$PROJECT_DIR$/app" />
             <option value="$PROJECT_DIR$/datashare" />
             <option value="$PROJECT_DIR$/kfzslibrary" />

+ 3 - 0
Aria/.gitignore

@@ -0,0 +1,3 @@
+/build
+gradle.properties
+downloadutil.iml

+ 12 - 0
Aria/bintray-release.gradle

@@ -0,0 +1,12 @@
+apply plugin: 'bintray-release'
+publish {
+  artifactId = 'aria-core'
+  userOrg = rootProject.userOrg
+  groupId = rootProject.groupId
+  //  uploadName = rootProject.uploadName
+  uploadName = 'AriaApi'
+  publishVersion = rootProject.publishVersion
+  description = rootProject.description
+  website = rootProject.website
+  licences = rootProject.licences
+}

+ 31 - 0
Aria/build.gradle

@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+  compileSdkVersion androidConfigs.compileSdkVersion
+  buildToolsVersion androidConfigs.buildToolsVersion
+
+  defaultConfig {
+    minSdkVersion 18
+    targetSdkVersion 27
+    versionCode 329
+    versionName "3.2.9"
+  }
+  buildTypes {
+    release {
+      minifyEnabled false
+      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+    }
+  }
+  lintOptions {
+    abortOnError false
+  }
+}
+
+dependencies {
+  compile 'com.android.support:appcompat-v7:27.1.0'
+  compile project(':AriaAnnotations')
+  compile 'com.arialyy.aria:aria-ftp-plug:1.0.3'
+
+  //  compile project(':AriaFtpPlug')
+}
+apply from: 'bintray-release.gradle'

+ 143 - 0
Aria/jcenter.gradle

@@ -0,0 +1,143 @@
+group = PROJ_GROUP_ID
+version = PROJ_VERSION
+project.archivesBaseName = PROJ_ARTIFACT_ID
+apply plugin: 'com.jfrog.bintray'
+apply plugin: 'com.github.dcendents.android-maven'
+//输入:gradlew bintray 执行
+
+//############################## jar、sources、doc 打包 start #######################################
+task sourcesJar(type: Jar) {
+  from android.sourceSets.main.java.srcDirs
+  classifier = 'sources'
+}
+
+task javadoc(type: Javadoc) {
+  source = android.sourceSets.main.java.srcDirs
+  classpath += configurations.compile
+  classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+  classifier = 'javadoc'
+  from javadoc.destinationDir
+}
+
+javadoc {
+  options {
+    encoding "UTF-8"
+    charSet 'UTF-8'
+    author true
+    version true
+    links "http://docs.oracle.com/javase/7/docs/api"
+    title PROJ_ARTIFACT_ID
+  }
+}
+
+//添加以下信息避免JAVADOC打包时引用其它类库而出现问题,比如出现以下错误
+// xxxx.java:20: 错误: 找不到符号
+// public static <T> T create(JsonElement json, Class<T> classOfModel) {
+//    ^
+//    符号:   类 JsonElement
+//    位置: 类 xxxx
+android.libraryVariants.all { variant ->
+  println variant.javaCompile.classpath.files
+  if (variant.name == 'release') {
+    //我们只需 release 的 javadoc
+    task("generate${variant.name.capitalize()}Javadoc", type: Javadoc) {
+      // title = ''
+      // description = ''
+      source = variant.javaCompile.source
+      classpath = files(variant.javaCompile.classpath.files, project.android.getBootClasspath())
+      options {
+        encoding "utf-8"
+        links "http://docs.oracle.com/javase/7/docs/api/"
+        linksOffline "http://d.android.com/reference", "${android.sdkDirectory}/docs/reference"
+      }
+      exclude '**/BuildConfig.java'
+      exclude '**/R.java'
+    }
+    task("javadoc${variant.name.capitalize()}Jar", type: Jar,
+        dependsOn: "generate${variant.name.capitalize()}Javadoc") {
+      classifier = 'javadoc'
+      from tasks.getByName("generate${variant.name.capitalize()}Javadoc").destinationDir
+    }
+    artifacts {
+      archives tasks.getByName("javadoc${variant.name.capitalize()}Jar")
+    }
+  }
+}
+
+artifacts {
+  archives javadocJar
+  archives sourcesJar
+}
+//############################## jar、sources、doc 打包 end #######################################
+
+//################################# jcenter 上传配置 start #########################################
+bintray {
+//  user = hasProperty("bintrayUser") ? getProperty("bintrayUser") : getProperty("BINTRAY_USER")
+//  groupName = hasProperty("bintrayKey") ? getProperty("bintrayKey") : getProperty("BINTRAY_KEY")
+  user = BINTRAY_USER
+  key = BINTRAY_KEY
+  configurations = ['archives']
+
+  pkg {
+    repo = PROJ_REPO
+    name = PROJ_NAME
+    desc = PROJ_DESCRIPTION
+    websiteUrl = PROJ_WEB_SITE_URL
+    issueTrackerUrl = PROJ_ISSUE_TRACKER_URL
+    vcsUrl = PROJ_VCS_URL
+    publish = true
+    publicDownloadNumbers = true
+    licenses = LICENSES
+//    version {
+//      desc = libraryDescription
+//      gpg {
+//        sign = true //Determines whether to GPG sign the files. The default is false
+//        passphrase = properties.getProperty("bintray.gpg.password")
+//        //Optional. The passphrase for GPG signing'
+//      }
+//    }
+  }
+}
+
+//install
+install {
+  repositories.mavenInstaller {
+    // This generates POM.xml with proper parameters
+    pom {
+      project {
+        packaging 'aar'
+        groupId PROJ_GROUP_ID
+        artifactId PROJ_ARTIFACT_ID
+
+        // Add your description here
+        name PROJ_NAME
+        description PROJ_DESCRIPTION
+        url PROJ_WEB_SITE_URL
+
+        // Set your license
+        licenses {
+          license {
+            name LICENSE_NAME
+            url LICENSE_URL
+          }
+        }
+        developers {
+          developer {
+            id DEVELOPER_ID
+            name DEVELOPER_NAME
+            email DEVELOPER_EMAIL
+          }
+        }
+        scm {
+          connection PROJ_WEB_SITE_URL
+          developerConnection PROJ_WEB_SITE_URL
+          url PROJ_VCS_URL
+        }
+      }
+    }
+  }
+}
+//################################# jcenter 上传配置 end #########################################

+ 17 - 0
Aria/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 30 - 0
Aria/src/androidTest/java/com/arialyy/downloadutil/ApplicationTest.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 AriaLyy(DownloadUtil)
+ *
+ * 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 com.arialyy.downloadutil;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+  public ApplicationTest() {
+    super(Application.class);
+  }
+}

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

@@ -0,0 +1,10 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.arialyy.aria">
+
+  <application
+      android:allowBackup="true"
+      android:supportsRtl="true">
+
+  </application>
+
+</manifest>

+ 123 - 0
Aria/src/main/java/com/arialyy/aria/core/Aria.java

@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import com.arialyy.aria.core.download.DownloadReceiver;
+import com.arialyy.aria.core.upload.UploadReceiver;
+
+/**
+ * Created by lyy on 2016/12/1.
+ * https://github.com/AriaLyy/Aria
+ * Aria启动,管理全局任务
+ * <pre>
+ *   <code>
+ *   //下载
+ *   Aria.download(this)
+ *       .load(URL)     //下载地址,必填
+ *       //文件保存路径,必填
+ *       .setDownloadPath(Environment.getExternalStorageDirectory().getPath() + "/test.apk")
+ *       .start();
+ *   </code>
+ *   <code>
+ *    //上传
+ *    Aria.upload(this)
+ *        .load(filePath)     //文件路径,必填
+ *        .setUploadUrl(uploadUrl)  //上传路径,必填
+ *        .setAttachment(fileKey)   //服务器读取文件的key,必填
+ *        .start();
+ *   </code>
+ * </pre>
+ *
+ * 如果你需要在【Activity、Service、Application、DialogFragment、Fragment、PopupWindow、Dialog】
+ * 之外的java中使用Aria,那么你应该在Application或Activity初始化的时候调用{@link #init(Context)}对Aria进行初始化
+ * 然后才能使用{@link #download(Object)}、{@link #upload(Object)}
+ *
+ * <pre>
+ *   <code>
+ *       Aria.init(getContext());
+ *
+ *      Aria.download(this)
+ *       .load(URL)     //下载地址,必填
+ *       //文件保存路径,必填
+ *       .setDownloadPath(Environment.getExternalStorageDirectory().getPath() + "/test.apk")
+ *       .start();
+ *
+ *   </code>
+ *
+ * </pre>
+ */
+@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) public class Aria {
+
+  private Aria() {
+  }
+
+  /**
+   * 初始化下载
+   *
+   * @param context 支持类型有【Activity、Service、Application、DialogFragment、Fragment、PopupWindow、Dialog】
+   */
+  public static DownloadReceiver download(Context context) {
+    return get(context).download(context);
+  }
+
+  /**
+   * 初始化上传
+   *
+   * @param context 支持类型有【Activity、Service、Application、DialogFragment、Fragment、PopupWindow、Dialog】
+   */
+  public static UploadReceiver upload(Context context) {
+    return get(context).upload(context);
+  }
+
+  /**
+   * 在任意对象中初始化下载,前提是你需要在Application或Activity初始化的时候调用{@link #init(Context)}对Aria进行初始化
+   *
+   * @param obj 任意对象
+   */
+  public static DownloadReceiver download(Object obj) {
+    return AriaManager.getInstance().download(obj);
+  }
+
+  /**
+   * 在任意对象中初始化上传,前提是你需要在Application或Activity初始化的时候调用{@link #init(Context)}对Aria进行初始化
+   *
+   * @param obj 任意对象
+   */
+  public static UploadReceiver upload(Object obj) {
+    return AriaManager.getInstance().upload(obj);
+  }
+
+  /**
+   * 处理通用事件
+   */
+  public static AriaManager get(Context context) {
+    return AriaManager.getInstance(context);
+  }
+
+  /**
+   * 初始化Aria,如果你需要在【Activity、Service、Application、DialogFragment、Fragment、PopupWindow、Dialog】
+   * 之外的java中使用Aria,那么你应该在Application或Activity初始化的时候调用本方法对Aria进行初始化
+   * 只需要初始化一次就可以
+   * {@link #download(Object)}、{@link #upload(Object)}
+   */
+  public static AriaManager init(Context context) {
+    return AriaManager.getInstance(context);
+  }
+}

+ 500 - 0
Aria/src/main/java/com/arialyy/aria/core/AriaManager.java

@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Application;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.widget.PopupWindow;
+import com.arialyy.aria.core.command.ICmd;
+import com.arialyy.aria.core.common.QueueMod;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadGroupEntity;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadReceiver;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.AbsReceiver;
+import com.arialyy.aria.core.inf.IReceiver;
+import com.arialyy.aria.core.upload.UploadEntity;
+import com.arialyy.aria.core.upload.UploadReceiver;
+import com.arialyy.aria.core.upload.UploadTaskEntity;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.orm.DelegateWrapper;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.AriaCrashHandler;
+import com.arialyy.aria.util.CommonUtil;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import org.xml.sax.SAXException;
+
+/**
+ * Created by lyy on 2016/12/1.
+ * https://github.com/AriaLyy/Aria
+ * Aria管理器,任务操作在这里执行
+ */
+@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public class AriaManager {
+  private static final String TAG = "AriaManager";
+  private static final String DOWNLOAD = "_download";
+  private static final String UPLOAD = "_upload";
+  public static final Object LOCK = new Object();
+  public static final String DOWNLOAD_TEMP_DIR = "/Aria/temp/download/";
+  public static final String UPLOAD_TEMP_DIR = "/Aria/temp/upload/";
+
+  @SuppressLint("StaticFieldLeak") private static volatile AriaManager INSTANCE = null;
+  private Map<String, AbsReceiver> mReceivers = new ConcurrentHashMap<>();
+  public static Context APP;
+  private List<ICmd> mCommands = new ArrayList<>();
+  private Configuration.DownloadConfig mDConfig;
+  private Configuration.UploadConfig mUConfig;
+  private Configuration.AppConfig mAConfig;
+
+  private AriaManager(Context context) {
+    APP = context.getApplicationContext();
+    initDb(APP);
+    regAppLifeCallback(context);
+    initConfig();
+    initAria();
+  }
+
+  public static AriaManager getInstance(Context context) {
+    if (INSTANCE == null) {
+      synchronized (LOCK) {
+        INSTANCE = new AriaManager(context);
+      }
+    }
+    return INSTANCE;
+  }
+
+  static AriaManager getInstance() {
+    if (INSTANCE == null) {
+      throw new NullPointerException("请在Application或Activity初始化时调用一次Aria.init(context)方法进行初始化操作");
+    }
+    return INSTANCE;
+  }
+
+  private void initDb(Context context) {
+    String dbBase = context.getFilesDir().getPath() + context.getPackageName() + "/databases/";
+    File db = new File(dbBase + "AriaLyyDb");
+    File dbConfig = new File(dbBase + "AriaLyyDb-journal");
+    if (db.exists()) {
+      db.renameTo(new File(dbBase + "AndroidAria.db"));
+      // 如果数据库是在/data/data/{packagename}/databases/下面,journal文件因权限问题将无法删除和重命名
+      if (dbConfig.exists()) {
+        dbConfig.delete();
+      }
+    }
+    DelegateWrapper.init(context.getApplicationContext());
+  }
+
+  private void initAria() {
+    if (mAConfig.getUseAriaCrashHandler()) {
+      Thread.setDefaultUncaughtExceptionHandler(new AriaCrashHandler());
+    }
+    mAConfig.setLogLevel(mAConfig.getLogLevel());
+  }
+
+  public Map<String, AbsReceiver> getReceiver() {
+    return mReceivers;
+  }
+
+  /**
+   * 设置上传任务的执行队列类型,后续版本会删除该api,请使用:
+   * <pre>
+   *   <code>
+   *     Aria.get(this).getUploadConfig().setQueueMod(mod.tag)
+   *   </code>
+   * </pre>
+   *
+   * @param mod {@link QueueMod}
+   * @deprecated 后续版本会删除该api
+   */
+  @Deprecated
+  public AriaManager setUploadQueueMod(QueueMod mod) {
+    mUConfig.setQueueMod(mod.tag);
+    return this;
+  }
+
+  /**
+   * 设置下载任务的执行队列类型,后续版本会删除该api,请使用:
+   * <pre>
+   *   <code>
+   *     Aria.get(this).getDownloadConfig().setQueueMod(mod.tag)
+   *   </code>
+   * </pre>
+   *
+   * @param mod {@link QueueMod}
+   * @deprecated 后续版本会删除该api
+   */
+  @Deprecated
+  public AriaManager setDownloadQueueMod(QueueMod mod) {
+    mDConfig.setQueueMod(mod.tag);
+    return this;
+  }
+
+  /**
+   * 如果需要在代码中修改下载配置,请使用以下方法
+   * <pre>
+   *   <code>
+   *     //修改最大任务队列数
+   *     Aria.get(this).getDownloadConfig().setMaxTaskNum(3);
+   *   </code>
+   * </pre>
+   */
+  public Configuration.DownloadConfig getDownloadConfig() {
+    return mDConfig;
+  }
+
+  /**
+   * 如果需要在代码中修改下载配置,请使用以下方法
+   * <pre>
+   *   <code>
+   *     //修改最大任务队列数
+   *     Aria.get(this).getUploadConfig().setMaxTaskNum(3);
+   *   </code>
+   * </pre>
+   */
+  public Configuration.UploadConfig getUploadConfig() {
+    return mUConfig;
+  }
+
+  /**
+   * 获取APP配置
+   */
+  public Configuration.AppConfig getAppConfig() {
+    return mAConfig;
+  }
+
+  /**
+   * 设置命令
+   */
+  public AriaManager setCmd(ICmd command) {
+    mCommands.add(command);
+    return this;
+  }
+
+  /**
+   * 设置一组命令
+   */
+  public <T extends ICmd> AriaManager setCmds(List<T> commands) {
+    if (commands != null && commands.size() > 0) {
+      mCommands.addAll(commands);
+    }
+    return this;
+  }
+
+  /**
+   * 执行所有设置的命令
+   */
+  public synchronized void exe() {
+    for (ICmd command : mCommands) {
+      command.executeCmd();
+    }
+    mCommands.clear();
+  }
+
+  /**
+   * 处理下载操作
+   */
+  DownloadReceiver download(Object obj) {
+    IReceiver receiver = mReceivers.get(getKey(true, obj));
+    if (receiver == null) {
+      receiver = putReceiver(true, obj);
+    }
+    return (receiver instanceof DownloadReceiver) ? (DownloadReceiver) receiver : null;
+  }
+
+  /**
+   * 处理上传操作
+   */
+  UploadReceiver upload(Object obj) {
+    IReceiver receiver = mReceivers.get(getKey(false, obj));
+    if (receiver == null) {
+      receiver = putReceiver(false, obj);
+    }
+    return (receiver instanceof UploadReceiver) ? (UploadReceiver) receiver : null;
+  }
+
+  /**
+   * 删除任务记录
+   *
+   * @param type 需要删除的任务类型,1、表示单任务下载。2、表示任务组下载。3、单任务上传
+   * @param key 下载为保存路径、任务组为任务组名、上传为上传文件路径
+   */
+  public void delRecord(int type, String key) {
+    switch (type) {
+      case 1:
+        DbEntity.deleteData(DownloadEntity.class, "url=?", key);
+        DbEntity.deleteData(DownloadTaskEntity.class, "key=? and isGroupTask='false'", key);
+        break;
+      case 2:
+        DbEntity.deleteData(DownloadGroupEntity.class, "groupName=?", key);
+        DbEntity.deleteData(DownloadGroupTaskEntity.class, "key=?", key);
+        break;
+      case 3:
+        DbEntity.deleteData(UploadEntity.class, "filePath=?", key);
+        DbEntity.deleteData(UploadTaskEntity.class, "key=?", key);
+        break;
+    }
+  }
+
+  private IReceiver putReceiver(boolean isDownload, Object obj) {
+    final String key = getKey(isDownload, obj);
+    IReceiver receiver = mReceivers.get(key);
+    boolean needRmReceiver = false;
+    // 监控Dialog、fragment、popupWindow的生命周期
+    final WidgetLiftManager widgetLiftManager = new WidgetLiftManager();
+    if (obj instanceof Dialog) {
+      needRmReceiver = widgetLiftManager.handleDialogLift((Dialog) obj);
+    } else if (obj instanceof PopupWindow) {
+      needRmReceiver = widgetLiftManager.handlePopupWindowLift((PopupWindow) obj);
+    } else if (obj instanceof DialogFragment) {
+      needRmReceiver = widgetLiftManager.handleDialogFragmentLift((DialogFragment) obj);
+    } else if (obj instanceof android.app.DialogFragment) {
+      needRmReceiver = widgetLiftManager.handleDialogFragmentLift((android.app.DialogFragment) obj);
+    }
+
+    if (receiver == null) {
+      AbsReceiver absReceiver;
+      if (isDownload) {
+        absReceiver = new DownloadReceiver();
+      } else {
+        absReceiver = new UploadReceiver();
+      }
+      receiver = checkTarget(key, absReceiver, obj, needRmReceiver);
+    }
+    return receiver;
+  }
+
+  /**
+   * 不允许在"onDestroy"、"finish"、"onStop"这三个方法中添加注册器
+   */
+  private AbsReceiver checkTarget(String key, AbsReceiver receiver, Object obj,
+      boolean needRmReceiver) {
+    StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+    int i = 0;
+    for (StackTraceElement e : stack) {
+      String name = e.getClassName();
+      if (!name.equals(AriaManager.class.getName())) {
+        i++;
+      } else {
+        break;
+      }
+    }
+    i += 4;
+    String methodName = stack[i].getMethodName();
+    boolean isDestroyed =
+        methodName.equals("onDestroy") || methodName.equals("finish") || methodName.equals(
+            "onStop");
+
+    if (isDestroyed) {
+      ALog.e(TAG,
+          "请不要在Activity或Fragment的onDestroy、finish、onStop等方法中注册Aria,Aria的unRegister会在Activity页面销毁时自动执行");
+    }
+
+    if (obj instanceof Activity && isDestroyed) {
+      return receiver;
+    } else if (obj instanceof Fragment && isDestroyed) {
+      return receiver;
+    }
+    receiver.targetName = obj.getClass().getName();
+    receiver.obj = obj;
+    receiver.needRmListener = needRmReceiver;
+    mReceivers.put(key, receiver);
+    return receiver;
+  }
+
+  /**
+   * 根据功能类型和控件类型获取对应的key
+   */
+  private String getKey(boolean isDownload, Object obj) {
+    String clsName = obj.getClass().getName();
+    String key;
+    if (obj instanceof DialogFragment) {
+      key = clsName + "_" + ((DialogFragment) obj).getActivity().getClass().getName();
+    } else if (obj instanceof android.app.DialogFragment) {
+      key = clsName + "_" + ((android.app.DialogFragment) obj).getActivity().getClass().getName();
+    } else if (obj instanceof android.support.v4.app.Fragment) {
+      key = clsName + "_" + ((Fragment) obj).getActivity().getClass().getName();
+    } else if (obj instanceof android.app.Fragment) {
+      key = clsName + "_" + ((android.app.Fragment) obj).getActivity().getClass().getName();
+    } else if (obj instanceof Dialog) {
+      Activity activity = ((Dialog) obj).getOwnerActivity();
+      if (activity != null) {
+        key = clsName + "_" + activity.getClass().getName();
+      } else {
+        key = clsName;
+      }
+    } else if (obj instanceof PopupWindow) {
+      Context context = ((PopupWindow) obj).getContentView().getContext();
+      if (context instanceof Activity) {
+        key = clsName + "_" + context.getClass().getName();
+      } else {
+        key = clsName;
+      }
+    } else {
+      key = clsName;
+    }
+    key += (isDownload ? DOWNLOAD : UPLOAD) + obj.hashCode();
+    return key;
+  }
+
+  /**
+   * 初始化配置文件
+   */
+  private void initConfig() {
+    File xmlFile = new File(APP.getFilesDir().getPath() + Configuration.XML_FILE);
+    File tempDir = new File(APP.getFilesDir().getPath() + "/temp");
+    if (!xmlFile.exists()) {
+      loadConfig();
+    } else {
+      try {
+        String md5Code = CommonUtil.getFileMD5(xmlFile);
+        File file = new File(APP.getFilesDir().getPath() + "/temp.xml");
+        if (file.exists()) {
+          file.delete();
+        }
+        CommonUtil.createFileFormInputStream(APP.getAssets().open("aria_config.xml"),
+            file.getPath());
+        if (!CommonUtil.checkMD5(md5Code, file)) {
+          loadConfig();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+    mDConfig = Configuration.DownloadConfig.getInstance();
+    mUConfig = Configuration.UploadConfig.getInstance();
+    mAConfig = Configuration.AppConfig.getInstance();
+    if (tempDir.exists()) {
+      File newDir = new File(APP.getFilesDir().getPath() + DOWNLOAD_TEMP_DIR);
+      newDir.mkdirs();
+      tempDir.renameTo(newDir);
+    }
+  }
+
+  /**
+   * 加载配置文件
+   */
+  private void loadConfig() {
+    try {
+      ConfigHelper helper = new ConfigHelper();
+      SAXParserFactory factory = SAXParserFactory.newInstance();
+      SAXParser parser = factory.newSAXParser();
+      parser.parse(APP.getAssets().open("aria_config.xml"), helper);
+      CommonUtil.createFileFormInputStream(APP.getAssets().open("aria_config.xml"),
+          APP.getFilesDir().getPath() + Configuration.XML_FILE);
+    } catch (ParserConfigurationException | IOException | SAXException e) {
+      ALog.e(TAG, e.toString());
+    }
+  }
+
+  /**
+   * 注册APP生命周期回调
+   */
+  private void regAppLifeCallback(Context context) {
+    Context app = context.getApplicationContext();
+    if (app instanceof Application) {
+      LifeCallback lifeCallback = new LifeCallback();
+      ((Application) app).registerActivityLifecycleCallbacks(lifeCallback);
+    }
+  }
+
+  /**
+   * 移除指定对象的receiver
+   */
+  public void removeReceiver(Object obj) {
+    if (obj == null) return;
+    String clsName = obj.getClass().getName();
+    for (Iterator<Map.Entry<String, AbsReceiver>> iter = mReceivers.entrySet().iterator();
+        iter.hasNext(); ) {
+      Map.Entry<String, AbsReceiver> entry = iter.next();
+      String key = entry.getKey();
+      if (key.contains(clsName)) {
+        iter.remove();
+      }
+    }
+  }
+
+  /**
+   * Aria注册对象被销毁时调用
+   */
+  void destroySchedulerListener(Object obj) {
+    String clsName = obj.getClass().getName();
+    for (Iterator<Map.Entry<String, AbsReceiver>> iter = mReceivers.entrySet().iterator();
+        iter.hasNext(); ) {
+      Map.Entry<String, AbsReceiver> entry = iter.next();
+      String key = entry.getKey();
+      if (key.contains(clsName)) {
+        AbsReceiver receiver = mReceivers.get(key);
+        if (receiver != null) {
+          receiver.unRegisterListener();
+          receiver.destroy();
+        }
+        iter.remove();
+      }
+    }
+  }
+
+  /**
+   * Activity生命周期
+   */
+  private class LifeCallback implements Application.ActivityLifecycleCallbacks {
+
+    @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+
+    }
+
+    @Override public void onActivityStarted(Activity activity) {
+
+    }
+
+    @Override public void onActivityResumed(Activity activity) {
+
+    }
+
+    @Override public void onActivityPaused(Activity activity) {
+
+    }
+
+    @Override public void onActivityStopped(Activity activity) {
+
+    }
+
+    @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+
+    }
+
+    @Override public void onActivityDestroyed(Activity activity) {
+      destroySchedulerListener(activity);
+      // TODO: 2018/4/11 维护一个activity堆栈,应用被kill,activity会回调onDestroy方法,需要考虑server后台情况
+    }
+  }
+}

+ 363 - 0
Aria/src/main/java/com/arialyy/aria/core/ConfigHelper.java

@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core;
+
+import android.text.TextUtils;
+import com.arialyy.aria.util.ALog;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Created by lyy on 2017/5/22.
+ * 读取配置文件
+ */
+class ConfigHelper extends DefaultHandler {
+  private final String TAG = "ConfigHelper";
+
+  private boolean isDownloadConfig = false, isUploadConfig = false, isAppConfig = false;
+  private Configuration.DownloadConfig mDownloadConfig = Configuration.DownloadConfig.getInstance();
+  private Configuration.UploadConfig mUploadConfig = Configuration.UploadConfig.getInstance();
+  private Configuration.AppConfig mAppConfig = Configuration.AppConfig.getInstance();
+
+  @Override public void startDocument() throws SAXException {
+    super.startDocument();
+  }
+
+  @Override
+  public void startElement(String uri, String localName, String qName, Attributes attributes)
+      throws SAXException {
+    super.startElement(uri, localName, qName, attributes);
+    if (qName.equals("download")) {
+      isDownloadConfig = true;
+      isUploadConfig = false;
+      isAppConfig = false;
+    } else if (qName.equals("upload")) {
+      isUploadConfig = true;
+      isDownloadConfig = false;
+      isAppConfig = false;
+    } else if (qName.equals("app")) {
+      isUploadConfig = false;
+      isDownloadConfig = false;
+      isAppConfig = true;
+    }
+
+    if (isDownloadConfig || isUploadConfig) {
+
+      String value = attributes.getValue("value");
+      switch (qName) {
+        case "openDynamicFile":
+          loadOpenDynamicFile(value);
+          break;
+        case "threadNum":
+          loadThreadNum(value);
+          break;
+        case "maxTaskNum":
+          loadMaxQueue(value);
+          break;
+        case "reTryNum":
+          loadReTry(value);
+          break;
+        case "connectTimeOut":
+          loadConnectTime(value);
+          break;
+        case "iOTimeOut":
+          loadIOTimeout(value);
+          break;
+        case "reTryInterval":
+          loadReTryInterval(value);
+          break;
+        case "buffSize":
+          loadBuffSize(value);
+          break;
+        case "ca":
+          String caName = attributes.getValue("name");
+          String caPath = attributes.getValue("path");
+          loadCA(caName, caPath);
+          break;
+        case "convertSpeed":
+          loadConvertSpeed(value);
+          break;
+        case "maxSpeed":
+          loadMaxSpeed(value);
+          break;
+        case "queueMod":
+          loadQueueMod(value);
+          break;
+        case "updateInterval":
+          loadUpdateInterval(value);
+          break;
+        case "notNetRetry":
+          loadNotNetRetry(value);
+          break;
+      }
+    } else if (isAppConfig) {
+      String value = attributes.getValue("value");
+      switch (qName) {
+        case "useAriaCrashHandler":
+          loadUseAriaCrashHandler(value);
+          break;
+        case "logLevel":
+          loadLogLevel(value);
+          break;
+      }
+    }
+  }
+
+  private void loadOpenDynamicFile(String value) {
+    if (isDownloadConfig) {
+      mDownloadConfig.openDynamicFile = checkBoolean(value) ? Boolean.valueOf(value) : false;
+    }
+  }
+
+  private void loadNotNetRetry(String value) {
+    if (isDownloadConfig) {
+      mDownloadConfig.notNetRetry = checkBoolean(value) ? Boolean.valueOf(value) : false;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.notNetRetry = checkBoolean(value) ? Boolean.valueOf(value) : false;
+    }
+  }
+
+  private void loadLogLevel(String value) {
+    int level;
+    try {
+      level = Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      e.printStackTrace();
+      level = ALog.LOG_LEVEL_VERBOSE;
+    }
+    if (level < ALog.LOG_LEVEL_VERBOSE || level > ALog.LOG_CLOSE) {
+      ALog.w(TAG, "level【" + level + "】错误");
+      mAppConfig.logLevel = ALog.LOG_LEVEL_VERBOSE;
+    } else {
+      mAppConfig.logLevel = level;
+    }
+  }
+
+  private void loadUseAriaCrashHandler(String value) {
+    if (checkBoolean(value)) {
+      mAppConfig.useAriaCrashHandler = Boolean.parseBoolean(value);
+    } else {
+      ALog.w(TAG, "useAriaCrashHandler【" + value + "】错误");
+      mAppConfig.useAriaCrashHandler = true;
+    }
+  }
+
+  private void loadUpdateInterval(String value) {
+    long temp = checkLong(value) ? Long.parseLong(value) : 1000;
+    if (isDownloadConfig) {
+      mDownloadConfig.updateInterval = temp;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.updateInterval = temp;
+    }
+  }
+
+  private void loadQueueMod(String value) {
+    String mod = "now";
+    if (!TextUtils.isEmpty(value) && (value.equalsIgnoreCase("now") || value.equalsIgnoreCase(
+        "wait"))) {
+      mod = value;
+    }
+    if (isDownloadConfig) {
+      mDownloadConfig.queueMod = mod;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.queueMod = mod;
+    }
+  }
+
+  private void loadMaxSpeed(String value) {
+    int maxSpeed = checkInt(value) ? Integer.parseInt(value) : 0;
+    if (isDownloadConfig) {
+      mDownloadConfig.maxSpeed = maxSpeed;
+    }
+  }
+
+  private void loadConvertSpeed(String value) {
+    boolean open = true;
+    if (checkBoolean(value)) {
+      open = Boolean.parseBoolean(value);
+    }
+
+    if (isDownloadConfig) {
+      mDownloadConfig.isConvertSpeed = open;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.isConvertSpeed = open;
+    }
+  }
+
+  private void loadReTryInterval(String value) {
+    int time = checkInt(value) ? Integer.parseInt(value) : 2 * 1000;
+
+    if (time < 2 * 1000) {
+      time = 2 * 1000;
+    }
+
+    if (isDownloadConfig) {
+      mDownloadConfig.reTryInterval = time;
+    }
+  }
+
+  private void loadCA(String name, String path) {
+    if (isDownloadConfig) {
+      mDownloadConfig.caName = name;
+      mDownloadConfig.caPath = path;
+    }
+  }
+
+  private void loadBuffSize(String value) {
+    int buffSize = checkInt(value) ? Integer.parseInt(value) : 8192;
+
+    if (buffSize < 2048) {
+      buffSize = 2048;
+    }
+
+    if (isDownloadConfig) {
+      mDownloadConfig.buffSize = buffSize;
+    }
+
+    if (isUploadConfig) {
+      mUploadConfig.buffSize = buffSize;
+    }
+  }
+
+  private void loadIOTimeout(String value) {
+    int time = checkInt(value) ? Integer.parseInt(value) : 10 * 1000;
+
+    if (time < 10 * 1000) {
+      time = 10 * 1000;
+    }
+
+    if (isDownloadConfig) {
+      mDownloadConfig.iOTimeOut = time;
+    }
+
+    if (isUploadConfig) {
+      mUploadConfig.iOTimeOut = time;
+    }
+  }
+
+  private void loadConnectTime(String value) {
+    int time = checkInt(value) ? Integer.parseInt(value) : 5 * 1000;
+
+    if (isDownloadConfig) {
+      mDownloadConfig.connectTimeOut = time;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.connectTimeOut = time;
+    }
+  }
+
+  private void loadReTry(String value) {
+    int num = checkInt(value) ? Integer.parseInt(value) : 0;
+
+    if (isDownloadConfig) {
+      mDownloadConfig.reTryNum = num;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.reTryNum = num;
+    }
+  }
+
+  private void loadMaxQueue(String value) {
+    int num = checkInt(value) ? Integer.parseInt(value) : 2;
+    if (num < 1) {
+      ALog.w(TAG, "任务队列数不能小于 1");
+      num = 2;
+    }
+    if (isDownloadConfig) {
+      mDownloadConfig.maxTaskNum = num;
+    }
+    if (isUploadConfig) {
+      mUploadConfig.maxTaskNum = num;
+    }
+  }
+
+  private void loadThreadNum(String value) {
+    int num = checkInt(value) ? Integer.parseInt(value) : 3;
+    if (num < 1) {
+      ALog.e(TAG, "下载线程数不能小于 1");
+      num = 1;
+    }
+    if (isDownloadConfig) {
+      mDownloadConfig.threadNum = num;
+    }
+  }
+
+  /**
+   * 检查是否int值是否合法
+   *
+   * @return {@code true} 合法
+   */
+  private boolean checkInt(String value) {
+    if (TextUtils.isEmpty(value)) {
+      return false;
+    }
+    try {
+      Integer l = Integer.parseInt(value);
+      return true;
+    } catch (NumberFormatException e) {
+      e.printStackTrace();
+      return false;
+    }
+  }
+
+  /**
+   * 检查是否long值是否合法
+   *
+   * @return {@code true} 合法
+   */
+  private boolean checkLong(String value) {
+    if (TextUtils.isEmpty(value)) {
+      return false;
+    }
+    try {
+      Long l = Long.parseLong(value);
+      return true;
+    } catch (NumberFormatException e) {
+      e.printStackTrace();
+      return false;
+    }
+  }
+
+  /**
+   * 检查boolean值是否合法
+   *
+   * @return {@code true} 合法
+   */
+  private boolean checkBoolean(String value) {
+    return !TextUtils.isEmpty(value) && (value.equalsIgnoreCase("true") || value.equalsIgnoreCase(
+        "false"));
+  }
+
+  @Override public void characters(char[] ch, int start, int length) throws SAXException {
+    super.characters(ch, start, length);
+  }
+
+  @Override public void endElement(String uri, String localName, String qName) throws SAXException {
+    super.endElement(uri, localName, qName);
+  }
+
+  @Override public void endDocument() throws SAXException {
+    super.endDocument();
+    mDownloadConfig.saveAll();
+    mUploadConfig.saveAll();
+    mAppConfig.saveAll();
+  }
+}

+ 558 - 0
Aria/src/main/java/com/arialyy/aria/core/Configuration.java

@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.common.QueueMod;
+import com.arialyy.aria.core.queue.DownloadTaskQueue;
+import com.arialyy.aria.core.queue.UploadTaskQueue;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.AriaCrashHandler;
+import com.arialyy.aria.util.CommonUtil;
+import com.arialyy.aria.util.ErrorHelp;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Created by lyy on 2016/12/8.
+ * 信息配置
+ */
+class Configuration {
+  static final String TAG = "Configuration";
+  static final String DOWNLOAD_CONFIG_FILE = "/Aria/DownloadConfig.properties";
+  static final String UPLOAD_CONFIG_FILE = "/Aria/UploadConfig.properties";
+  static final String APP_CONFIG_FILE = "/Aria/AppConfig.properties";
+  static final String XML_FILE = "/Aria/aria_config.xml";
+  static final int TYPE_DOWNLOAD = 1;
+  static final int TYPE_UPLOAD = 2;
+  static final int TYPE_APP = 3;
+
+  abstract static class BaseConfig {
+
+    /**
+     * 类型
+     *
+     * @return {@link #TYPE_DOWNLOAD}、{@link #TYPE_UPLOAD}、{@link #TYPE_APP}
+     */
+    abstract int getType();
+
+    /**
+     * 加载配置
+     */
+    void loadConfig() {
+      String path = null;
+      Class clazz = null;
+      switch (getType()) {
+        case TYPE_DOWNLOAD:
+          path = DOWNLOAD_CONFIG_FILE;
+          clazz = DownloadConfig.class;
+          break;
+        case TYPE_UPLOAD:
+          path = UPLOAD_CONFIG_FILE;
+          clazz = UploadConfig.class;
+          break;
+        case TYPE_APP:
+          path = APP_CONFIG_FILE;
+          clazz = AppConfig.class;
+          break;
+      }
+      if (TextUtils.isEmpty(path)) {
+        ALog.e(TAG, "读取配置失败:未知文件类型");
+        ErrorHelp.saveError(TAG, "读取配置失败:未知文件类型", "");
+        return;
+      }
+
+      File file = new File(AriaManager.APP.getFilesDir().getPath() + path);
+      if (file.exists()) {
+        Properties properties = CommonUtil.loadConfig(file);
+        List<Field> fields = CommonUtil.getAllFields(clazz);
+        try {
+          for (Field field : fields) {
+            int m = field.getModifiers();
+            String fileName = field.getName();
+            if (fileName.equals("oldMaxTaskNum")
+                || field.isSynthetic()
+                || Modifier.isFinal(m)
+                || Modifier.isStatic(m)
+                || fileName.equals("shadow$_klass_")
+                || fileName.equals("shadow$_monitor_")) {
+              continue;
+            }
+            field.setAccessible(true);
+            String value = properties.getProperty(field.getName());
+            if (TextUtils.isEmpty(value) || value.equalsIgnoreCase("null")) continue;
+            Class<?> type = field.getType();
+            if (type == String.class) {
+              field.set(this, value);
+            } else if (type == int.class || type == Integer.class) {
+              if (fileName.equalsIgnoreCase("maxSpeed")) { //兼容以前版本,以前maxSpeed是double类型的
+                Double d = Double.parseDouble(value);
+                field.setInt(this, (int) d.doubleValue());
+              } else {
+                field.setInt(this, Integer.parseInt(value));
+              }
+            } else if (type == float.class || type == Float.class) {
+              field.setFloat(this, Float.parseFloat(value));
+            } else if (type == double.class || type == Double.class) {
+              if (TextUtils.isEmpty(value)) {
+                value = "0";
+              }
+              field.setDouble(this, Double.parseDouble(value));
+            } else if (type == long.class || type == Long.class) {
+              field.setLong(this, Long.parseLong(value));
+            } else if (type == boolean.class || type == Boolean.class) {
+              field.setBoolean(this, Boolean.parseBoolean(value));
+            }
+          }
+        } catch (IllegalAccessException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+
+    /**
+     * 保存key
+     */
+    void saveKey(String key, String value) {
+      String path = null;
+      switch (getType()) {
+        case TYPE_DOWNLOAD:
+          path = DOWNLOAD_CONFIG_FILE;
+          break;
+        case TYPE_UPLOAD:
+          path = UPLOAD_CONFIG_FILE;
+          break;
+        case TYPE_APP:
+          path = APP_CONFIG_FILE;
+          break;
+      }
+      File file = new File(
+          AriaManager.APP.getFilesDir().getPath() + path);
+      if (file.exists()) {
+        Properties properties = CommonUtil.loadConfig(file);
+        properties.setProperty(key, value);
+        CommonUtil.saveConfig(file, properties);
+      }
+    }
+
+    /**
+     * 保存配置
+     */
+    void saveAll() {
+      List<Field> fields = CommonUtil.getAllFields(getClass());
+      try {
+        String path = null;
+        switch (getType()) {
+          case TYPE_DOWNLOAD:
+            path = DOWNLOAD_CONFIG_FILE;
+            break;
+          case TYPE_UPLOAD:
+            path = UPLOAD_CONFIG_FILE;
+            break;
+          case TYPE_APP:
+            path = APP_CONFIG_FILE;
+            break;
+        }
+        File file = new File(
+            AriaManager.APP.getFilesDir().getPath() + path);
+        Properties properties = CommonUtil.loadConfig(file);
+        for (Field field : fields) {
+          int m = field.getModifiers();
+          if (field.isSynthetic() || Modifier.isFinal(m) || Modifier.isStatic(m) || field.getName()
+              .equals("shadow$_klass_") || field.getName().equals("shadow$_monitor_")) {
+            continue;
+          }
+          field.setAccessible(true);
+          properties.setProperty(field.getName(), field.get(this) + "");
+        }
+        CommonUtil.saveConfig(file, properties);
+      } catch (IllegalAccessException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  /**
+   * 通用任务配置
+   */
+  abstract static class BaseTaskConfig extends BaseConfig {
+
+    /**
+     * 设置写文件buff大小,该数值大小不能小于2048,数值变小,下载速度会变慢
+     */
+    int buffSize = 8192;
+
+    /**
+     * 进度刷新间隔,默认1秒
+     */
+    long updateInterval = 1000;
+
+    /**
+     * 旧任务数
+     */
+    public int oldMaxTaskNum = 2;
+
+    /**
+     * 任务队列最大任务数, 默认为2
+     */
+    int maxTaskNum = 2;
+    /**
+     * 下载失败,重试次数,默认为10
+     */
+    int reTryNum = 10;
+    /**
+     * 设置重试间隔,单位为毫秒,默认2000毫秒
+     */
+    int reTryInterval = 2000;
+    /**
+     * 设置url连接超时时间,单位为毫秒,默认5000毫秒
+     */
+    int connectTimeOut = 5000;
+
+    /**
+     * 是否需要转换速度单位,转换完成后为:1b/s、1k/s、1m/s、1g/s、1t/s,如果不需要将返回byte长度
+     */
+    boolean isConvertSpeed = false;
+
+    /**
+     * 执行队列类型
+     *
+     * @see QueueMod
+     */
+    String queueMod = "wait";
+
+    /**
+     * 断网的时候是否重试,{@code true}断网也重试;{@code false}断网不重试,直接走失败的回调
+     */
+    boolean notNetRetry = false;
+
+    /**
+     * 设置IO流读取时间,单位为毫秒,默认20000毫秒,该时间不能少于10000毫秒
+     */
+    int iOTimeOut = 20 * 1000;
+
+    public long getUpdateInterval() {
+      return updateInterval;
+    }
+
+    /**
+     * 设置进度更新间隔,该设置对正在运行的任务无效,默认为1000毫秒
+     *
+     * @param updateInterval 不能小于0
+     */
+    public BaseTaskConfig setUpdateInterval(long updateInterval) {
+      if (updateInterval <= 0) {
+        ALog.w("Configuration", "进度更新间隔不能小于0");
+        return this;
+      }
+      this.updateInterval = updateInterval;
+      saveKey("updateInterval", String.valueOf(updateInterval));
+      return this;
+    }
+
+    public String getQueueMod() {
+      return queueMod;
+    }
+
+    public BaseTaskConfig setQueueMod(String queueMod) {
+      this.queueMod = queueMod;
+      saveKey("queueMod", queueMod);
+      return this;
+    }
+
+    public int getMaxTaskNum() {
+      return maxTaskNum;
+    }
+
+    public int getReTryNum() {
+      return reTryNum;
+    }
+
+    public BaseTaskConfig setReTryNum(int reTryNum) {
+      this.reTryNum = reTryNum;
+      saveKey("reTryNum", String.valueOf(reTryNum));
+      return this;
+    }
+
+    public int getReTryInterval() {
+      return reTryInterval;
+    }
+
+    public BaseTaskConfig setReTryInterval(int reTryInterval) {
+      this.reTryInterval = reTryInterval;
+      saveKey("reTryInterval", String.valueOf(reTryInterval));
+      return this;
+    }
+
+    public boolean isConvertSpeed() {
+      return isConvertSpeed;
+    }
+
+    public BaseTaskConfig setConvertSpeed(boolean convertSpeed) {
+      isConvertSpeed = convertSpeed;
+      saveKey("isConvertSpeed", String.valueOf(isConvertSpeed));
+      return this;
+    }
+
+    public int getConnectTimeOut() {
+      return connectTimeOut;
+    }
+
+    public BaseTaskConfig setConnectTimeOut(int connectTimeOut) {
+      this.connectTimeOut = connectTimeOut;
+      saveKey("connectTimeOut", String.valueOf(connectTimeOut));
+      return this;
+    }
+
+    public boolean isNotNetRetry() {
+      return notNetRetry;
+    }
+
+    public BaseTaskConfig setNotNetRetry(boolean notNetRetry) {
+      this.notNetRetry = notNetRetry;
+      saveKey("notNetRetry", String.valueOf(notNetRetry));
+      return this;
+    }
+
+    public int getIOTimeOut() {
+      return iOTimeOut;
+    }
+
+    public BaseTaskConfig setIOTimeOut(int iOTimeOut) {
+      this.iOTimeOut = iOTimeOut;
+      saveKey("iOTimeOut", String.valueOf(iOTimeOut));
+      return this;
+    }
+
+    public int getBuffSize() {
+      return buffSize;
+    }
+
+    public BaseTaskConfig setBuffSize(int buffSize) {
+      this.buffSize = buffSize;
+      saveKey("buffSize", String.valueOf(buffSize));
+      return this;
+    }
+  }
+
+  /**
+   * 下载配置
+   */
+  public static class DownloadConfig extends BaseTaskConfig {
+
+    /**
+     * 设置https ca 证书信息;path 为assets目录下的CA证书完整路径
+     */
+    String caPath;
+    /**
+     * name 为CA证书名
+     */
+    String caName;
+    /**
+     * 下载线程数,下载线程数不能小于1
+     */
+    int threadNum = 3;
+
+    /**
+     * 设置最大下载速度,单位:kb, 为0表示不限速
+     */
+    int maxSpeed = 0;
+
+    /**
+     * 是否开启动态文件,开启动态文件后初始化时将不占用磁盘空间,下载多少byte,占多少空间,效果见chrome的下载
+     * 注意:
+     * 1、使用该功能,将自动关闭多线程下载;
+     * 2、对于已经采用了多线程的任务,依然采用原来的下载方式;
+     * 3、原本参数是true,任务没下载完成,就参数改为false,那么没下载完成的任务还是会按照参数修改前的方式下载,只有新任务才会根据参数调用不同的下载方式
+     * {@code true}使用
+     */
+    boolean openDynamicFile = true;
+
+    public DownloadConfig setOpenDynamicFile(boolean openDynamicFile) {
+      this.openDynamicFile = openDynamicFile;
+      saveKey("openDynamicFile", String.valueOf(openDynamicFile));
+      return this;
+    }
+
+    public boolean isOpenDynamicFile() {
+      return openDynamicFile;
+    }
+
+    public DownloadConfig setMaxTaskNum(int maxTaskNum) {
+      oldMaxTaskNum = this.maxTaskNum;
+      this.maxTaskNum = maxTaskNum;
+      saveKey("maxTaskNum", String.valueOf(maxTaskNum));
+      DownloadTaskQueue.getInstance().setMaxTaskNum(maxTaskNum);
+      return this;
+    }
+
+    public int getMaxSpeed() {
+      return maxSpeed;
+    }
+
+    public DownloadConfig setMaxSpeed(int maxSpeed) {
+      this.maxSpeed = maxSpeed;
+      saveKey("maxSpeed", String.valueOf(maxSpeed));
+      DownloadTaskQueue.getInstance().setMaxSpeed(maxSpeed);
+      return this;
+    }
+
+    public void setThreadNum(int threadNum) {
+      this.threadNum = threadNum;
+      saveKey("threadNum", String.valueOf(threadNum));
+    }
+
+    public String getCaPath() {
+      return caPath;
+    }
+
+    public DownloadConfig setCaPath(String caPath) {
+      this.caPath = caPath;
+      saveKey("caPath", caPath);
+      return this;
+    }
+
+    public String getCaName() {
+      return caName;
+    }
+
+    public DownloadConfig setCaName(String caName) {
+      this.caName = caName;
+      saveKey("caName", caName);
+      return this;
+    }
+
+    public int getThreadNum() {
+      return threadNum;
+    }
+
+    private DownloadConfig() {
+      loadConfig();
+    }
+
+    private static DownloadConfig INSTANCE = null;
+
+    static DownloadConfig getInstance() {
+      if (INSTANCE == null) {
+        synchronized (DownloadConfig.class) {
+          INSTANCE = new DownloadConfig();
+        }
+      }
+      return INSTANCE;
+    }
+
+    @Override int getType() {
+      return TYPE_DOWNLOAD;
+    }
+  }
+
+  /**
+   * 上传配置
+   */
+  public static class UploadConfig extends BaseTaskConfig {
+    private static UploadConfig INSTANCE = null;
+
+    private UploadConfig() {
+      loadConfig();
+    }
+
+    public UploadConfig setMaxTaskNum(int maxTaskNum) {
+      oldMaxTaskNum = this.maxTaskNum;
+      this.maxTaskNum = maxTaskNum;
+      saveKey("maxTaskNum", String.valueOf(maxTaskNum));
+      UploadTaskQueue.getInstance().setMaxTaskNum(maxTaskNum);
+      return this;
+    }
+
+    static UploadConfig getInstance() {
+      if (INSTANCE == null) {
+        synchronized (DownloadConfig.class) {
+          INSTANCE = new UploadConfig();
+        }
+      }
+      return INSTANCE;
+    }
+
+    @Override int getType() {
+      return TYPE_UPLOAD;
+    }
+  }
+
+  /**
+   * 应用配置
+   */
+  public static class AppConfig extends BaseConfig {
+    private static AppConfig INSTANCE = null;
+    /**
+     * 是否使用{@link AriaCrashHandler}来捕获异常
+     * {@code true} 使用;{@code false} 不使用
+     */
+    boolean useAriaCrashHandler;
+
+    /**
+     * 设置Aria的日志级别
+     *
+     * {@link ALog#LOG_LEVEL_VERBOSE}
+     */
+    int logLevel;
+
+    AppConfig() {
+      loadConfig();
+    }
+
+    static AppConfig getInstance() {
+      if (INSTANCE == null) {
+        synchronized (AppConfig.class) {
+          INSTANCE = new AppConfig();
+        }
+      }
+      return INSTANCE;
+    }
+
+    public AppConfig setLogLevel(int level) {
+      this.logLevel = level;
+      ALog.LOG_LEVEL = level;
+      saveKey("logLevel", String.valueOf(logLevel));
+      return this;
+    }
+
+    public int getLogLevel() {
+      return logLevel;
+    }
+
+    public boolean getUseAriaCrashHandler() {
+      return useAriaCrashHandler;
+    }
+
+    public AppConfig setUseAriaCrashHandler(boolean useAriaCrashHandler) {
+      this.useAriaCrashHandler = useAriaCrashHandler;
+      saveKey("useAriaCrashHandler", String.valueOf(useAriaCrashHandler));
+      if (useAriaCrashHandler) {
+        Thread.setDefaultUncaughtExceptionHandler(new AriaCrashHandler());
+      } else {
+        Thread.setDefaultUncaughtExceptionHandler(null);
+      }
+      return this;
+    }
+
+    @Override int getType() {
+      return TYPE_APP;
+    }
+  }
+}

+ 83 - 0
Aria/src/main/java/com/arialyy/aria/core/FtpUrlEntity.java

@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 com.arialyy.aria.core;
+
+import java.net.InetAddress;
+
+/**
+ * Created by Aria.Lao on 2017/10/24.
+ * ftp url 信息链接实体
+ */
+public class FtpUrlEntity implements Cloneable {
+  /**
+   * 如:ftp://127.0.0.1:21/download/AriaPrj.zip
+   * remotePath便是:download/AriaPrj.zip
+   */
+  public String remotePath;
+
+  public String account;
+
+  /**
+   * 原始url
+   */
+  public String url;
+
+  /**
+   * ftp协议:ftp
+   */
+  public String protocol;
+
+  /**
+   * 登录的用户名
+   */
+  public String user;
+  /**
+   * 密码
+   */
+  public String password;
+
+  /**
+   * 端口
+   */
+  public String port;
+
+  /**
+   * 主机域名
+   */
+  public String hostName;
+
+  /**
+   * 是否需要登录
+   */
+  public boolean needLogin = false;
+
+  /**
+   * 有效的ip地址
+   */
+  public InetAddress validAddr;
+
+  @Override public FtpUrlEntity clone() {
+    FtpUrlEntity entity = null;
+    try {
+      entity = (FtpUrlEntity) super.clone();
+    } catch (CloneNotSupportedException e) {
+      e.printStackTrace();
+    }
+    return entity;
+  }
+}

+ 139 - 0
Aria/src/main/java/com/arialyy/aria/core/WidgetLiftManager.java

@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core;
+
+import android.annotation.TargetApi;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.os.Message;
+import android.support.v4.app.DialogFragment;
+import android.widget.PopupWindow;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import java.lang.reflect.Field;
+
+/**
+ * Created by lyy on 2017/2/7.
+ * 为组件添加生命周期
+ */
+final class WidgetLiftManager {
+  private final String TAG = "WidgetLiftManager";
+
+  /**
+   * 处理DialogFragment事件
+   *
+   * @param dialogFragment {@link android.app.DialogFragment}
+   */
+  @TargetApi(Build.VERSION_CODES.HONEYCOMB) boolean handleDialogFragmentLift(
+      android.app.DialogFragment dialogFragment) {
+    return handleDialogLift(dialogFragment.getDialog());
+  }
+
+  /**
+   * 处理DialogFragment事件
+   *
+   * @param dialogFragment {@link android.support.v4.app.DialogFragment}
+   */
+  @TargetApi(Build.VERSION_CODES.HONEYCOMB) boolean handleDialogFragmentLift(
+      DialogFragment dialogFragment) {
+    return handleDialogLift(dialogFragment.getDialog());
+  }
+
+  /**
+   * 处理悬浮框取消或dismiss事件
+   */
+  boolean handlePopupWindowLift(PopupWindow popupWindow) {
+    try {
+      Field dismissField = CommonUtil.getField(popupWindow.getClass(), "mOnDismissListener");
+      PopupWindow.OnDismissListener listener =
+          (PopupWindow.OnDismissListener) dismissField.get(popupWindow);
+      if (listener != null) {
+        ALog.e(TAG, "你已经对PopupWindow设置了Dismiss事件。为了防止内存泄露,"
+            + "请在dismiss方法中调用Aria.download(this).unRegister();来注销事件");
+        return true;
+      } else {
+        popupWindow.setOnDismissListener(createPopupWindowListener(popupWindow));
+      }
+    } catch (IllegalAccessException e) {
+      e.printStackTrace();
+    }
+    return false;
+  }
+
+  /**
+   * 创建popupWindow dismiss事件
+   */
+  private PopupWindow.OnDismissListener createPopupWindowListener(final PopupWindow popupWindow) {
+    return new PopupWindow.OnDismissListener() {
+      @Override public void onDismiss() {
+        AriaManager.getInstance(AriaManager.APP).destroySchedulerListener(popupWindow);
+      }
+    };
+  }
+
+  /**
+   * 处理对话框取消或dismiss
+   */
+  boolean handleDialogLift(Dialog dialog) {
+    try {
+      Field dismissField = CommonUtil.getField(dialog.getClass(), "mDismissMessage");
+      Message dismissMsg = (Message) dismissField.get(dialog);
+      //如果Dialog已经设置Dismiss事件,则查找cancel事件
+      if (dismissMsg != null) {
+        Field cancelField = CommonUtil.getField(dialog.getClass(), "mCancelMessage");
+        Message cancelMsg = (Message) cancelField.get(dialog);
+        if (cancelMsg != null) {
+          ALog.e(TAG, "你已经对Dialog设置了Dismiss和cancel事件。"
+              + "为了防止内存泄露,请在dismiss方法中调用Aria.download(this).unRegister();来注销事件\n"
+              + "如果你使用的是DialogFragment,那么你需要在onDestroy()中进行销毁Aria事件操作");
+          return true;
+        } else {
+          dialog.setOnCancelListener(createCancelListener());
+        }
+      } else {
+        dialog.setOnDismissListener(createDismissListener());
+      }
+    } catch (IllegalAccessException e) {
+      e.printStackTrace();
+    }
+    return false;
+  }
+
+  /**
+   * 创建Dialog取消事件
+   */
+  private Dialog.OnCancelListener createCancelListener() {
+    return new Dialog.OnCancelListener() {
+
+      @Override public void onCancel(DialogInterface dialog) {
+        AriaManager.getInstance(AriaManager.APP).destroySchedulerListener(dialog);
+      }
+    };
+  }
+
+  /**
+   * 创建Dialog dismiss取消事件
+   */
+  private Dialog.OnDismissListener createDismissListener() {
+    return new Dialog.OnDismissListener() {
+
+      @Override public void onDismiss(DialogInterface dialog) {
+        AriaManager.getInstance(AriaManager.APP).destroySchedulerListener(dialog);
+      }
+    };
+  }
+}

+ 36 - 0
Aria/src/main/java/com/arialyy/aria/core/command/AbsCmd.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command;
+
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.queue.ITaskQueue;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ */
+public abstract class AbsCmd<T extends AbsTaskEntity> implements ICmd {
+  protected ITaskQueue mQueue;
+  protected T mTaskEntity;
+  protected String TAG;
+  protected String mTargetName;
+
+  /**
+   * 是否是下载任务的命令
+   * {@code true} 下载任务的命令,{@code false} 上传任务的命令
+   */
+  protected boolean isDownloadCmd = true;
+}

+ 33 - 0
Aria/src/main/java/com/arialyy/aria/core/command/AbsCmdFactory.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command;
+
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 抽象命令工厂
+ */
+public abstract class AbsCmdFactory<TASK_ENTITY extends AbsTaskEntity, CMD extends AbsCmd> {
+
+  /**
+   * @param target 创建任务的对象
+   * @param entity 下载实体
+   * @param taskType {@link ICmd#TASK_TYPE_DOWNLOAD}、{@link ICmd#TASK_TYPE_DOWNLOAD_GROUP}、{@link
+   * ICmd#TASK_TYPE_UPLOAD}
+   */
+  public abstract CMD createCmd(String target, TASK_ENTITY entity, int type, int taskType);
+}

+ 40 - 0
Aria/src/main/java/com/arialyy/aria/core/command/ICmd.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command;
+
+/**
+ * Created by lyy on 2017/2/9.
+ */
+
+public interface ICmd {
+  /**
+   * 单任务下载任务
+   */
+  int TASK_TYPE_DOWNLOAD = 0x01;
+  /**
+   * 任务组下载任务
+   */
+  int TASK_TYPE_DOWNLOAD_GROUP = 0x02;
+  /**
+   * 上传任务
+   */
+  int TASK_TYPE_UPLOAD = 0x10;
+
+  /**
+   * 执行命令
+   */
+  void executeCmd();
+}

+ 73 - 0
Aria/src/main/java/com/arialyy/aria/core/command/group/AbsGroupCmd.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.group;
+
+import com.arialyy.aria.core.command.AbsCmd;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.inf.AbsGroupTask;
+import com.arialyy.aria.core.inf.AbsGroupTaskEntity;
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.queue.DownloadGroupTaskQueue;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 任务组命令
+ */
+public abstract class AbsGroupCmd<T extends AbsGroupTaskEntity> extends AbsCmd<T> {
+  /**
+   * 需要控制的子任务url
+   */
+  String childUrl;
+
+  AbsGroupTask tempTask;
+
+  /**
+   * @param targetName 创建任务的对象名
+   */
+  AbsGroupCmd(String targetName, T entity) {
+    mTargetName = targetName;
+    mTaskEntity = entity;
+    TAG = CommonUtil.getClassName(this);
+    if (entity instanceof DownloadGroupTaskEntity) {
+      mQueue = DownloadGroupTaskQueue.getInstance();
+      isDownloadCmd = true;
+    }
+  }
+
+  /**
+   * 创建任务
+   *
+   * @return 创建的任务
+   */
+  AbsTask createTask() {
+    tempTask = (AbsGroupTask) mQueue.createTask(mTargetName, mTaskEntity);
+    return tempTask;
+  }
+
+  boolean checkTask() {
+    tempTask = (AbsGroupTask) mQueue.getTask(mTaskEntity.getEntity().getKey());
+    if (tempTask == null) {
+      createTask();
+      if (tempTask.isComplete()) {
+        ALog.i(TAG, "任务已完成");
+        return false;
+      }
+    }
+    return true;
+  }
+}

+ 37 - 0
Aria/src/main/java/com/arialyy/aria/core/command/group/GroupCancelCmd.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.group;
+
+import com.arialyy.aria.core.inf.AbsGroupTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 删除任务组
+ */
+class GroupCancelCmd<T extends AbsGroupTaskEntity> extends AbsGroupCmd<T> {
+  /**
+   * @param targetName 创建任务的对象名
+   */
+  GroupCancelCmd(String targetName, T entity) {
+    super(targetName, entity);
+  }
+
+  @Override public void executeCmd() {
+    if (checkTask()) {
+      tempTask.cancelSubTask(childUrl);
+    }
+  }
+}

+ 78 - 0
Aria/src/main/java/com/arialyy/aria/core/command/group/GroupCmdFactory.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.group;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.inf.AbsGroupTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 任务组子任务控制命令
+ */
+public class GroupCmdFactory {
+  /**
+   * 启动子任务
+   */
+  public static final int SUB_TASK_START = 0xa1;
+  /**
+   * 停止子任务
+   */
+  public static final int SUB_TASK_STOP = 0xa2;
+  /**
+   * 取消子任务
+   */
+  public static final int SUB_TASK_CANCEL = 0xa3;
+
+  private static volatile GroupCmdFactory INSTANCE = null;
+
+  private GroupCmdFactory() {
+
+  }
+
+  public static GroupCmdFactory getInstance() {
+    if (INSTANCE == null) {
+      synchronized (AriaManager.LOCK) {
+        INSTANCE = new GroupCmdFactory();
+      }
+    }
+    return INSTANCE;
+  }
+
+  /**
+   * @param target 创建任务的对象
+   * @param entity 下载实体
+   * @param type 命令类型{@link #SUB_TASK_START}、{@link #SUB_TASK_STOP}、{@link #SUB_TASK_CANCEL}
+   * @param childUrl 需要控制的子任务url
+   */
+  public AbsGroupCmd createCmd(String target, AbsGroupTaskEntity entity, int type,
+      String childUrl) {
+    AbsGroupCmd cmd = null;
+    switch (type) {
+      case SUB_TASK_START:
+        cmd = new GroupStartCmd<>(target, entity);
+        break;
+      case SUB_TASK_STOP:
+        cmd = new GroupStopCmd<>(target, entity);
+        break;
+      case SUB_TASK_CANCEL:
+        cmd = new GroupCancelCmd<>(target, entity);
+    }
+    if (cmd != null) {
+      cmd.childUrl = childUrl;
+    }
+    return cmd;
+  }
+}

+ 37 - 0
Aria/src/main/java/com/arialyy/aria/core/command/group/GroupStartCmd.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.group;
+
+import com.arialyy.aria.core.inf.AbsGroupTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 任务组开始命令,该命令负责处理任务组子任务的开始\恢复等工作
+ */
+class GroupStartCmd<T extends AbsGroupTaskEntity> extends AbsGroupCmd<T> {
+  /**
+   * @param targetName 创建任务的对象名
+   */
+  GroupStartCmd(String targetName, T entity) {
+    super(targetName, entity);
+  }
+
+  @Override public void executeCmd() {
+    if (checkTask()) {
+      tempTask.startSubTask(childUrl);
+    }
+  }
+}

+ 37 - 0
Aria/src/main/java/com/arialyy/aria/core/command/group/GroupStopCmd.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.group;
+
+import com.arialyy.aria.core.inf.AbsGroupTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 停止任务组的命令
+ */
+class GroupStopCmd<T extends AbsGroupTaskEntity> extends AbsGroupCmd<T> {
+  /**
+   * @param targetName 创建任务的对象名
+   */
+  GroupStopCmd(String targetName, T entity) {
+    super(targetName, entity);
+  }
+
+  @Override public void executeCmd() {
+    if (checkTask()) {
+      tempTask.stopSubTask(childUrl);
+    }
+  }
+}

+ 185 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/AbsNormalCmd.java

@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import com.arialyy.aria.core.command.AbsCmd;
+import com.arialyy.aria.core.command.ICmd;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.manager.TEManager;
+import com.arialyy.aria.core.queue.DownloadGroupTaskQueue;
+import com.arialyy.aria.core.queue.DownloadTaskQueue;
+import com.arialyy.aria.core.queue.UploadTaskQueue;
+import com.arialyy.aria.core.scheduler.ISchedulers;
+import com.arialyy.aria.core.upload.UploadTaskEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+
+/**
+ * Created by lyy on 2016/8/22.
+ * 下载命令
+ */
+public abstract class AbsNormalCmd<T extends AbsTaskEntity> extends AbsCmd<T> {
+  /**
+   * 能否执行命令
+   */
+  boolean canExeCmd = true;
+
+  private AbsTask tempTask = null;
+  int taskType;
+
+  /**
+   * @param targetName 产生任务的对象名
+   * @param taskType 下载任务类型{@link ICmd#TASK_TYPE_DOWNLOAD}、{@link ICmd#TASK_TYPE_DOWNLOAD_GROUP}、{@link
+   * ICmd#TASK_TYPE_UPLOAD}
+   */
+  AbsNormalCmd(String targetName, T entity, int taskType) {
+    this.taskType = taskType;
+    mTargetName = targetName;
+    mTaskEntity = entity;
+    TAG = CommonUtil.getClassName(this);
+    if (taskType == ICmd.TASK_TYPE_DOWNLOAD) {
+      if (!(entity instanceof DownloadTaskEntity)) {
+        ALog.e(TAG, "任务类型错误,任务类型应该为ICM.TASK_TYPE_DOWNLOAD");
+        return;
+      }
+      mQueue = DownloadTaskQueue.getInstance();
+    } else if (taskType == ICmd.TASK_TYPE_DOWNLOAD_GROUP) {
+      if (!(entity instanceof DownloadGroupTaskEntity)) {
+        ALog.e(TAG, "任务类型错误,任务类型应该为ICM.TASK_TYPE_DOWNLOAD_GROUP");
+        return;
+      }
+      mQueue = DownloadGroupTaskQueue.getInstance();
+    } else if (taskType == ICmd.TASK_TYPE_UPLOAD) {
+      if (!(entity instanceof UploadTaskEntity)) {
+        ALog.e(TAG, "任务类型错误,任务类型应该为ICM.TASK_TYPE_UPLOAD");
+        return;
+      }
+      mQueue = UploadTaskQueue.getInstance();
+    } else {
+      ALog.e(TAG, "任务类型错误,任务类型应该为ICM.TASK_TYPE_DOWNLOAD、TASK_TYPE_DOWNLOAD_GROUP、TASK_TYPE_UPLOAD");
+      return;
+    }
+    isDownloadCmd = taskType < ICmd.TASK_TYPE_UPLOAD;
+  }
+
+  /**
+   * 发送等待状态
+   */
+  void sendWaitState() {
+    if (tempTask != null) {
+      tempTask.getOutHandler().obtainMessage(ISchedulers.WAIT, tempTask).sendToTarget();
+    }
+  }
+
+  /**
+   * 删除所有任务
+   */
+  void removeAll() {
+    mQueue.removeAllTask();
+  }
+
+  /**
+   * 停止所有任务
+   */
+  void stopAll() {
+    mQueue.stopAllTask();
+  }
+
+  /**
+   * 停止任务
+   */
+  void stopTask() {
+    if (tempTask == null) createTask();
+    mQueue.stopTask(tempTask);
+  }
+
+  /**
+   * 删除任务
+   */
+  void removeTask() {
+    if (tempTask == null) createTask();
+    mQueue.cancelTask(tempTask);
+  }
+
+  /**
+   * 启动任务
+   */
+  void startTask() {
+    mQueue.startTask(tempTask);
+  }
+
+  /**
+   * 恢复任务
+   */
+  void resumeTask() {
+    mQueue.resumeTask(tempTask);
+  }
+
+  /**
+   * 启动指定任务
+   *
+   * @param task 指定任务
+   */
+  void startTask(AbsTask task) {
+    mQueue.startTask(task);
+  }
+
+  /**
+   * 从队列中获取任务
+   *
+   * @return 执行任务
+   */
+  AbsTask getTask() {
+    tempTask = mQueue.getTask(mTaskEntity.getEntity().getKey());
+    return tempTask;
+  }
+
+  /**
+   * 从队列中获取任务
+   *
+   * @return 执行任务
+   */
+  AbsTask getTask(AbsEntity entity) {
+    tempTask = mQueue.getTask(entity.getKey());
+    return tempTask;
+  }
+
+  /**
+   * 创建任务
+   *
+   * @return 创建的任务
+   */
+  AbsTask createTask() {
+    tempTask = mQueue.createTask(mTargetName, mTaskEntity);
+    return tempTask;
+  }
+
+  /**
+   * 创建指定实体的任务
+   *
+   * @param taskEntity 特定的任务实体
+   * @return 创建的任务
+   */
+  AbsTask createTask(AbsTaskEntity taskEntity) {
+    TEManager.getInstance().addTEntity(taskEntity);
+    return mQueue.createTask(mTargetName, taskEntity);
+  }
+}

+ 45 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/AddCmd.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.util.ALog;
+
+/**
+ * Created by lyy on 2016/8/22.
+ * 添加任务的命令
+ */
+class AddCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+
+  AddCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    if (!canExeCmd) return;
+    AbsTask task = getTask();
+    if (task == null) {
+      mTaskEntity.getEntity().setState(IEntity.STATE_WAIT);
+      createTask();
+      sendWaitState();
+    } else {
+      ALog.w(TAG, "添加命令执行失败,【该任务已经存在】");
+    }
+  }
+}

+ 42 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/CancelAllCmd.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/27.
+ * 删除所有任务,并且删除所有回掉
+ */
+public class CancelAllCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+  /**
+   * removeFile {@code true} 删除已经下载完成的任务,不仅删除下载记录,还会删除已经下载完成的文件,{@code false}
+   * 如果文件已经下载完成,只删除下载记录
+   */
+  public boolean removeFile = false;
+
+  /**
+   * @param targetName 产生任务的对象名
+   */
+  CancelAllCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    removeAll();
+  }
+}

+ 52 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/CancelCmd.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+
+/**
+ * Created by lyy on 2016/9/20.
+ * 取消命令
+ */
+public class CancelCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+  /**
+   * removeFile {@code true} 删除已经下载完成的任务,不仅删除下载记录,还会删除已经下载完成的文件,{@code false}
+   * 如果文件已经下载完成,只删除下载记录
+   */
+  public boolean removeFile = false;
+
+  CancelCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    if (!canExeCmd) return;
+    AbsTask task = getTask();
+    if (task == null) {
+      task = createTask();
+    }
+    if (task != null) {
+      mTaskEntity.setRemoveFile(removeFile);
+      if (!TextUtils.isEmpty(mTargetName)) {
+        task.setTargetName(mTargetName);
+      }
+      removeTask();
+    }
+  }
+}

+ 63 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/HighestPriorityCmd.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.download.DownloadTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.queue.DownloadTaskQueue;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.NetUtils;
+
+/**
+ * Created by lyy on 2017/6/2.
+ * 最高优先级命令,最高优先级命令有以下属性
+ * 1、在下载队列中,有且只有一个最高优先级任务
+ * 2、最高优先级任务会一直存在,直到用户手动暂停或任务完成
+ * 3、任务调度器不会暂停最高优先级任务
+ * 4、用户手动暂停或任务完成后,第二次重新执行该任务,该命令将失效
+ * 5、如果下载队列中已经满了,则会停止队尾的任务,当高优先级任务完成后,该队尾任务将自动执行
+ * 6、把任务设置为最高优先级任务后,将自动执行任务,不需要重新调用start()启动任务
+ *
+ * 目前只只支持单下载任务的最高优先级任务
+ */
+final class HighestPriorityCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+  /**
+   * @param targetName 产生任务的对象名
+   */
+  HighestPriorityCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    if (!canExeCmd) return;
+    if (!NetUtils.isConnected(AriaManager.APP)){
+      ALog.e(TAG, "启动任务失败,网络未连接");
+      return;
+    }
+    DownloadTask task = (DownloadTask) getTask();
+    if (task == null) {
+      task = (DownloadTask) createTask();
+    }
+    if (task != null) {
+      if (!TextUtils.isEmpty(mTargetName)) {
+        task.setTargetName(mTargetName);
+      }
+      ((DownloadTaskQueue) mQueue).setTaskHighestPriority(task);
+    }
+  }
+}

+ 111 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/NormalCmdFactory.java

@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.command.AbsCmdFactory;
+import com.arialyy.aria.core.command.ICmd;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+
+/**
+ * Created by Lyy on 2016/9/23.
+ * 命令工厂
+ */
+public class NormalCmdFactory extends AbsCmdFactory<AbsTaskEntity, AbsNormalCmd> {
+  /**
+   * 创建任务
+   */
+  public static final int TASK_CREATE = 0xb1;
+  /**
+   * 启动任务
+   */
+  public static final int TASK_START = 0xb2;
+  /**
+   * 恢复任务
+   */
+  public static final int TASK_RESUME = 0xb3;
+  /**
+   * 取消任务
+   */
+  public static final int TASK_CANCEL = 0xb4;
+  /**
+   * 停止任务
+   */
+  public static final int TASK_STOP = 0xb5;
+  /**
+   * 设置任务为最高优先级
+   */
+  public static final int TASK_HIGHEST_PRIORITY = 0xb6;
+  /**
+   * 停止所有任务
+   */
+  public static final int TASK_STOP_ALL = 0xb7;
+  /**
+   * 恢复所有停止的任务
+   */
+  public static final int TASK_RESUME_ALL = 0xb8;
+  /**
+   * 删除所有任务,
+   */
+  public static final int TASK_CANCEL_ALL = 0xb9;
+  private static volatile NormalCmdFactory INSTANCE = null;
+
+  private NormalCmdFactory() {
+
+  }
+
+  public static NormalCmdFactory getInstance() {
+    if (INSTANCE == null) {
+      synchronized (AriaManager.LOCK) {
+        INSTANCE = new NormalCmdFactory();
+      }
+    }
+    return INSTANCE;
+  }
+
+  /**
+   * @param target 创建任务的对象
+   * @param entity 下载实体
+   * @param type 命令类型{@link #TASK_CREATE}、{@link #TASK_START}、{@link #TASK_CANCEL}、{@link
+   * #TASK_STOP}、{@link #TASK_HIGHEST_PRIORITY}、{@link #TASK_STOP_ALL}、{@link #TASK_RESUME_ALL}
+   * @param taskType {@link ICmd#TASK_TYPE_DOWNLOAD}、{@link ICmd#TASK_TYPE_DOWNLOAD_GROUP}、{@link
+   * ICmd#TASK_TYPE_UPLOAD}
+   */
+  public AbsNormalCmd createCmd(String target, AbsTaskEntity entity, int type, int taskType) {
+    switch (type) {
+      case TASK_CREATE:
+        return new AddCmd<>(target, entity, taskType);
+      case TASK_RESUME:
+      case TASK_START:
+        return new StartCmd<>(target, entity, taskType);
+      case TASK_CANCEL:
+        return new CancelCmd<>(target, entity, taskType);
+      case TASK_STOP:
+        return new StopCmd<>(target, entity, taskType);
+      case TASK_HIGHEST_PRIORITY:
+        return new HighestPriorityCmd<>(target, entity, taskType);
+      case TASK_STOP_ALL:
+        return new StopAllCmd<>(target, entity, taskType);
+      case TASK_RESUME_ALL:
+        return new ResumeAllCmd<>(target, entity, taskType);
+      case TASK_CANCEL_ALL:
+        return new CancelAllCmd<>(target, entity, taskType);
+      default:
+        return null;
+    }
+  }
+}

+ 149 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/ResumeAllCmd.java

@@ -0,0 +1,149 @@
+package com.arialyy.aria.core.command.normal;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.download.wrapper.DGTEWrapper;
+import com.arialyy.aria.core.download.wrapper.DTEWrapper;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.core.queue.DownloadGroupTaskQueue;
+import com.arialyy.aria.core.queue.DownloadTaskQueue;
+import com.arialyy.aria.core.queue.UploadTaskQueue;
+import com.arialyy.aria.core.upload.UploadTaskEntity;
+import com.arialyy.aria.core.upload.wrapper.UTEWrapper;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import com.arialyy.aria.util.NetUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by AriaL on 2017/6/13.
+ * 恢复所有停止的任务
+ * 1.如果执行队列没有满,则开始下载任务,直到执行队列满
+ * 2.如果队列执行队列已经满了,则将所有任务添加到等待队列中
+ * 3.如果队列中只有等待状态的任务,如果执行队列没有满,则会启动等待状态的任务,如果执行队列已经满了,则会将所有等待状态的任务加载到缓存队列中
+ */
+final class ResumeAllCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+  private List<AbsTaskEntity> mWaitList = new ArrayList<>();
+
+  /**
+   * @param targetName 产生任务的对象名
+   */
+  ResumeAllCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    if (!NetUtils.isConnected(AriaManager.APP)) {
+      ALog.w(TAG, "恢复任务失败,网络未连接");
+      return;
+    }
+    if (isDownloadCmd) {
+      resumeTask(findTaskData(1));
+      resumeTask(findTaskData(2));
+    } else {
+      resumeTask(findTaskData(3));
+    }
+    resumeWaitTask();
+  }
+
+  /**
+   * 查找数据库中的所有任务数据
+   *
+   * @param type {@code 1}单任务下载任务;{@code 2}任务组下载任务;{@code 3} 单任务上传任务
+   */
+  private List<AbsTaskEntity> findTaskData(int type) {
+    // TODO: 2018/4/20 需要测试
+    List<AbsTaskEntity> tempList = new ArrayList<>();
+    if (type == 1) {
+      List<DTEWrapper> wrappers = DbEntity.findRelationData(DTEWrapper.class,
+          "DownloadTaskEntity.isGroupTask=? and DownloadTaskEntity.state!=?", "false", "1");
+      if (wrappers != null && !wrappers.isEmpty()) {
+        for (DTEWrapper w : wrappers) {
+          tempList.add(w.taskEntity);
+        }
+      }
+    } else if (type == 2) {
+      List<DGTEWrapper> wrappers =
+          DbEntity.findRelationData(DGTEWrapper.class, "DownloadGroupTaskEntity.state!=?", "1");
+      if (wrappers != null && !wrappers.isEmpty()) {
+        for (DGTEWrapper w : wrappers) {
+          tempList.add(w.taskEntity);
+        }
+      }
+    } else if (type == 3) {
+      List<UTEWrapper> wrappers =
+          DbEntity.findRelationData(UTEWrapper.class, "UploadTaskEntity.state!=?", "1");
+      if (wrappers != null && !wrappers.isEmpty()) {
+        for (UTEWrapper w : wrappers) {
+          tempList.add(w.taskEntity);
+        }
+      }
+    }
+    return tempList;
+  }
+
+  /**
+   * 恢复任务
+   */
+  private void resumeTask(List<AbsTaskEntity> taskList) {
+    if (taskList != null && !taskList.isEmpty()) {
+      for (AbsTaskEntity te : taskList) {
+        if (te == null || te.getEntity() == null) continue;
+        int state = te.getState();
+        if (state == IEntity.STATE_STOP || state == IEntity.STATE_OTHER) {
+          resumeEntity(te);
+        } else if (state == IEntity.STATE_WAIT) {
+          mWaitList.add(te);
+        } else if (state == IEntity.STATE_RUNNING) {
+          if (!mQueue.taskIsRunning(te.getEntity().getKey())) {
+            resumeEntity(te);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * 处理等待状态的任务
+   */
+  private void resumeWaitTask() {
+    int maxTaskNum = mQueue.getMaxTaskNum();
+    if (mWaitList == null || mWaitList.isEmpty()) return;
+    for (AbsTaskEntity te : mWaitList) {
+      if (mQueue.getCurrentExePoolNum() < maxTaskNum) {
+        startTask(createTask(te));
+      } else {
+        createTask(te);
+      }
+    }
+  }
+
+  /**
+   * 恢复实体任务
+   *
+   * @param te 任务实体
+   */
+  private void resumeEntity(AbsTaskEntity te) {
+    if (te instanceof DownloadTaskEntity) {
+      if (te.getRequestType() == AbsTaskEntity.D_FTP || te.getRequestType() == AbsTaskEntity.U_FTP) {
+        te.setUrlEntity(CommonUtil.getFtpUrlInfo(te.getEntity().getKey()));
+      }
+      mQueue = DownloadTaskQueue.getInstance();
+    } else if (te instanceof UploadTaskEntity) {
+      mQueue = UploadTaskQueue.getInstance();
+    } else if (te instanceof DownloadGroupTaskEntity) {
+      mQueue = DownloadGroupTaskQueue.getInstance();
+    }
+    int exeNum = mQueue.getCurrentExePoolNum();
+    if (exeNum == 0 || exeNum < mQueue.getMaxTaskNum()) {
+      startTask(createTask(te));
+    } else {
+      te.getEntity().setState(IEntity.STATE_WAIT);
+      createTask(te);
+    }
+  }
+}

+ 167 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/StartCmd.java

@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.QueueMod;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.download.wrapper.DGTEWrapper;
+import com.arialyy.aria.core.download.wrapper.DTEWrapper;
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.core.queue.DownloadGroupTaskQueue;
+import com.arialyy.aria.core.queue.DownloadTaskQueue;
+import com.arialyy.aria.core.queue.UploadTaskQueue;
+import com.arialyy.aria.core.upload.UploadTaskEntity;
+import com.arialyy.aria.core.upload.wrapper.UTEWrapper;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import com.arialyy.aria.util.NetUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by lyy on 2016/8/22.
+ * 开始命令
+ * 队列模型{@link QueueMod#NOW}、{@link QueueMod#WAIT}
+ */
+class StartCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+
+  StartCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    if (!canExeCmd) return;
+    if (!NetUtils.isConnected(AriaManager.APP)) {
+      ALog.e(TAG, "启动任务失败,网络未连接");
+      return;
+    }
+    String mod;
+    int maxTaskNum = mQueue.getMaxTaskNum();
+    AriaManager manager = AriaManager.getInstance(AriaManager.APP);
+    if (isDownloadCmd) {
+      mod = manager.getDownloadConfig().getQueueMod();
+    } else {
+      mod = manager.getUploadConfig().getQueueMod();
+    }
+
+    AbsTask task = getTask();
+    if (task == null) {
+      task = createTask();
+      if (!TextUtils.isEmpty(mTargetName)) {
+        task.setTargetName(mTargetName);
+      }
+      // 任务不存在时,根据配置不同,对任务执行操作
+      if (mod.equals(QueueMod.NOW.getTag())) {
+        startTask();
+      } else if (mod.equals(QueueMod.WAIT.getTag())) {
+        if (mQueue.getCurrentExePoolNum() < maxTaskNum
+            || task.getState() == IEntity.STATE_STOP
+            || task.getState() == IEntity.STATE_FAIL
+            || task.getState() == IEntity.STATE_OTHER
+            || task.getState() == IEntity.STATE_POST_PRE
+            || task.getState() == IEntity.STATE_COMPLETE) {
+          resumeTask();
+        } else {
+          sendWaitState();
+        }
+      }
+    } else {
+      if (!task.isRunning()) {
+        resumeTask();
+      }
+    }
+    if (mQueue.getCurrentCachePoolNum() == 0) {
+      findAllWaitTask();
+    }
+  }
+
+  /**
+   * 当缓冲队列为null时,查找数据库中所有等待中的任务
+   */
+  private void findAllWaitTask() {
+    new Thread(new WaitTaskThread()).start();
+  }
+
+  private class WaitTaskThread implements Runnable {
+
+    @Override public void run() {
+      if (isDownloadCmd) {
+        handleTask(findWaitData(1));
+        handleTask(findWaitData(2));
+      } else {
+        handleTask(findWaitData(3));
+      }
+    }
+
+    private List<AbsTaskEntity> findWaitData(int type) {
+      // TODO: 2018/4/20 需要测试
+      List<AbsTaskEntity> waitList = new ArrayList<>();
+      if (type == 1) {
+        List<DTEWrapper> wrappers = DbEntity.findRelationData(DTEWrapper.class,
+            "DownloadTaskEntity.isGroupTask=? and DownloadTaskEntity.state=?", "false", "3");
+        if (wrappers != null && !wrappers.isEmpty()) {
+          for (DTEWrapper w : wrappers) {
+            waitList.add(w.taskEntity);
+          }
+        }
+      } else if (type == 2) {
+        List<DGTEWrapper> wrappers =
+            DbEntity.findRelationData(DGTEWrapper.class, "DownloadGroupTaskEntity.state=?", "3");
+        if (wrappers != null && !wrappers.isEmpty()) {
+          for (DGTEWrapper w : wrappers) {
+            waitList.add(w.taskEntity);
+          }
+        }
+      } else if (type == 3) {
+        List<UTEWrapper> wrappers = DbEntity.findRelationData(UTEWrapper.class,
+            "UploadTaskEntity.state=?", "3");
+        if (wrappers != null && !wrappers.isEmpty()) {
+          for (UTEWrapper w : wrappers) {
+            waitList.add(w.taskEntity);
+          }
+        }
+      }
+      return waitList;
+    }
+
+    private void handleTask(List<AbsTaskEntity> waitList) {
+      for (AbsTaskEntity te : waitList) {
+        if (te.getEntity() == null) continue;
+        AbsTask task = getTask(te.getEntity());
+        if (task != null) continue;
+        if (te instanceof DownloadTaskEntity) {
+          if (te.getRequestType() == AbsTaskEntity.D_FTP || te.getRequestType() == AbsTaskEntity.U_FTP) {
+            te.setUrlEntity(CommonUtil.getFtpUrlInfo(te.getEntity().getKey()));
+          }
+          mQueue = DownloadTaskQueue.getInstance();
+        } else if (te instanceof UploadTaskEntity) {
+          mQueue = UploadTaskQueue.getInstance();
+        } else if (te instanceof DownloadGroupTaskEntity) {
+          mQueue = DownloadGroupTaskQueue.getInstance();
+        }
+        createTask(te);
+        sendWaitState();
+      }
+    }
+  }
+}

+ 20 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/StopAllCmd.java

@@ -0,0 +1,20 @@
+package com.arialyy.aria.core.command.normal;
+
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+
+/**
+ * Created by AriaL on 2017/6/13.
+ * 停止所有任务的命令,并清空所有等待队列
+ */
+final class StopAllCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+  /**
+   * @param targetName 产生任务的对象名
+   */
+  StopAllCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    stopAll();
+  }
+}

+ 51 - 0
Aria/src/main/java/com/arialyy/aria/core/command/normal/StopCmd.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.command.normal;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.util.ALog;
+
+/**
+ * Created by lyy on 2016/9/20.
+ * 停止命令
+ */
+class StopCmd<T extends AbsTaskEntity> extends AbsNormalCmd<T> {
+
+  StopCmd(String targetName, T entity, int taskType) {
+    super(targetName, entity, taskType);
+  }
+
+  @Override public void executeCmd() {
+    if (!canExeCmd) return;
+    AbsTask task = getTask();
+    if (task == null) {
+      if (mTaskEntity.getEntity().getState() == IEntity.STATE_RUNNING) {
+        stopTask();
+      } else {
+        ALog.w(TAG, "停止命令执行失败,【调度器中没有该任务】");
+      }
+    } else {
+      if (!TextUtils.isEmpty(mTargetName)) {
+        task.setTargetName(mTargetName);
+      }
+      stopTask();
+    }
+  }
+}

+ 514 - 0
Aria/src/main/java/com/arialyy/aria/core/common/AbsFileer.java

@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import android.content.Context;
+import android.util.SparseArray;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.inf.AbsNormalEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.core.inf.IEventListener;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import com.arialyy.aria.util.DbHelper;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by AriaL on 2017/7/1.
+ * 任务处理器
+ */
+public abstract class AbsFileer<ENTITY extends AbsNormalEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>>
+    implements Runnable, IUtil {
+  public static final String STATE = "_state_";
+  public static final String RECORD = "_record_";
+  protected static final long SUB_LEN = 1024 * 1024;
+
+  private final String TAG = "AbsFileer";
+  protected IEventListener mListener;
+  protected TASK_ENTITY mTaskEntity;
+  protected ENTITY mEntity;
+  protected Context mContext;
+  protected File mTempFile; //文件
+  protected StateConstance mConstance;
+  private ExecutorService mFixedThreadPool;
+  //总线程数
+  private int mTotalThreadNum;
+  //启动线程数
+  private int mStartThreadNum;
+  //已完成的线程数
+  private int mCompleteThreadNum;
+  private SparseArray<AbsThreadTask> mTask = new SparseArray<>();
+
+  /**
+   * 小于1m的文件不启用多线程
+   */
+  private Timer mTimer;
+  @Deprecated
+  private File mConfigFile;
+  /**
+   * 进度刷新间隔
+   */
+  private long mUpdateInterval = 1000;
+  protected TaskRecord mRecord;
+
+  protected AbsFileer(IEventListener listener, TASK_ENTITY taskEntity) {
+    mListener = listener;
+    mTaskEntity = taskEntity;
+    mEntity = mTaskEntity.getEntity();
+    mContext = AriaManager.APP;
+    mConstance = new StateConstance();
+  }
+
+  public void setNewTask(boolean newTask) {
+    mTaskEntity.setNewTask(newTask);
+  }
+
+  @Override public void setMaxSpeed(double maxSpeed) {
+    for (int i = 0; i < mTotalThreadNum; i++) {
+      AbsThreadTask task = mTask.get(i);
+      if (task != null) {
+        task.setMaxSpeed(maxSpeed);
+      }
+    }
+  }
+
+  @Override public void run() {
+    if (mConstance.isRunning) {
+      return;
+    }
+    startFlow();
+  }
+
+  /**
+   * 开始流程
+   */
+  private void startFlow() {
+    mConstance.resetState();
+    checkTask();
+    mConstance.TASK_RECORD = mRecord;
+    if (mListener instanceof IDownloadListener) {
+      ((IDownloadListener) mListener).onPostPre(mEntity.getFileSize());
+    }
+    if (!mTaskEntity.isSupportBP()) {
+      mTotalThreadNum = 1;
+      mStartThreadNum = 1;
+      handleNoSupportBP();
+    } else {
+      mTotalThreadNum =
+          mTaskEntity.isNewTask() ? (mStartThreadNum = setNewTaskThreadNum()) : mTotalThreadNum;
+      handleBreakpoint();
+    }
+    mConstance.START_THREAD_NUM = mTotalThreadNum;
+    startTimer();
+  }
+
+  /**
+   * 设置新任务的最大线程数
+   */
+  protected abstract int setNewTaskThreadNum();
+
+  /**
+   * 启动进度获取定时器
+   */
+  private void startTimer() {
+    mTimer = new Timer(true);
+    mTimer.schedule(new TimerTask() {
+      @Override public void run() {
+        if (mConstance.isComplete()
+            || mConstance.isStop()
+            || mConstance.isCancel()
+            || !mConstance.isRunning) {
+          closeTimer();
+        } else if (mConstance.CURRENT_LOCATION >= 0) {
+          mListener.onProgress(mConstance.CURRENT_LOCATION);
+        }
+      }
+    }, 0, mUpdateInterval);
+  }
+
+  protected void closeTimer() {
+    if (mTimer != null) {
+      mTimer.purge();
+      mTimer.cancel();
+      mTimer = null;
+    }
+  }
+
+  /**
+   * 设置定时器更新间隔
+   *
+   * @param interval 单位毫秒,不能小于0
+   */
+  protected void setUpdateInterval(long interval) {
+    if (interval < 0) {
+      ALog.w(TAG, "更新间隔不能小于0,默认为1000毫秒");
+      return;
+    }
+    mUpdateInterval = interval;
+  }
+
+  @Override public long getFileSize() {
+    return mEntity.getFileSize();
+  }
+
+  /**
+   * 获取当前下载位置
+   */
+  @Override public long getCurrentLocation() {
+    return mConstance.CURRENT_LOCATION;
+  }
+
+  @Override public boolean isRunning() {
+    return mConstance.isRunning;
+  }
+
+  @Override public void cancel() {
+    closeTimer();
+    mConstance.isRunning = false;
+    mConstance.isCancel = true;
+    if (mFixedThreadPool != null) {
+      mFixedThreadPool.shutdown();
+    }
+    for (int i = 0; i < mStartThreadNum; i++) {
+      AbsThreadTask task = mTask.get(i);
+      if (task != null) {
+        task.cancel();
+      }
+    }
+  }
+
+  @Override public void stop() {
+    closeTimer();
+    mConstance.isRunning = false;
+    mConstance.isStop = true;
+    if (mConstance.isComplete()) return;
+    if (mFixedThreadPool != null) {
+      mFixedThreadPool.shutdown();
+    }
+    for (int i = 0; i < mStartThreadNum; i++) {
+      AbsThreadTask task = mTask.get(i);
+      if (task != null) {
+        task.stop();
+      }
+    }
+  }
+
+  /**
+   * 直接调用的时候会自动启动线程执行
+   */
+  @Override public void start() {
+    new Thread(this).start();
+  }
+
+  @Override public void resume() {
+    start();
+  }
+
+  /**
+   * 检查任务、检查线程数
+   * 新任务条件:
+   * 1、文件不存在
+   * 2、下载记录文件缺失或不匹配
+   * 3、数据库记录不存在
+   * 4、不支持断点,则是新任务
+   */
+  protected void checkTask() {
+    mConfigFile = new File(CommonUtil.getFileConfigPath(false, mEntity.getFileName()));
+    if (mConfigFile.exists()) {
+      convertDb();
+    } else {
+      mRecord = DbHelper.getTaskRecord(mTaskEntity.getKey());
+      if (mRecord == null) {
+        initRecord();
+        mTaskEntity.setNewTask(true);
+      } else {
+        if (mRecord.threadRecords == null || mRecord.threadRecords.isEmpty()) {
+          initRecord();
+          mTaskEntity.setNewTask(true);
+        } else if (mTempFile.length() == 0) {
+          mRecord.deleteData();
+          initRecord();
+          mTaskEntity.setNewTask(true);
+        } else {
+          for (ThreadRecord tr : mRecord.threadRecords) {
+            if (tr.isComplete) {
+              mCompleteThreadNum++;
+            } else {
+              mStartThreadNum++;
+            }
+          }
+          mTotalThreadNum = mRecord.threadRecords.size();
+          mTaskEntity.setNewTask(false);
+        }
+      }
+    }
+  }
+
+  /**
+   * convertDb 为兼容性代码
+   * 从3.4.1开始,线程配置信息将存储在数据库中。
+   * 将配置文件的内容复制到数据库中,并将配置文件删除
+   */
+  private void convertDb() {
+    List<RecordWrapper> records =
+        DbEntity.findRelationData(RecordWrapper.class, "TaskRecord.filePath=?",
+            mTaskEntity.getKey());
+    if (records == null || records.size() == 0) {
+      Properties pro = CommonUtil.loadConfig(mConfigFile);
+      if (pro.isEmpty()) {
+        mTaskEntity.setNewTask(true);
+        return;
+      }
+      initRecord();
+      Set<Object> keys = pro.keySet();
+      // 老版本记录是5s存一次,但是5s中内,如果线程执行完成,record记录是没有的,只有state记录...
+      // 第一步应该是record 和 state去重取正确的线程数
+      Set<Integer> set = new HashSet<>();
+      for (Object key : keys) {
+        String str = String.valueOf(key);
+        int i = Integer.parseInt(str.substring(str.length() - 1, str.length()));
+        set.add(i);
+      }
+      int threadNum = set.size();
+      if (threadNum == 0) {
+        mTaskEntity.setNewTask(true);
+        return;
+      }
+      mRecord.threadNum = threadNum;
+      mTotalThreadNum = threadNum;
+
+      for (int i = 0; i < threadNum; i++) {
+        ThreadRecord tRecord = new ThreadRecord();
+        tRecord.key = mRecord.filePath;
+        Object state = pro.getProperty(mTempFile.getName() + STATE + i);
+        Object record = pro.getProperty(mTempFile.getName() + RECORD + i);
+        if (state != null && Integer.parseInt(state + "") == 1) {
+          mCompleteThreadNum++;
+          tRecord.isComplete = true;
+          continue;
+        }
+        mStartThreadNum++;
+        if (record != null) {
+          Long temp = Long.parseLong(record + "");
+          tRecord.startLocation = temp > 0 ? temp : 0;
+        } else {
+          tRecord.startLocation = 0;
+        }
+        mRecord.threadRecords.add(tRecord);
+      }
+      mConfigFile.delete();
+    }
+  }
+
+  /**
+   * 初始化记录
+   */
+  private void initRecord() {
+    mRecord = new TaskRecord();
+    mRecord.fileName = mEntity.getFileName();
+    mRecord.filePath = mTaskEntity.getKey();
+    mRecord.threadRecords = new ArrayList<>();
+    mRecord.isGroupRecord = mTaskEntity.getEntity().isGroupChild();
+    mRecord.isOpenDynamicFile =
+        AriaManager.getInstance(AriaManager.APP).getDownloadConfig().isOpenDynamicFile();
+    if (mRecord.isGroupRecord) {
+      if (mTaskEntity.getEntity() instanceof DownloadEntity) {
+        mRecord.dGroupName = ((DownloadEntity) mTaskEntity.getEntity()).getGroupName();
+      }
+    }
+  }
+
+  /**
+   * 保存任务记录
+   */
+  private void saveRecord() {
+    mRecord.save();
+    for (ThreadRecord tr : mRecord.threadRecords) {
+      tr.save();
+    }
+  }
+
+  public TaskRecord getRecord() {
+    return mRecord;
+  }
+
+  /**
+   * 恢复记录地址
+   *
+   * @return {@code true}任务已完成
+   */
+  private boolean resumeRecordLocation(int i, long startL, long endL) {
+    mConstance.CURRENT_LOCATION += endL - startL;
+    ALog.d(TAG, "任务【" + mTaskEntity.getEntity().getFileName() + "】线程__" + i + "__已完成");
+    mConstance.COMPLETE_THREAD_NUM = mCompleteThreadNum;
+    mConstance.STOP_NUM++;
+    mConstance.CANCEL_NUM++;
+    if (mConstance.isComplete()) {
+      mRecord.deleteData();
+      mListener.onComplete();
+      mConstance.isRunning = false;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * 启动断点任务时,创建单线程任务
+   *
+   * @param i 线程id
+   * @param startL 该任务起始位置
+   * @param endL 该任务结束位置
+   * @param fileLength 该任务需要处理的文件长度
+   */
+  private AbsThreadTask createSingThreadTask(int i, long startL, long endL, long fileLength,
+      ThreadRecord record) {
+    SubThreadConfig<TASK_ENTITY> config = new SubThreadConfig<>();
+    config.FILE_SIZE = fileLength;
+    config.URL = mEntity.isRedirect() ? mEntity.getRedirectUrl() : mEntity.getUrl();
+    config.TEMP_FILE = mTempFile;
+    config.THREAD_ID = i;
+    config.START_LOCATION = startL;
+    config.END_LOCATION = endL;
+    config.SUPPORT_BP = mTaskEntity.isSupportBP();
+    config.TASK_ENTITY = mTaskEntity;
+    config.THREAD_RECORD = record;
+    return selectThreadTask(config);
+  }
+
+  private void handleBreakpoint() {
+    long fileLength = mEntity.getFileSize();
+    long blockSize = fileLength / mTotalThreadNum;
+    int[] threadId = new int[mTotalThreadNum];
+    int rl = 0;
+
+    mRecord.fileLength = fileLength;
+    for (int i = 0; i < mTotalThreadNum; i++) {
+      threadId[i] = -1;
+    }
+    if (mTaskEntity.isNewTask() && !handleNewTask()) {
+      return;
+    }
+    for (int i = 0; i < mTotalThreadNum; i++) {
+      long startL = i * blockSize, endL = (i + 1) * blockSize;
+      ThreadRecord tr;
+      boolean isNewTr = false;  // 是否是新的线程记录
+      if (mTaskEntity.isNewTask()) {
+        tr = new ThreadRecord();
+        tr.key = mRecord.filePath;
+        tr.threadId = i;
+        isNewTr = true;
+      } else {
+        tr = mRecord.threadRecords.get(i);
+      }
+      if (tr.isComplete) {//该线程已经完成
+        if (resumeRecordLocation(i, startL, endL)) return;
+        continue;
+      }
+
+      //如果有记录,则恢复下载
+      if (tr.startLocation >= 0) {
+        Long r = tr.startLocation;
+        //记录的位置需要在线程区间中
+        if (startL < r && r < (i == (mTotalThreadNum - 1) ? fileLength : endL)) {
+          mConstance.CURRENT_LOCATION += r - startL;
+          startL = r;
+        }
+        ALog.d(TAG, "任务【" + mEntity.getFileName() + "】线程__" + i + "__恢复下载");
+      }
+      //最后一个线程的结束位置即为文件的总长度
+      if (i == (mTotalThreadNum - 1)) {
+        endL = fileLength;
+      }
+      // 更新记录
+      tr.startLocation = startL;
+      tr.endLocation = endL;
+      if (isNewTr) {
+        mRecord.threadRecords.add(tr);
+      }
+      AbsThreadTask task = createSingThreadTask(i, startL, endL, fileLength, tr);
+      if (task == null) return;
+      mTask.put(i, task);
+      threadId[rl] = i;
+      rl++;
+    }
+    saveRecord();
+    startThreadTask(threadId);
+  }
+
+  /**
+   * 启动单线程下载任务
+   */
+  private void startThreadTask(int[] recordL) {
+    if (mConstance.CURRENT_LOCATION > 0) {
+      mListener.onResume(mConstance.CURRENT_LOCATION);
+    } else {
+      mListener.onStart(mConstance.CURRENT_LOCATION);
+    }
+    mFixedThreadPool = Executors.newFixedThreadPool(recordL.length);
+    for (int l : recordL) {
+      if (l == -1) continue;
+      Runnable task = mTask.get(l);
+      if (task != null) {
+        mFixedThreadPool.execute(task);
+      }
+    }
+  }
+
+  /**
+   * 处理新任务
+   *
+   * @return {@code true}创建新任务失败
+   */
+  protected abstract boolean handleNewTask();
+
+  /**
+   * 处理不支持断点的下载
+   */
+  private void handleNoSupportBP() {
+    SubThreadConfig<TASK_ENTITY> config = new SubThreadConfig<>();
+    config.FILE_SIZE = mEntity.getFileSize();
+    config.URL = mEntity.isRedirect() ? mEntity.getRedirectUrl() : mEntity.getUrl();
+    config.TEMP_FILE = mTempFile;
+    config.THREAD_ID = 0;
+    config.START_LOCATION = 0;
+    config.END_LOCATION = config.FILE_SIZE;
+    config.SUPPORT_BP = mTaskEntity.isSupportBP();
+    config.TASK_ENTITY = mTaskEntity;
+    AbsThreadTask task = selectThreadTask(config);
+    if (task == null) return;
+    mTask.put(0, task);
+    mFixedThreadPool = Executors.newFixedThreadPool(1);
+    mFixedThreadPool.execute(task);
+    mListener.onStart(0);
+  }
+
+  /**
+   * 选择单任务线程的类型
+   */
+  protected abstract AbsThreadTask selectThreadTask(SubThreadConfig<TASK_ENTITY> config);
+}

+ 298 - 0
Aria/src/main/java/com/arialyy/aria/core/common/AbsFtpInfoThread.java

@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.FtpUrlEntity;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.upload.UploadEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.Regular;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPFile;
+import org.apache.commons.net.ftp.FTPReply;
+
+/**
+ * Created by Aria.Lao on 2017/7/25.
+ * 获取ftp文件夹信息
+ */
+public abstract class AbsFtpInfoThread<ENTITY extends AbsEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>>
+    implements Runnable {
+
+  private final String TAG = "AbsFtpInfoThread";
+  protected ENTITY mEntity;
+  protected TASK_ENTITY mTaskEntity;
+  private int mConnectTimeOut;
+  protected OnFileInfoCallback mCallback;
+  protected long mSize = 0;
+  protected String charSet = "UTF-8";
+  private boolean isUpload = false;
+
+  public AbsFtpInfoThread(TASK_ENTITY taskEntity, OnFileInfoCallback callback) {
+    mTaskEntity = taskEntity;
+    mEntity = taskEntity.getEntity();
+    mConnectTimeOut =
+        AriaManager.getInstance(AriaManager.APP).getDownloadConfig().getConnectTimeOut();
+    mCallback = callback;
+    if (mEntity instanceof UploadEntity) {
+      isUpload = true;
+    }
+  }
+
+  /**
+   * 设置请求的远程文件路径
+   *
+   * @return 远程文件路径
+   */
+  protected abstract String setRemotePath();
+
+  @Override public void run() {
+    FTPClient client = null;
+    try {
+      client = createFtpClient();
+      if (client == null) {
+        failDownload("创建FTP客户端失败", true);
+        return;
+      }
+      String remotePath =
+          new String(setRemotePath().getBytes(charSet), AbsFtpThreadTask.SERVER_CHARSET);
+      FTPFile[] files = client.listFiles(remotePath);
+      String s = client.getReplyString();
+      ALog.i(TAG, s);
+      boolean isExist = files.length != 0;
+      if (!isExist && !isUpload) {
+        failDownload("文件不存在,任务链接【" + mTaskEntity.getUrlEntity().url + "】,remotePath:" + remotePath,
+            false);
+        int i = remotePath.lastIndexOf(File.separator);
+        FTPFile[] files1;
+        if (i == -1) {
+          files1 = client.listFiles();
+        } else {
+          files1 = client.listFiles(remotePath.substring(0, i + 1));
+        }
+        if (files1.length > 0) {
+          ALog.i(TAG, "路径【" + setRemotePath() + "】下的文件列表 ===================================");
+          for (FTPFile file : files1) {
+            ALog.d(TAG, file.toString());
+          }
+          ALog.i(TAG,
+              "================================= --end-- ===================================");
+        } else {
+          String msg = client.getReplyString();
+          ALog.w(TAG, msg);
+        }
+        client.disconnect();
+        return;
+      }
+      //为了防止编码错乱,需要使用原始字符串
+      mSize = getFileSize(files, client, setRemotePath());
+      int reply = client.getReplyCode();
+      if (!FTPReply.isPositiveCompletion(reply)) {
+        if (isUpload) {
+          //服务器上没有该文件路径,表示该任务为新的上传任务
+          mTaskEntity.setNewTask(true);
+        } else {
+          client.disconnect();
+          failDownload("获取文件信息错误,错误码为:" + reply + ",msg:" + client.getReplyString(), true);
+          return;
+        }
+      }
+      mTaskEntity.setCode(reply);
+      if (mSize != 0 && !isUpload) {
+        mEntity.setFileSize(mSize);
+      }
+      mTaskEntity.update();
+      onPreComplete(reply);
+    } catch (IOException e) {
+      failDownload(e.getMessage(), true);
+    } finally {
+      if (client != null) {
+        try {
+          client.disconnect();
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+  }
+
+  /**
+   * 检查文件是否存在
+   *
+   * @return {@code true}存在
+   */
+  private boolean checkFileExist(FTPFile[] ftpFiles, String fileName) {
+    for (FTPFile ff : ftpFiles) {
+      if (ff.getName().equals(fileName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public void start() {
+    new Thread(this).start();
+  }
+
+  protected void onPreComplete(int code) {
+
+  }
+
+  /**
+   * 创建FTP客户端
+   */
+  private FTPClient createFtpClient() {
+    FTPClient client = null;
+    final FtpUrlEntity urlEntity = mTaskEntity.getUrlEntity();
+    try {
+      Pattern p = Pattern.compile(Regular.REG_IP_V4);
+      Matcher m = p.matcher(urlEntity.hostName);
+      if (m.find() && m.groupCount() > 0) {
+        client = new FTPClient();
+        InetAddress ip = InetAddress.getByName(urlEntity.hostName);
+        client.setConnectTimeout(10000);  // 连接10s超时
+        client.connect(ip, Integer.parseInt(urlEntity.port));
+        mTaskEntity.getUrlEntity().validAddr = ip;
+      } else {
+        InetAddress[] ips = InetAddress.getAllByName(urlEntity.hostName);
+        client = connect(new FTPClient(), ips, 0, Integer.parseInt(urlEntity.port));
+      }
+
+      if (client == null) {
+        failDownload("链接失败", false);
+        return null;
+      }
+
+      boolean loginSuccess = true;
+      if (urlEntity.needLogin) {
+        try {
+          if (TextUtils.isEmpty(urlEntity.account)) {
+            loginSuccess = client.login(urlEntity.user, urlEntity.password);
+          } else {
+            loginSuccess = client.login(urlEntity.user, urlEntity.password, urlEntity.account);
+          }
+        } catch (IOException e) {
+          ALog.e(TAG, client.getReplyString());
+          return null;
+        }
+      }
+
+      if (!loginSuccess) {
+        failDownload("登录失败", false);
+        return null;
+      }
+
+      int reply = client.getReplyCode();
+      if (!FTPReply.isPositiveCompletion(reply)) {
+        client.disconnect();
+        failDownload("无法连接到ftp服务器,错误码为:" + reply + ",msg:" + client.getReplyString(), true);
+        return null;
+      }
+      // 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码
+      charSet = "UTF-8";
+      reply = client.sendCommand("OPTS UTF8", "ON");
+      if (!TextUtils.isEmpty(mTaskEntity.getCharSet()) || (!FTPReply.isPositiveCompletion(reply)
+          && reply != FTPReply.COMMAND_OK)) {
+        ALog.i(TAG, "FTP 服务器不支持开启UTF8编码,尝试使用Aria手动设置的编码");
+        charSet = mTaskEntity.getCharSet();
+      }
+      client.setControlEncoding(charSet);
+      client.setDataTimeout(10 * 1000);
+      client.enterLocalPassiveMode();
+      client.setFileType(FTP.BINARY_FILE_TYPE);
+      client.setConnectTimeout(mConnectTimeOut);
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+    return client;
+  }
+
+  /**
+   * 连接到ftp服务器
+   */
+  private FTPClient connect(FTPClient client, InetAddress[] ips, int index, int port) {
+    try {
+      client.connect(ips[index], port);
+      mTaskEntity.getUrlEntity().validAddr = ips[index];
+      return client;
+    } catch (IOException e) {
+      //e.printStackTrace();
+      try {
+        if (client.isConnected()) {
+          client.disconnect();
+        }
+      } catch (IOException e1) {
+        e1.printStackTrace();
+      }
+      if (index + 1 >= ips.length) {
+        ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,已没有其他地址,链接失败");
+        return null;
+      }
+      try {
+        Thread.sleep(1000);
+      } catch (InterruptedException e1) {
+        e1.printStackTrace();
+      }
+      ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,正在尝试下一个地址");
+      return connect(new FTPClient(), ips, index + 1, port);
+    }
+  }
+
+  /**
+   * 遍历FTP服务器上对应文件或文件夹大小
+   *
+   * @throws IOException 字符串编码转换错误
+   */
+  private long getFileSize(FTPFile[] files, FTPClient client, String dirName) throws IOException {
+    long size = 0;
+    String path = dirName + "/";
+    for (FTPFile file : files) {
+      if (file.isFile()) {
+        size += file.getSize();
+        handleFile(path + file.getName(), file);
+      } else {
+        String remotePath =
+            new String((path + file.getName()).getBytes(charSet), AbsFtpThreadTask.SERVER_CHARSET);
+        size += getFileSize(client.listFiles(remotePath), client, path + file.getName());
+      }
+    }
+    return size;
+  }
+
+  /**
+   * 处理FTP文件信息
+   *
+   * @param remotePath ftp服务器文件夹路径
+   * @param ftpFile ftp服务器上对应的文件
+   */
+  protected void handleFile(String remotePath, FTPFile ftpFile) {
+  }
+
+  private void failDownload(String errorMsg, boolean needRetry) {
+    ALog.e(TAG, errorMsg);
+    if (mCallback != null) {
+      mCallback.onFail(mEntity.getKey(), errorMsg, needRetry);
+    }
+  }
+}

+ 141 - 0
Aria/src/main/java/com/arialyy/aria/core/common/AbsFtpThreadTask.java

@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.FtpUrlEntity;
+import com.arialyy.aria.core.inf.AbsNormalEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IEventListener;
+import com.arialyy.aria.util.ALog;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPReply;
+
+/**
+ * Created by lyy on 2017/9/26.
+ * FTP单任务父类
+ */
+public abstract class AbsFtpThreadTask<ENTITY extends AbsNormalEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>>
+    extends AbsThreadTask<ENTITY, TASK_ENTITY> {
+  private final String TAG = "AbsFtpThreadTask";
+  protected String charSet, port;
+  /**
+   * D_FTP 服务器编码
+   */
+  public static String SERVER_CHARSET = "ISO-8859-1";
+
+  protected AbsFtpThreadTask(StateConstance constance, IEventListener listener,
+      SubThreadConfig<TASK_ENTITY> info) {
+    super(constance, listener, info);
+  }
+
+  /**
+   * 构建FTP客户端
+   */
+  protected FTPClient createClient() {
+    FTPClient client = null;
+    final FtpUrlEntity urlEntity = mTaskEntity.getUrlEntity();
+    if (urlEntity.validAddr == null) {
+      try {
+        InetAddress[] ips = InetAddress.getAllByName(urlEntity.hostName);
+        client = connect(new FTPClient(), ips, 0, Integer.parseInt(urlEntity.port));
+        if (client == null) {
+          return null;
+        }
+      } catch (UnknownHostException e) {
+        e.printStackTrace();
+      }
+    } else {
+      client = new FTPClient();
+      try {
+        client.connect(urlEntity.validAddr, Integer.parseInt(urlEntity.port));
+      } catch (IOException e) {
+        ALog.e(TAG, ALog.getExceptionString(e));
+        return null;
+      }
+    }
+
+    if (client == null) {
+      return null;
+    }
+
+    try {
+      if (urlEntity.needLogin) {
+        if (TextUtils.isEmpty(urlEntity.account)) {
+          client.login(urlEntity.user, urlEntity.password);
+        } else {
+          client.login(urlEntity.user, urlEntity.password, urlEntity.account);
+        }
+      }
+      int reply = client.getReplyCode();
+      if (!FTPReply.isPositiveCompletion(reply)) {
+        client.disconnect();
+        fail(STATE.CURRENT_LOCATION,
+            "无法连接到ftp服务器,错误码为:" + reply + ",msg:" + client.getReplyString(), null);
+        return null;
+      }
+      // 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码
+      charSet = "UTF-8";
+      if (!TextUtils.isEmpty(mTaskEntity.getCharSet()) || !FTPReply.isPositiveCompletion(
+          client.sendCommand("OPTS UTF8", "ON"))) {
+        charSet = mTaskEntity.getCharSet();
+      }
+      client.setControlEncoding(charSet);
+      client.setDataTimeout(mReadTimeOut);
+      client.setConnectTimeout(mConnectTimeOut);
+      client.enterLocalPassiveMode();
+      client.setFileType(FTP.BINARY_FILE_TYPE);
+      client.setControlKeepAliveTimeout(5000);
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+    return client;
+  }
+
+  /**
+   * 连接到ftp服务器
+   */
+  private FTPClient connect(FTPClient client, InetAddress[] ips, int index, int port) {
+    try {
+      client.connect(ips[index], port);
+      mTaskEntity.getUrlEntity().validAddr = ips[index];
+      return client;
+    } catch (IOException e) {
+      try {
+        if (client.isConnected()) {
+          client.disconnect();
+        }
+      } catch (IOException e1) {
+        e1.printStackTrace();
+      }
+      if (index + 1 >= ips.length) {
+        ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,已没有其他地址,链接失败");
+        return null;
+      }
+      try {
+        Thread.sleep(1000);
+      } catch (InterruptedException e1) {
+        e1.printStackTrace();
+      }
+      ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,正在尝试下一个地址");
+      return connect(new FTPClient(), ips, index + 1, port);
+    }
+  }
+}

+ 265 - 0
Aria/src/main/java/com/arialyy/aria/core/common/AbsThreadTask.java

@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import android.os.Build;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.inf.AbsNormalEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IEventListener;
+import com.arialyy.aria.core.upload.UploadEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.ErrorHelp;
+import com.arialyy.aria.util.NetUtils;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by lyy on 2017/1/18.
+ * 任务线程
+ */
+public abstract class AbsThreadTask<ENTITY extends AbsNormalEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>>
+    implements Runnable {
+  /**
+   * 线程重试次数
+   */
+  private final int RETRY_NUM = 2;
+  /**
+   * 线程重试间隔
+   */
+  private final int RETRY_INTERVAL = 5000;
+  private final String TAG = "AbsThreadTask";
+  protected long mChildCurrentLocation = 0, mSleepTime = 0;
+  protected int mBufSize;
+  protected IEventListener mListener;
+  protected StateConstance STATE;
+  protected SubThreadConfig<TASK_ENTITY> mConfig;
+  protected ENTITY mEntity;
+  protected TASK_ENTITY mTaskEntity;
+  private int mFailNum = 0;
+  private String mTaskType;
+  private Timer mFailTimer;
+  private long mLastSaveTime;
+  private ExecutorService mConfigThreadPool;
+  protected int mConnectTimeOut; //连接超时时间
+  protected int mReadTimeOut; //流读取的超时时间
+  protected boolean isNotNetRetry = false;  //断网情况是否重试
+
+  private Thread mConfigThread = new Thread(new Runnable() {
+    @Override public void run() {
+      final long currentTemp = mChildCurrentLocation;
+      try {
+        writeConfig(false, currentTemp);
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+  });
+
+  protected AbsThreadTask(StateConstance constance, IEventListener listener,
+      SubThreadConfig<TASK_ENTITY> info) {
+    STATE = constance;
+    mListener = listener;
+    mConfig = info;
+    mTaskEntity = mConfig.TASK_ENTITY;
+    mEntity = mTaskEntity.getEntity();
+    mTaskType = getTaskType();
+    mLastSaveTime = System.currentTimeMillis();
+    mConfigThreadPool = Executors.newCachedThreadPool();
+  }
+
+  protected abstract String getTaskType();
+
+  public void setMaxSpeed(double maxSpeed) {
+    if (-0.9999 < maxSpeed && maxSpeed < 0.00001) {
+      mSleepTime = 0;
+    } else {
+      BigDecimal db = new BigDecimal(
+          ((mBufSize / 1024) * (filterVersion() ? 1 : STATE.START_THREAD_NUM) / maxSpeed) * 1000);
+      mSleepTime = db.setScale(0, BigDecimal.ROUND_HALF_UP).longValue();
+    }
+  }
+
+  private boolean filterVersion() {
+    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+  }
+
+  @Override protected void finalize() throws Throwable {
+    super.finalize();
+    if (mConfigThreadPool != null) {
+      mConfigThreadPool.shutdown();
+    }
+  }
+
+  /**
+   * 停止任务
+   */
+  public void stop() {
+    synchronized (AriaManager.LOCK) {
+      try {
+        if (mConfig.SUPPORT_BP) {
+          final long currentTemp = mChildCurrentLocation;
+          STATE.STOP_NUM++;
+          ALog.d(TAG, "任务【"
+              + mConfig.TEMP_FILE.getName()
+              + "】thread__"
+              + mConfig.THREAD_ID
+              + "__停止【停止位置: "
+              + currentTemp
+              + "】");
+          writeConfig(false, currentTemp);
+          if (STATE.isStop()) {
+            ALog.i(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】已停止");
+            STATE.isRunning = false;
+            mListener.onStop(STATE.CURRENT_LOCATION);
+          }
+        } else {
+          ALog.i(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】已停止");
+          STATE.isRunning = false;
+          mListener.onStop(STATE.CURRENT_LOCATION);
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  /**
+   * 执行中
+   */
+  protected void progress(long len) {
+    synchronized (AriaManager.LOCK) {
+      mChildCurrentLocation += len;
+      STATE.CURRENT_LOCATION += len;
+      if (System.currentTimeMillis() - mLastSaveTime > 5000
+          && mChildCurrentLocation < mConfig.END_LOCATION) {
+        mLastSaveTime = System.currentTimeMillis();
+        if (!mConfigThreadPool.isShutdown()) {
+          mConfigThreadPool.execute(mConfigThread);
+        }
+      }
+    }
+  }
+
+  /**
+   * 取消任务
+   */
+  public void cancel() {
+    synchronized (AriaManager.LOCK) {
+      if (mConfig.SUPPORT_BP) {
+        STATE.CANCEL_NUM++;
+        ALog.d(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】thread__" + mConfig.THREAD_ID + "__取消");
+        if (STATE.isCancel()) {
+          if (mConfig.TEMP_FILE.exists() && !(mEntity instanceof UploadEntity)) {
+            mConfig.TEMP_FILE.delete();
+          }
+          ALog.d(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】已取消");
+          STATE.isRunning = false;
+          mListener.onCancel();
+        }
+      } else {
+        ALog.d(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】已取消");
+        STATE.isRunning = false;
+        mListener.onCancel();
+      }
+    }
+  }
+
+  /**
+   * 任务失败
+   */
+  protected void fail(final long currentLocation, String msg, Exception ex) {
+    synchronized (AriaManager.LOCK) {
+      try {
+        if (ex != null) {
+          ALog.e(TAG, msg + "\n" + ALog.getExceptionString(ex));
+        } else {
+          ALog.e(TAG, msg);
+        }
+        if (mConfig.SUPPORT_BP) {
+          writeConfig(false, currentLocation);
+          retryThis(STATE.START_THREAD_NUM != 1);
+        } else {
+          ALog.e(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】执行失败");
+          mListener.onFail(true);
+          ErrorHelp.saveError(TAG, "", ALog.getExceptionString(ex));
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  /**
+   * 重试当前线程,如果其中一条线程已经下载失败,则任务该任务下载失败,并且停止该任务的所有线程
+   *
+   * @param needRetry 是否可以重试
+   */
+  private void retryThis(boolean needRetry) {
+    if (mFailTimer != null) {
+      mFailTimer.purge();
+      mFailTimer.cancel();
+    }
+    if (!NetUtils.isConnected(AriaManager.APP) && !isNotNetRetry) {
+      ALog.w(TAG,
+          "任务【" + mConfig.TEMP_FILE.getName() + "】thread__" + mConfig.THREAD_ID + "__重试失败,网络未连接");
+    }
+    if (mFailNum < RETRY_NUM
+        && needRetry
+        && NetUtils.isConnected(AriaManager.APP)
+        && !isNotNetRetry
+        && !STATE.isCancel
+        && !STATE.isStop) {
+      mFailTimer = new Timer(true);
+      mFailTimer.schedule(new TimerTask() {
+        @Override public void run() {
+          mFailNum++;
+          ALog.w(TAG,
+              "任务【" + mConfig.TEMP_FILE.getName() + "】thread__" + mConfig.THREAD_ID + "__正在重试");
+          final long retryLocation =
+              mChildCurrentLocation == 0 ? mConfig.START_LOCATION : mChildCurrentLocation;
+          mConfig.START_LOCATION = retryLocation;
+          AbsThreadTask.this.run();
+        }
+      }, RETRY_INTERVAL);
+    } else {
+      STATE.FAIL_NUM++;
+      if (STATE.isFail()) {
+        STATE.isRunning = false;
+        STATE.isStop = true;
+        ALog.e(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】执行失败");
+        mListener.onFail(true);
+      }
+    }
+  }
+
+  /**
+   * 将记录写入到配置文件
+   */
+  protected void writeConfig(boolean isComplete, final long record) throws IOException {
+    if (mConfig.THREAD_RECORD != null) {
+      mConfig.THREAD_RECORD.isComplete = isComplete;
+      if (0 < record && record < mConfig.END_LOCATION) {
+        mConfig.THREAD_RECORD.startLocation = record;
+      }
+      mConfig.THREAD_RECORD.update();
+    }
+  }
+}

+ 31 - 0
Aria/src/main/java/com/arialyy/aria/core/common/CompleteInfo.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+/**
+ * Created by AriaL on 2018/3/3.
+ * 获取文件信息完成后 回调给下载线程的信息
+ */
+public class CompleteInfo {
+  /**
+   * 自定义的状态码
+   */
+  public int code;
+
+  public CompleteInfo(int code) {
+    this.code = code;
+  }
+}

+ 66 - 0
Aria/src/main/java/com/arialyy/aria/core/common/IUtil.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+/**
+ * Created by lyy on 2016/10/31.
+ * 任务功能接口
+ */
+public interface IUtil {
+
+  /**
+   * 获取文件大小
+   */
+  long getFileSize();
+
+  /**
+   * 获取当前位置
+   */
+  long getCurrentLocation();
+
+  /**
+   * 任务是否正在执行
+   *
+   * @return {@code true} 任务正在执行
+   */
+  boolean isRunning();
+
+  /**
+   * 取消
+   */
+  void cancel();
+
+  /**
+   * 停止
+   */
+  void stop();
+
+  /**
+   * 开始
+   */
+  void start();
+
+  /**
+   * 从上次断点恢复
+   */
+  void resume();
+
+  /**
+   * 设置最大速度
+   */
+  void setMaxSpeed(double maxSpeed);
+}

+ 32 - 0
Aria/src/main/java/com/arialyy/aria/core/common/OnFileInfoCallback.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+public interface OnFileInfoCallback {
+  /**
+   * 处理完成
+   *
+   * @param info 一些回调的信息
+   */
+  void onComplete(String url, CompleteInfo info);
+
+  /**
+   * 请求失败
+   *
+   * @param errorMsg 错误信息
+   */
+  void onFail(String url, String errorMsg, boolean needRetry);
+}

+ 107 - 0
Aria/src/main/java/com/arialyy/aria/core/common/ProxyHelper.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by Aria.Lao on 2017/7/10.
+ * 代理参数获取
+ */
+public class ProxyHelper {
+  public Set<String> downloadCounter, uploadCounter, downloadGroupCounter, downloadGroupSubCounter;
+
+  public static volatile ProxyHelper INSTANCE = null;
+
+  private ProxyHelper() {
+    init();
+  }
+
+  public static ProxyHelper getInstance() {
+    if (INSTANCE == null) {
+      synchronized (AriaManager.LOCK) {
+        INSTANCE = new ProxyHelper();
+      }
+    }
+    return INSTANCE;
+  }
+
+  private void init() {
+    List<String> classes = CommonUtil.getClassName(AriaManager.APP, "com.arialyy.aria");
+    for (String className : classes) {
+      if (!className.startsWith("com.arialyy.aria.ProxyClassCounter")){
+        continue;
+      }
+      count(className);
+    }
+  }
+
+  private void count(String className) {
+    try {
+      Class clazz = Class.forName(className);
+      Method download = clazz.getMethod("getDownloadCounter");
+      Method downloadGroup = clazz.getMethod("getDownloadGroupCounter");
+      Method downloadGroupSub = clazz.getMethod("getDownloadGroupSubCounter");
+      Method upload = clazz.getMethod("getUploadCounter");
+      Object object = clazz.newInstance();
+      Object dc = download.invoke(object);
+      if (dc != null) {
+        if (downloadCounter == null) {
+          downloadCounter = new HashSet<>();
+        }
+        downloadCounter.addAll((Set<String>) dc);
+      }
+      Object dgc = downloadGroup.invoke(object);
+      if (dgc != null) {
+        if (downloadGroupCounter == null) {
+          downloadGroupCounter = new HashSet<>();
+        }
+        downloadGroupCounter.addAll((Set<String>) dgc);
+      }
+      Object dgsc = downloadGroupSub.invoke(object);
+      if (dgsc != null) {
+        if (downloadGroupSubCounter == null) {
+          downloadGroupSubCounter = new HashSet<>();
+        }
+        downloadGroupSubCounter.addAll((Set<String>) dgsc);
+      }
+      Object uc = upload.invoke(object);
+      if (uc != null) {
+        if (uploadCounter == null) {
+          uploadCounter = new HashSet<>();
+        }
+        uploadCounter.addAll((Set<String>) uc);
+      }
+    } catch (ClassNotFoundException e) {
+      e.printStackTrace();
+    } catch (InstantiationException e) {
+      e.printStackTrace();
+    } catch (IllegalAccessException e) {
+      e.printStackTrace();
+    } catch (NoSuchMethodException e) {
+      e.printStackTrace();
+    } catch (InvocationTargetException e) {
+      e.printStackTrace();
+    }
+  }
+}

+ 31 - 0
Aria/src/main/java/com/arialyy/aria/core/common/QueueMod.java

@@ -0,0 +1,31 @@
+package com.arialyy.aria.core.common;
+
+/**
+ * Created by Aria.Lao on 2017/6/21.
+ * 执行队列类型
+ */
+public enum QueueMod {
+  /**
+   * 等待模式,
+   * 如果执行队列已经满了,再对其它任务(TASK_A)使用start命令执行任务时
+   * 1、TASK_A添加到缓存队列中,当执行队列中的任务完成时,系统会将自动执行缓存队列中的TASK_A
+   * 2、如果再次对TASK_A使用start命令,TASK_A将会立刻执行
+   */
+  WAIT("wait"),
+
+  /**
+   * 立刻执行模式
+   * 如果执行队列已经满了,再次使用start命令执行任务时,该任务会添加到执行队列队尾,而原来执行队列的队首任务会停止
+   */
+  NOW("now");
+
+  public String tag;
+
+  public String getTag() {
+    return tag;
+  }
+
+  QueueMod(String tag) {
+    this.tag = tag;
+  }
+}

+ 45 - 0
Aria/src/main/java/com/arialyy/aria/core/common/RecordWrapper.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import com.arialyy.aria.orm.AbsWrapper;
+import com.arialyy.aria.orm.annotation.Many;
+import com.arialyy.aria.orm.annotation.One;
+import com.arialyy.aria.orm.annotation.Wrapper;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by laoyuyu on 2018/3/30.
+ * 任务记录和线程记录的关系
+ */
+@Wrapper
+public class RecordWrapper extends AbsWrapper {
+
+  @One
+  public TaskRecord taskRecord;
+
+  @Many(parentColumn = "filePath", entityColumn = "key")
+  public List<ThreadRecord> threadRecords;
+
+  @Override protected void handleConvert() {
+    if (threadRecords != null && !threadRecords.isEmpty()) {
+      taskRecord.threadRecords = threadRecords;
+    } else {
+      taskRecord.threadRecords = new ArrayList<>();
+    }
+  }
+}

+ 31 - 0
Aria/src/main/java/com/arialyy/aria/core/common/RequestEnum.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+/**
+ * Created by lyy on 2017/1/23.
+ * url请求方式,目前支持GET、POST
+ */
+public enum RequestEnum {
+  GET("GET"), POST("POST");
+
+  public String name;
+
+  RequestEnum(String name) {
+    this.name = name;
+  }
+
+}

+ 75 - 0
Aria/src/main/java/com/arialyy/aria/core/common/StateConstance.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+/**
+ * Created by lyy on 2017/1/18.
+ * 下载状态常量
+ */
+public class StateConstance {
+  public int CANCEL_NUM = 0;
+  public int STOP_NUM = 0;
+  public int FAIL_NUM = 0;
+  public int COMPLETE_THREAD_NUM = 0;
+  public int START_THREAD_NUM;  //启动的线程数
+  public long CURRENT_LOCATION = 0;
+  public boolean isRunning = false;
+  public boolean isCancel = false;
+  public boolean isStop = false;
+  public TaskRecord TASK_RECORD;
+
+  public StateConstance() {
+  }
+
+  public void resetState() {
+    isCancel = false;
+    isStop = false;
+    isRunning = true;
+    CURRENT_LOCATION = 0;
+    CANCEL_NUM = 0;
+    STOP_NUM = 0;
+    FAIL_NUM = 0;
+  }
+
+  /**
+   * 所有子线程是否都已经停止下载
+   */
+  public boolean isStop() {
+    return STOP_NUM == START_THREAD_NUM;
+  }
+
+  /**
+   * 所有子线程是否都已经下载失败
+   */
+  public boolean isFail() {
+    return COMPLETE_THREAD_NUM != START_THREAD_NUM
+        && FAIL_NUM + COMPLETE_THREAD_NUM >= START_THREAD_NUM;
+  }
+
+  /**
+   * 所有子线程是否都已经完成下载
+   */
+  public boolean isComplete() {
+    return COMPLETE_THREAD_NUM >= START_THREAD_NUM;
+  }
+
+  /**
+   * 所有子线程是否都已经取消下载
+   */
+  public boolean isCancel() {
+    return CANCEL_NUM == START_THREAD_NUM;
+  }
+}

+ 25 - 0
Aria/src/main/java/com/arialyy/aria/core/common/SubThreadConfig.java

@@ -0,0 +1,25 @@
+package com.arialyy.aria.core.common;
+
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import java.io.File;
+
+/**
+ * 子线程下载信息类
+ */
+public class SubThreadConfig<TASK_ENTITY extends AbsTaskEntity> {
+  //线程Id
+  public int THREAD_ID;
+  //下载文件大小
+  public long FILE_SIZE;
+  //子线程启动下载位置
+  public long START_LOCATION;
+  //子线程结束下载位置
+  public long END_LOCATION;
+  //下载文件或上传的文件路径
+  public File TEMP_FILE;
+  //服务器地址
+  public String URL;
+  public TASK_ENTITY TASK_ENTITY;
+  public boolean SUPPORT_BP = true;
+  public ThreadRecord THREAD_RECORD;
+}

+ 82 - 0
Aria/src/main/java/com/arialyy/aria/core/common/TaskRecord.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import com.arialyy.aria.core.download.DownloadGroupEntity;
+import com.arialyy.aria.orm.ActionPolicy;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.orm.annotation.Foreign;
+import com.arialyy.aria.orm.annotation.Ignore;
+import com.arialyy.aria.orm.annotation.NoNull;
+import com.arialyy.aria.orm.annotation.Primary;
+import java.util.List;
+
+/**
+ * Created by laoyuyu on 2018/3/21.
+ * 任务上传或下载的任务记录
+ */
+public class TaskRecord extends DbEntity {
+
+  @Ignore
+  public List<ThreadRecord> threadRecords;
+
+  /**
+   * 任务线程数
+   */
+  public int threadNum;
+
+  /**
+   * 任务文件路径
+   */
+  @Primary
+  public String filePath;
+
+  /**
+   * 文件长度
+   */
+  public long fileLength;
+
+  /**
+   * 任务文件名
+   */
+  @NoNull
+  public String fileName;
+
+  /**
+   * 是否是任务组的子任务记录
+   * {@code true}是
+   */
+  public boolean isGroupRecord = false;
+
+  /**
+   * 下载任务组名
+   */
+  @Foreign(parent = DownloadGroupEntity.class, column = "groupName", onUpdate = ActionPolicy.CASCADE, onDelete = ActionPolicy.CASCADE)
+  public String dGroupName;
+
+  /**
+   * 上传组任务名,暂时没有用
+   */
+  @Ignore
+  @Deprecated
+  public String uGroupName;
+
+  /**
+   * 是否是使用虚拟文件下载的
+   * {@code true}是,{@code false}不是
+   */
+  public boolean isOpenDynamicFile = false;
+}

+ 50 - 0
Aria/src/main/java/com/arialyy/aria/core/common/ThreadRecord.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.common;
+
+import com.arialyy.aria.orm.ActionPolicy;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.orm.annotation.Foreign;
+
+/**
+ * Created by laoyuyu on 2018/5/8.
+ * 任务的线程记录
+ */
+public class ThreadRecord extends DbEntity {
+  @Foreign(parent = TaskRecord.class, column = "filePath", onUpdate = ActionPolicy.CASCADE, onDelete = ActionPolicy.CASCADE)
+  public String key;
+
+  /**
+   * 开始位置
+   */
+  public long startLocation;
+
+  /**
+   * 结束位置
+   */
+  public long endLocation;
+
+  /**
+   * 线程是否完成
+   * {@code true}完成,{@code false}未完成
+   */
+  public boolean isComplete = false;
+
+  /**
+   * 线程id
+   */
+  public int threadId = -1;
+}

+ 66 - 0
Aria/src/main/java/com/arialyy/aria/core/delegate/FtpDelegate.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.delegate;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IFtpTarget;
+import com.arialyy.aria.core.inf.ITarget;
+import com.arialyy.aria.util.ALog;
+
+/**
+ * Created by laoyuyu on 2018/3/9.
+ * ftp 委托
+ */
+public class FtpDelegate<TARGET extends ITarget, ENTITY extends AbsEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>>
+    implements IFtpTarget<TARGET> {
+  private static final String TAG = "FtpDelegate";
+  private ENTITY mEntity;
+  private TASK_ENTITY mTaskEntity;
+  private TARGET mTarget;
+
+  public FtpDelegate(TARGET target, TASK_ENTITY taskEntity) {
+    mTarget = target;
+    mTaskEntity = taskEntity;
+    mEntity = mTaskEntity.getEntity();
+  }
+
+  @Override public TARGET charSet(String charSet) {
+    if (TextUtils.isEmpty(charSet)) return mTarget;
+    mTaskEntity.setCharSet(charSet);
+    return mTarget;
+  }
+
+  @Override public TARGET login(String userName, String password) {
+    return login(userName, password, null);
+  }
+
+  @Override public TARGET login(String userName, String password, String account) {
+    if (TextUtils.isEmpty(userName)) {
+      ALog.e(TAG, "用户名不能为null");
+      return mTarget;
+    } else if (TextUtils.isEmpty(password)) {
+      ALog.e(TAG, "密码不能为null");
+      return mTarget;
+    }
+    mTaskEntity.getUrlEntity().needLogin = true;
+    mTaskEntity.getUrlEntity().user = userName;
+    mTaskEntity.getUrlEntity().password = password;
+    mTaskEntity.getUrlEntity().account = account;
+    return mTarget;
+  }
+}

+ 139 - 0
Aria/src/main/java/com/arialyy/aria/core/delegate/HttpHeaderDelegate.java

@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.delegate;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import com.arialyy.aria.core.common.RequestEnum;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IHttpHeaderTarget;
+import com.arialyy.aria.core.inf.ITarget;
+import com.arialyy.aria.util.ALog;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Created by laoyuyu on 2018/3/9.
+ * HTTP header参数设置委托类
+ */
+public class HttpHeaderDelegate<TARGET extends ITarget, ENTITY extends AbsEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>>
+    implements IHttpHeaderTarget<TARGET> {
+  private static final String TAG = "HttpHeaderDelegate";
+  private ENTITY mEntity;
+  private TASK_ENTITY mTaskEntity;
+  private TARGET mTarget;
+
+  public HttpHeaderDelegate(TARGET target, TASK_ENTITY taskEntity) {
+    mTarget = target;
+    mTaskEntity = taskEntity;
+
+    mEntity = mTaskEntity.getEntity();
+  }
+
+  /**
+   * 给url请求添加Header数据
+   * 如果新的header数据和数据保存的不一致,则更新数据库中对应的header数据
+   *
+   * @param key header对应的key
+   * @param value header对应的value
+   */
+  @Override
+  public TARGET addHeader(@NonNull String key, @NonNull String value) {
+    if (TextUtils.isEmpty(key)) {
+      ALog.w(TAG, "设置header失败,header对应的key不能为null");
+      return mTarget;
+    } else if (TextUtils.isEmpty(value)) {
+      ALog.w(TAG, "设置header失败,header对应的value不能为null");
+      return mTarget;
+    }
+    if (mTaskEntity.getHeaders().get(key) == null) {
+      mTaskEntity.getHeaders().put(key, value);
+    } else if (!mTaskEntity.getHeaders().get(key).equals(value)) {
+      mTaskEntity.getHeaders().put(key, value);
+    }
+    return mTarget;
+  }
+
+  /**
+   * 给url请求添加一组header数据
+   * 如果新的header数据和数据保存的不一致,则更新数据库中对应的header数据
+   *
+   * @param headers 一组http header数据
+   */
+  @Override
+  public TARGET addHeaders(@NonNull Map<String, String> headers) {
+    if (headers.size() == 0) {
+      ALog.w(TAG, "设置header失败,map没有header数据");
+      return mTarget;
+    }
+    /*
+      两个map比较逻辑
+      1、比对key是否相同
+      2、如果key相同,比对value是否相同
+      3、只有当上面两个步骤中key 和 value都相同时才能任务两个map数据一致
+     */
+    boolean mapEquals = false;
+    if (mTaskEntity.getHeaders().size() == headers.size()) {
+      int i = 0;
+      Set<String> keys = mTaskEntity.getHeaders().keySet();
+      for (String key : keys) {
+        if (headers.containsKey(key)) {
+          i++;
+        } else {
+          break;
+        }
+      }
+      if (i == mTaskEntity.getHeaders().size()) {
+        int j = 0;
+        Collection<String> values = mTaskEntity.getHeaders().values();
+        for (String value : values) {
+          if (headers.containsValue(value)) {
+            j++;
+          } else {
+            break;
+          }
+        }
+        if (j == mTaskEntity.getHeaders().size()) {
+          mapEquals = true;
+        }
+      }
+    }
+
+    if (!mapEquals) {
+      mTaskEntity.getHeaders().clear();
+      Set<String> keys = headers.keySet();
+      for (String key : keys) {
+        mTaskEntity.getHeaders().put(key, headers.get(key));
+      }
+    }
+
+    return mTarget;
+  }
+
+  /**
+   * 设置请求类型,POST或GET,默认为在GET
+   * 只试用于HTTP请求
+   *
+   * @param requestEnum {@link RequestEnum}
+   */
+  @Override
+  public TARGET setRequestMode(RequestEnum requestEnum) {
+    mTaskEntity.setRequestEnum(requestEnum);
+    return mTarget;
+  }
+}

+ 97 - 0
Aria/src/main/java/com/arialyy/aria/core/download/AbsDownloadTarget.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.command.normal.NormalCmdFactory;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsTarget;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.util.CommonUtil;
+
+/**
+ * Created by lyy on 2017/2/28.
+ */
+abstract class AbsDownloadTarget<TARGET extends AbsTarget, ENTITY extends AbsEntity, TASK_ENTITY extends AbsTaskEntity>
+    extends AbsTarget<TARGET, ENTITY, TASK_ENTITY> {
+
+  static final int HTTP = 1;
+  static final int FTP = 2;
+  //HTTP任务组
+  static final int GROUP_HTTP = 3;
+  //FTP文件夹
+  static final int GROUP_FTP_DIR = 4;
+
+  /**
+   * 设置的文件保存路径的临时变量
+   */
+  String mTempFilePath;
+
+  /**
+   * 将任务设置为最高优先级任务,最高优先级任务有以下特点:
+   * 1、在下载队列中,有且只有一个最高优先级任务
+   * 2、最高优先级任务会一直存在,直到用户手动暂停或任务完成
+   * 3、任务调度器不会暂停最高优先级任务
+   * 4、用户手动暂停或任务完成后,第二次重新执行该任务,该命令将失效
+   * 5、如果下载队列中已经满了,则会停止队尾的任务,当高优先级任务完成后,该队尾任务将自动执行
+   * 6、把任务设置为最高优先级任务后,将自动执行任务,不需要重新调用start()启动任务
+   */
+  protected void setHighestPriority() {
+    if (checkEntity()) {
+      AriaManager.getInstance(AriaManager.APP)
+          .setCmd(CommonUtil.createNormalCmd(mTargetName, mTaskEntity,
+              NormalCmdFactory.TASK_HIGHEST_PRIORITY, checkTaskType()))
+          .exe();
+    }
+  }
+
+  /**
+   * 添加任务
+   */
+  public void add() {
+    if (checkEntity()) {
+      AriaManager.getInstance(AriaManager.APP)
+          .setCmd(CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_CREATE,
+              checkTaskType()))
+          .exe();
+    }
+  }
+
+  /**
+   * 获取任务文件大小
+   *
+   * @return 文件大小
+   */
+  public long getFileSize() {
+    return getSize();
+  }
+
+  /**
+   * 获取单位转换后的文件大小
+   *
+   * @return 文件大小{@code xxx mb}
+   */
+  public String getConvertFileSize() {
+    return getConvertSize();
+  }
+
+  /**
+   * 设置target类型
+   *
+   * @return {@link #HTTP}、{@link #FTP}、{@link #GROUP_HTTP}、{@link #GROUP_FTP_DIR}
+   */
+  protected abstract int getTargetType();
+}

+ 180 - 0
Aria/src/main/java/com/arialyy/aria/core/download/BaseDListener.java

@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Handler;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.TaskRecord;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsGroupEntity;
+import com.arialyy.aria.core.inf.AbsNormalEntity;
+import com.arialyy.aria.core.inf.AbsTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.core.scheduler.ISchedulers;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.util.CommonUtil;
+import java.lang.ref.WeakReference;
+
+/**
+ * 下载监听类
+ */
+class BaseDListener<ENTITY extends AbsEntity, TASK_ENTITY extends AbsTaskEntity<ENTITY>, TASK extends AbsTask<TASK_ENTITY>>
+    implements IDownloadListener {
+  private static final String TAG = "BaseDListener";
+  protected WeakReference<Handler> outHandler;
+  private int RUN_SAVE_INTERVAL = 5 * 1000;  //5s保存一次下载中的进度
+  private long mLastLen = 0;   //上一次发送长度
+  private boolean isFirst = true;
+  protected ENTITY mEntity;
+  protected TASK_ENTITY mTaskEntity;
+  protected TASK mTask;
+  private boolean isConvertSpeed = false;
+  boolean isWait = false;
+  private long mLastSaveTime;
+  private long mUpdateInterval;
+
+  BaseDListener(TASK task, Handler outHandler) {
+    this.outHandler = new WeakReference<>(outHandler);
+    this.mTask = new WeakReference<>(task).get();
+    this.mEntity = mTask.getTaskEntity().getEntity();
+    this.mTaskEntity = mTask.getTaskEntity();
+    final AriaManager manager = AriaManager.getInstance(AriaManager.APP);
+    isConvertSpeed = manager.getDownloadConfig().isConvertSpeed();
+    mLastLen = mEntity.getCurrentProgress();
+    mLastSaveTime = System.currentTimeMillis();
+    mUpdateInterval = manager.getDownloadConfig().getUpdateInterval();
+  }
+
+  @Override public void onPre() {
+    saveData(IEntity.STATE_PRE, -1);
+    sendInState2Target(ISchedulers.PRE);
+  }
+
+  @Override public void onPostPre(long fileSize) {
+    mEntity.setFileSize(fileSize);
+    mEntity.setConvertFileSize(CommonUtil.formatFileSize(fileSize));
+    saveData(IEntity.STATE_POST_PRE, -1);
+    sendInState2Target(ISchedulers.POST_PRE);
+  }
+
+  @Override public void supportBreakpoint(boolean support) {
+
+  }
+
+  @Override public void onStart(long startLocation) {
+    saveData(IEntity.STATE_RUNNING, startLocation);
+    sendInState2Target(ISchedulers.START);
+  }
+
+  @Override public void onResume(long resumeLocation) {
+    saveData(IEntity.STATE_RUNNING, resumeLocation);
+    sendInState2Target(ISchedulers.RESUME);
+  }
+
+  @Override public void onProgress(long currentLocation) {
+    mEntity.setCurrentProgress(currentLocation);
+    long speed = currentLocation - mLastLen;
+    if (isFirst) {
+      speed = 0;
+      isFirst = false;
+    }
+    handleSpeed(speed);
+    sendInState2Target(ISchedulers.RUNNING);
+    if (System.currentTimeMillis() - mLastSaveTime >= RUN_SAVE_INTERVAL) {
+      saveData(IEntity.STATE_RUNNING, currentLocation);
+      mLastSaveTime = System.currentTimeMillis();
+    }
+
+    mLastLen = currentLocation;
+  }
+
+  @Override public void onStop(long stopLocation) {
+    saveData(isWait ? IEntity.STATE_WAIT : IEntity.STATE_STOP, stopLocation);
+    handleSpeed(0);
+    sendInState2Target(ISchedulers.STOP);
+  }
+
+  @Override public void onCancel() {
+    saveData(IEntity.STATE_CANCEL, -1);
+    handleSpeed(0);
+    sendInState2Target(ISchedulers.CANCEL);
+  }
+
+  @Override public void onComplete() {
+    saveData(IEntity.STATE_COMPLETE, mEntity.getFileSize());
+    handleSpeed(0);
+    sendInState2Target(ISchedulers.COMPLETE);
+  }
+
+  @Override public void onFail(boolean needRetry) {
+    mEntity.setFailNum(mEntity.getFailNum() + 1);
+    saveData(IEntity.STATE_FAIL, mEntity.getCurrentProgress());
+    handleSpeed(0);
+    mTask.needRetry = needRetry;
+    sendInState2Target(ISchedulers.FAIL);
+  }
+
+  private void handleSpeed(long speed) {
+    if (mUpdateInterval != 1000) {
+      speed = speed * 1000 / mUpdateInterval;
+    }
+    if (isConvertSpeed) {
+      mEntity.setConvertSpeed(CommonUtil.formatFileSize(speed < 0 ? 0 : speed) + "/s");
+    }
+    mEntity.setSpeed(speed < 0 ? 0 : speed);
+
+    mEntity.setPercent((int) (mEntity.getFileSize() <= 0 ? 0
+        : mEntity.getCurrentProgress() * 100 / mEntity.getFileSize()));
+  }
+
+  /**
+   * 将任务状态发送给下载器
+   *
+   * @param state {@link ISchedulers#START}
+   */
+  private void sendInState2Target(int state) {
+    if (outHandler.get() != null) {
+      outHandler.get().obtainMessage(state, mTask).sendToTarget();
+    }
+  }
+
+  private void saveData(int state, long location) {
+    mTaskEntity.setState(state);
+    mEntity.setState(state);
+    mEntity.setComplete(state == IEntity.STATE_COMPLETE);
+    if (state == IEntity.STATE_CANCEL) {
+      if (mEntity instanceof AbsNormalEntity) {
+        TaskRecord record =
+            DbEntity.findFirst(TaskRecord.class, "TaskRecord.filePath=?", mTaskEntity.getKey());
+        if (record != null) {
+          CommonUtil.delTaskRecord(record, mTaskEntity.isRemoveFile(), (AbsNormalEntity) mEntity);
+        }
+      } else if (mEntity instanceof AbsGroupEntity) {
+        CommonUtil.delGroupTaskRecord(mTaskEntity.isRemoveFile(), ((AbsGroupEntity) mEntity));
+      }
+      //mEntity.deleteData();
+      return;
+    } else if (mEntity.isComplete()) {
+      mEntity.setCompleteTime(System.currentTimeMillis());
+      mEntity.setCurrentProgress(mEntity.getFileSize());
+    } else if (location > 0) {
+      mEntity.setCurrentProgress(location);
+    }
+    mTaskEntity.update();
+  }
+}

+ 165 - 0
Aria/src/main/java/com/arialyy/aria/core/download/BaseGroupTarget.java

@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.text.TextUtils;
+import android.util.Log;
+import com.arialyy.aria.core.manager.SubTaskManager;
+import com.arialyy.aria.core.queue.DownloadGroupTaskQueue;
+import com.arialyy.aria.util.ALog;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Created by Aria.Lao on 2017/7/26.
+ */
+abstract class BaseGroupTarget<TARGET extends BaseGroupTarget>
+    extends AbsDownloadTarget<TARGET, DownloadGroupEntity, DownloadGroupTaskEntity> {
+
+  /**
+   * 组任务名
+   */
+  String mGroupName;
+  /**
+   * 文件夹临时路径
+   */
+  String mDirPathTemp;
+  /**
+   * 是否需要修改路径
+   */
+  boolean needModifyPath = false;
+
+  private SubTaskManager mSubTaskManager;
+
+  /**
+   * 获取子任务管理器
+   *
+   * @return 子任务管理器
+   */
+  public SubTaskManager getSubTaskManager() {
+    if (mSubTaskManager == null) {
+      mSubTaskManager = new SubTaskManager(mTargetName, mTaskEntity);
+    }
+    return mSubTaskManager;
+  }
+
+  /**
+   * 设置任务组别名
+   */
+  public TARGET setGroupAlias(String alias) {
+    if (TextUtils.isEmpty(alias)) return (TARGET) this;
+    mEntity.setAlias(alias);
+    return (TARGET) this;
+  }
+
+  @Override public boolean taskExists() {
+    return DownloadGroupTaskQueue.getInstance().getTask(mEntity.getGroupName()) != null;
+  }
+
+  /**
+   * 设置任务组的文件夹路径,该api后续会删除
+   *
+   * @param groupDirPath 任务组保存文件夹路径
+   * @deprecated {@link #setDirPath(String)} 请使用这个api
+   */
+  @Deprecated
+  public TARGET setDownloadDirPath(String groupDirPath) {
+    return setDirPath(groupDirPath);
+  }
+
+  /**
+   * 设置任务组的文件夹路径,在Aria中,任务组的所有子任务都会下载到以任务组组名的文件夹中。
+   * 如:groupDirPath = "/mnt/sdcard/download/group_test"
+   * <pre>
+   *   {@code
+   *      + mnt
+   *        + sdcard
+   *          + download
+   *            + group_test
+   *              - task1.apk
+   *              - task2.apk
+   *              - task3.apk
+   *              ....
+   *
+   *   }
+   * </pre>
+   *
+   * @param dirPath 任务组保存文件夹路径
+   */
+  public TARGET setDirPath(String dirPath) {
+    mDirPathTemp = dirPath;
+    return (TARGET) this;
+  }
+
+  @Override public boolean isRunning() {
+    DownloadGroupTask task = DownloadGroupTaskQueue.getInstance().getTask(mEntity.getKey());
+    return task != null && task.isRunning();
+  }
+
+  /**
+   * 改变任务组文件夹路径,修改文件夹路径会将子任务所有路径更换
+   *
+   * @param newDirPath 新的文件夹路径
+   */
+  void reChangeDirPath(String newDirPath) {
+    List<DownloadTaskEntity> subTasks = mTaskEntity.getSubTaskEntities();
+    if (subTasks != null && !subTasks.isEmpty()) {
+      for (DownloadTaskEntity dte : subTasks) {
+        DownloadEntity de = dte.getEntity();
+        String oldPath = de.getDownloadPath();
+        String newPath = newDirPath + "/" + de.getFileName();
+        File file = new File(oldPath);
+        if (file.exists()) {
+          file.renameTo(new File(newPath));
+        }
+        de.setDownloadPath(newPath);
+        dte.setKey(newPath);
+        de.save();
+        dte.save();
+      }
+    }
+  }
+
+  /**
+   * 检查并设置文件夹路径
+   *
+   * @return {@code true} 合法
+   */
+  boolean checkDirPath() {
+    if (TextUtils.isEmpty(mDirPathTemp)) {
+      ALog.e(TAG, "文件夹路径不能为null");
+      return false;
+    } else if (!mDirPathTemp.startsWith("/")) {
+      ALog.e(TAG, "文件夹路径【" + mDirPathTemp + "】错误");
+      return false;
+    }
+    File file = new File(mDirPathTemp);
+    if (file.isFile()) {
+      ALog.e(TAG, "路径【" + mDirPathTemp + "】是文件,请设置文件夹路径");
+      return false;
+    }
+
+    if (TextUtils.isEmpty(mEntity.getDirPath()) || !mEntity.getDirPath().equals(mDirPathTemp)) {
+      if (!file.exists()) {
+        file.mkdirs();
+      }
+      needModifyPath = true;
+      mEntity.setDirPath(mDirPathTemp);
+    }
+
+    return true;
+  }
+}

+ 182 - 0
Aria/src/main/java/com/arialyy/aria/core/download/BaseNormalTarget.java

@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.manager.TEManager;
+import com.arialyy.aria.core.queue.DownloadTaskQueue;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import java.io.File;
+
+/**
+ * Created by Aria.Lao on 2017/7/26.
+ */
+abstract class BaseNormalTarget<TARGET extends BaseNormalTarget>
+    extends AbsDownloadTarget<TARGET, DownloadEntity, DownloadTaskEntity> {
+
+  /**
+   * 资源地址
+   */
+  protected String url;
+
+  /**
+   * 通过地址初始化target
+   */
+  void initTarget(String url, String targetName, boolean refreshInfo) {
+    this.url = url;
+    mTargetName = targetName;
+    mTaskEntity = TEManager.getInstance().getTEntity(DownloadTaskEntity.class, url);
+    mEntity = mTaskEntity.getEntity();
+    mTaskEntity.setRefreshInfo(refreshInfo);
+    if (mEntity != null) {
+      mTempFilePath = mEntity.getDownloadPath();
+    }
+  }
+
+  /**
+   * 将任务设置为最高优先级任务,最高优先级任务有以下特点:
+   * 1、在下载队列中,有且只有一个最高优先级任务
+   * 2、最高优先级任务会一直存在,直到用户手动暂停或任务完成
+   * 3、任务调度器不会暂停最高优先级任务
+   * 4、用户手动暂停或任务完成后,第二次重新执行该任务,该命令将失效
+   * 5、如果下载队列中已经满了,则会停止队尾的任务,当高优先级任务完成后,该队尾任务将自动执行
+   * 6、把任务设置为最高优先级任务后,将自动执行任务,不需要重新调用start()启动任务
+   */
+  @Override public void setHighestPriority() {
+    super.setHighestPriority();
+  }
+
+  /**
+   * 下载任务是否存在
+   *
+   * @return {@code true}任务存在
+   */
+  @Override public boolean taskExists() {
+    return DownloadTaskQueue.getInstance().getTask(mEntity.getUrl()) != null;
+  }
+
+  /**
+   * 获取下载实体
+   */
+  public DownloadEntity getDownloadEntity() {
+    return mEntity;
+  }
+
+  /**
+   * 是否在下载,该api后续版本会删除
+   *
+   * @deprecated {@link #isRunning()}
+   */
+  @Deprecated public boolean isDownloading() {
+    return isRunning();
+  }
+
+  /**
+   * 是否在下载
+   *
+   * @return {@code true}任务正在下载
+   */
+  @Override public boolean isRunning() {
+    DownloadTask task = DownloadTaskQueue.getInstance().getTask(mEntity.getKey());
+    return task != null && task.isRunning();
+  }
+
+  /**
+   * 检查下载实体,判断实体是否合法
+   * 合法标准为:
+   * 1、下载路径不为null,并且下载路径是正常的http或ftp路径
+   * 2、保存路径不为null,并且保存路径是android文件系统路径
+   * 3、保存路径不能重复
+   *
+   * @return {@code true}合法
+   */
+  @Override protected boolean checkEntity() {
+    boolean b = getTargetType() < GROUP_HTTP && checkUrl() && checkFilePath();
+    if (b) {
+      mEntity.save();
+      mTaskEntity.save();
+    }
+    return b;
+  }
+
+  /**
+   * 检查并设置普通任务的文件保存路径
+   *
+   * @return {@code true}保存路径合法
+   */
+  private boolean checkFilePath() {
+    String filePath = mTempFilePath;
+    if (TextUtils.isEmpty(filePath)) {
+      ALog.e(TAG, "下载失败,文件保存路径为null");
+      return false;
+    } else if (!filePath.startsWith("/")) {
+      ALog.e(TAG, "下载失败,文件保存路径【" + filePath + "】错误");
+      return false;
+    }
+    File file = new File(filePath);
+    if (file.isDirectory()) {
+      if (getTargetType() == HTTP) {
+        ALog.e(TAG, "下载失败,保存路径【" + filePath + "】不能为文件夹,路径需要是完整的文件路径,如:/mnt/sdcard/game.zip");
+        return false;
+      } else if (getTargetType() == FTP) {
+        filePath += mEntity.getFileName();
+      }
+    }
+    mEntity.setFileName(file.getName());
+
+    //设置文件保存路径,如果新文件路径和就文件路径不同,则修改路径
+    if (!filePath.equals(mEntity.getDownloadPath())) {
+      if (DbEntity.checkDataExist(DownloadEntity.class, "downloadPath=?", filePath)) {
+        ALog.e(TAG, "下载失败,保存路径【" + filePath + "】已经被其它任务占用,请设置其它保存路径");
+        return false;
+      }
+      File oldFile = new File(mEntity.getDownloadPath());
+      File newFile = new File(filePath);
+      mEntity.setDownloadPath(filePath);
+      mEntity.setFileName(newFile.getName());
+      mTaskEntity.setKey(filePath);
+      if (oldFile.exists()) {
+        oldFile.renameTo(newFile);
+        CommonUtil.modifyTaskRecord(oldFile.getPath(), newFile.getPath());
+      }
+    }
+    return true;
+  }
+
+  /**
+   * 检查普通任务的下载地址
+   *
+   * @return {@code true}地址合法
+   */
+  private boolean checkUrl() {
+    final String url = mEntity.getUrl();
+    if (TextUtils.isEmpty(url)) {
+      ALog.e(TAG, "下载失败,url为null");
+      return false;
+    } else if (!url.startsWith("http") && !url.startsWith("ftp")) {
+      ALog.e(TAG, "下载失败,url【" + url + "】错误");
+      return false;
+    }
+    int index = url.indexOf("://");
+    if (index == -1) {
+      ALog.e(TAG, "下载失败,url【" + url + "】不合法");
+      return false;
+    }
+    return true;
+  }
+}

+ 166 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadEntity.java

@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import com.arialyy.aria.core.inf.AbsNormalEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.orm.ActionPolicy;
+import com.arialyy.aria.orm.annotation.Foreign;
+import com.arialyy.aria.orm.annotation.Primary;
+import com.arialyy.aria.util.CommonUtil;
+
+/**
+ * Created by lyy on 2015/12/25.
+ * 下载实体
+ */
+public class DownloadEntity extends AbsNormalEntity implements Parcelable {
+  @Primary private String downloadPath; //保存路径
+
+  /**
+   * 所属任务组
+   */
+  @Foreign(parent = DownloadGroupEntity.class, column = "groupName",
+      onUpdate = ActionPolicy.CASCADE, onDelete = ActionPolicy.CASCADE)
+  private String groupName;
+
+  /**
+   * 从服务器的返回信息中获取的文件md5信息,如果服务器没有返回,则不会设置该信息
+   * 如果你已经设置了该任务的MD5信息,Aria也不会从服务器返回的信息中获取该信息
+   */
+  private String md5Code;
+
+  /**
+   * 从服务器的返回信息中获取的文件描述信息
+   */
+  private String disposition;
+
+  /**
+   * 从disposition获取到的文件名,如果可以获取到,则会赋值到这个字段
+   */
+  private String serverFileName;
+
+  @Override public String getKey() {
+    return getUrl();
+  }
+
+  @Override public int getTaskType() {
+    return getUrl().startsWith("ftp") ? AbsTaskEntity.D_FTP : AbsTaskEntity.D_HTTP;
+  }
+
+  public DownloadEntity() {
+  }
+
+  public String getMd5Code() {
+    return md5Code;
+  }
+
+  public void setMd5Code(String md5Code) {
+    this.md5Code = md5Code;
+  }
+
+  public String getDisposition() {
+    return TextUtils.isEmpty(disposition) ? "" : CommonUtil.decryptBASE64(disposition);
+  }
+
+  public void setDisposition(String disposition) {
+    this.disposition = disposition;
+  }
+
+  public String getServerFileName() {
+    return serverFileName;
+  }
+
+  public void setServerFileName(String serverFileName) {
+    this.serverFileName = serverFileName;
+  }
+
+  public String getGroupName() {
+    return groupName;
+  }
+
+  public void setGroupName(String groupName) {
+    this.groupName = groupName;
+  }
+
+  public String getDownloadPath() {
+    return downloadPath;
+  }
+
+  public DownloadEntity setDownloadPath(String downloadPath) {
+    this.downloadPath = downloadPath;
+    return this;
+  }
+
+  @Override public DownloadEntity clone() throws CloneNotSupportedException {
+    return (DownloadEntity) super.clone();
+  }
+
+  @Override public int describeContents() {
+    return 0;
+  }
+
+  @Override public void writeToParcel(Parcel dest, int flags) {
+    super.writeToParcel(dest, flags);
+    dest.writeString(this.downloadPath);
+    dest.writeString(this.groupName);
+    dest.writeString(this.md5Code);
+    dest.writeString(this.disposition);
+    dest.writeString(this.serverFileName);
+  }
+
+  @Override public String toString() {
+    return "DownloadEntity{"
+        + "downloadPath='"
+        + downloadPath
+        + '\''
+        + ", groupName='"
+        + groupName
+        + '\''
+        + ", md5Code='"
+        + md5Code
+        + '\''
+        + ", disposition='"
+        + disposition
+        + '\''
+        + ", serverFileName='"
+        + serverFileName
+        + '\''
+        + '}';
+  }
+
+  protected DownloadEntity(Parcel in) {
+    super(in);
+    this.downloadPath = in.readString();
+    this.groupName = in.readString();
+    this.md5Code = in.readString();
+    this.disposition = in.readString();
+    this.serverFileName = in.readString();
+  }
+
+  public static final Creator<DownloadEntity> CREATOR = new Creator<DownloadEntity>() {
+    @Override public DownloadEntity createFromParcel(Parcel source) {
+      return new DownloadEntity(source);
+    }
+
+    @Override public DownloadEntity[] newArray(int size) {
+      return new DownloadEntity[size];
+    }
+  };
+}

+ 79 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupEntity.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Parcel;
+import com.arialyy.aria.core.inf.AbsGroupEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.orm.annotation.Ignore;
+import java.util.List;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 下载任务组实体
+ */
+public class DownloadGroupEntity extends AbsGroupEntity {
+
+  @Ignore private List<DownloadEntity> subEntities;
+
+
+
+  /**
+   * 子任务实体列表
+   */
+  public List<DownloadEntity> getSubEntities() {
+    return subEntities;
+  }
+
+  public void setSubEntities(List<DownloadEntity> subTasks) {
+    this.subEntities = subTasks;
+  }
+
+  public void setGroupName(String key) {
+    this.groupName = key;
+  }
+
+  @Override public int getTaskType() {
+    return getKey().startsWith("ftp") ? AbsTaskEntity.D_FTP_DIR : AbsTaskEntity.DG_HTTP;
+  }
+
+  public DownloadGroupEntity() {
+  }
+
+  @Override public int describeContents() {
+    return 0;
+  }
+
+  @Override public void writeToParcel(Parcel dest, int flags) {
+    super.writeToParcel(dest, flags);
+    dest.writeTypedList(this.subEntities);
+  }
+
+  protected DownloadGroupEntity(Parcel in) {
+    super(in);
+    this.subEntities = in.createTypedArrayList(DownloadEntity.CREATOR);
+  }
+
+  public static final Creator<DownloadGroupEntity> CREATOR = new Creator<DownloadGroupEntity>() {
+    @Override public DownloadGroupEntity createFromParcel(Parcel source) {
+      return new DownloadGroupEntity(source);
+    }
+
+    @Override public DownloadGroupEntity[] newArray(int size) {
+      return new DownloadGroupEntity[size];
+    }
+  };
+}

+ 95 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupListener.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Handler;
+import com.arialyy.aria.core.download.downloader.IDownloadGroupListener;
+import com.arialyy.aria.core.inf.GroupSendParams;
+import com.arialyy.aria.core.scheduler.ISchedulers;
+
+/**
+ * Created by Aria.Lao on 2017/7/20.
+ * 任务组下载事件
+ */
+class DownloadGroupListener
+    extends BaseDListener<DownloadGroupEntity, DownloadGroupTaskEntity, DownloadGroupTask>
+    implements IDownloadGroupListener {
+  private final String TAG = "DownloadGroupListener";
+  private GroupSendParams<DownloadGroupTask, DownloadEntity> mSeedEntity;
+
+  DownloadGroupListener(DownloadGroupTask task, Handler outHandler) {
+    super(task, outHandler);
+    mSeedEntity = new GroupSendParams<>();
+    mSeedEntity.groupTask = task;
+  }
+
+  @Override public void onSubPre(DownloadEntity subEntity) {
+    sendInState2Target(ISchedulers.SUB_PRE, subEntity);
+  }
+
+  @Override public void supportBreakpoint(boolean support, DownloadEntity subEntity) {
+
+  }
+
+  @Override public void onSubStart(DownloadEntity subEntity) {
+    sendInState2Target(ISchedulers.SUB_START, subEntity);
+  }
+
+  @Override public void onSubStop(DownloadEntity subEntity) {
+    saveCurrentLocation();
+    sendInState2Target(ISchedulers.SUB_STOP, subEntity);
+  }
+
+  @Override public void onSubComplete(DownloadEntity subEntity) {
+    saveCurrentLocation();
+    sendInState2Target(ISchedulers.SUB_COMPLETE, subEntity);
+  }
+
+  @Override public void onSubFail(DownloadEntity subEntity) {
+    saveCurrentLocation();
+    sendInState2Target(ISchedulers.SUB_FAIL, subEntity);
+  }
+
+  @Override public void onSubCancel(DownloadEntity subEntity) {
+    saveCurrentLocation();
+    sendInState2Target(ISchedulers.SUB_CANCEL, subEntity);
+  }
+
+  @Override public void onSubRunning(DownloadEntity subEntity) {
+    sendInState2Target(ISchedulers.SUB_RUNNING, subEntity);
+  }
+
+  /**
+   * 将任务状态发送给下载器
+   *
+   * @param state {@link ISchedulers#START}
+   */
+  private void sendInState2Target(int state, DownloadEntity subEntity) {
+    if (outHandler.get() != null) {
+      mSeedEntity.entity = subEntity;
+      outHandler.get().obtainMessage(state, ISchedulers.IS_SUB_TASK, 0, mSeedEntity).sendToTarget();
+    }
+  }
+
+  private void saveCurrentLocation() {
+    long location = 0;
+    for (DownloadEntity e : mEntity.getSubEntities()) {
+      location += e.getCurrentProgress();
+    }
+    mEntity.setCurrentProgress(location);
+    mEntity.update();
+  }
+}

+ 251 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupTarget.java

@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.manager.TEManager;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 下载任务组
+ */
+public class DownloadGroupTarget extends BaseGroupTarget<DownloadGroupTarget> {
+  /**
+   * 子任务下载地址,
+   */
+  private List<String> mUrls = new ArrayList<>();
+
+  /**
+   * 子任务文件名
+   */
+  private List<String> mSubNameTemp = new ArrayList<>();
+
+  DownloadGroupTarget(DownloadGroupEntity groupEntity, String targetName) {
+    this.mTargetName = targetName;
+    if (groupEntity.getUrls() != null && !groupEntity.getUrls().isEmpty()) {
+      this.mUrls.addAll(groupEntity.getUrls());
+    }
+    init();
+  }
+
+  DownloadGroupTarget(List<String> urls, String targetName) {
+    this.mTargetName = targetName;
+    this.mUrls = urls;
+    init();
+  }
+
+  private void init() {
+    mGroupName = CommonUtil.getMd5Code(mUrls);
+    mTaskEntity = TEManager.getInstance().getGTEntity(DownloadGroupTaskEntity.class, mUrls);
+    mEntity = mTaskEntity.getEntity();
+
+    if (mEntity != null) {
+      mDirPathTemp = mEntity.getDirPath();
+    }
+  }
+
+  /**
+   * 任务组总任务大小,任务组是一个抽象的概念,没有真实的数据实体,任务组的大小是Aria动态获取子任务大小相加而得到的,
+   * 如果你知道当前任务组总大小,你也可以调用该方法给任务组设置大小
+   *
+   * 为了更好的用户体验,建议直接设置任务组文件大小
+   *
+   * @param fileSize 任务组总大小
+   */
+  public DownloadGroupTarget setFileSize(long fileSize) {
+    if (fileSize <= 0) {
+      ALog.e(TAG, "文件大小不能小于 0");
+      return this;
+    }
+    if (mEntity.getFileSize() <= 1 || mEntity.getFileSize() != fileSize) {
+      mEntity.setFileSize(fileSize);
+    }
+    return this;
+  }
+
+  /**
+   * 如果你是使用{@link DownloadReceiver#load(DownloadGroupEntity)}进行下载操作,那么你需要设置任务组的下载地址
+   */
+  public DownloadGroupTarget setGroupUrl(List<String> urls) {
+    mUrls.clear();
+    mUrls.addAll(urls);
+    return this;
+  }
+
+  /**
+   * 设置子任务文件名,该方法必须在{@link #setDirPath(String)}之后调用,否则不生效
+   *
+   * @deprecated {@link #setSubFileName(List)} 请使用该api
+   */
+  @Deprecated public DownloadGroupTarget setSubTaskFileName(List<String> subTaskFileName) {
+    return setSubFileName(subTaskFileName);
+  }
+
+  /**
+   * 设置子任务文件名,该方法必须在{@link #setDirPath(String)}之后调用,否则不生效
+   */
+  public DownloadGroupTarget setSubFileName(List<String> subTaskFileName) {
+    if (subTaskFileName == null || subTaskFileName.isEmpty()) {
+      ALog.e(TAG, "修改子任务的文件名失败:列表为null");
+      return this;
+    }
+    if (subTaskFileName.size() != mTaskEntity.getSubTaskEntities().size()) {
+      ALog.e(TAG, "修改子任务的文件名失败:子任务文件名列表数量和子任务的数量不匹配");
+      return this;
+    }
+    mSubNameTemp.clear();
+    mSubNameTemp.addAll(subTaskFileName);
+    return this;
+  }
+
+  @Override protected int getTargetType() {
+    return GROUP_HTTP;
+  }
+
+  @Override protected boolean checkEntity() {
+    if (getTargetType() == GROUP_HTTP) {
+      if (!checkDirPath()) {
+        return false;
+      }
+
+      if (!checkSubName()) {
+        return false;
+      }
+
+      if (!checkUrls()) {
+        return false;
+      }
+
+      mEntity.save();
+      mTaskEntity.save();
+
+      if (needModifyPath) {
+        reChangeDirPath(mDirPathTemp);
+      }
+
+      if (!mSubNameTemp.isEmpty()) {
+        updateSingleSubFileName();
+      }
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * 更新所有改动的子任务文件名
+   */
+  private void updateSingleSubFileName() {
+    List<DownloadTaskEntity> entities = mTaskEntity.getSubTaskEntities();
+    int i = 0;
+    for (DownloadTaskEntity entity : entities) {
+      if (i < mSubNameTemp.size()) {
+        String newName = mSubNameTemp.get(i);
+        updateSingleSubFileName(entity, newName);
+      }
+      i++;
+    }
+  }
+
+  /**
+   * 检查urls是否合法,并删除不合法的子任务
+   *
+   * @return {@code true} 合法
+   */
+  private boolean checkUrls() {
+    if (mUrls.isEmpty()) {
+      ALog.e(TAG, "下载失败,子任务下载列表为null");
+      return false;
+    }
+    Set<Integer> delItem = new HashSet<>();
+
+    int i = 0;
+    for (String url : mUrls) {
+      if (TextUtils.isEmpty(url)) {
+        ALog.e(TAG, "子任务url为null,即将删除该子任务。");
+        delItem.add(i);
+        continue;
+      } else if (!url.startsWith("http")) {
+        //} else if (!url.startsWith("http") && !url.startsWith("ftp")) {
+        ALog.e(TAG, "子任务url【" + url + "】错误,即将删除该子任务。");
+        delItem.add(i);
+        continue;
+      }
+      int index = url.indexOf("://");
+      if (index == -1) {
+        ALog.e(TAG, "子任务url【" + url + "】不合法,即将删除该子任务。");
+        delItem.add(i);
+        continue;
+      }
+
+      i++;
+    }
+
+    for (int index : delItem) {
+      mUrls.remove(index);
+      if (mSubNameTemp != null && !mSubNameTemp.isEmpty()) {
+        mSubNameTemp.remove(index);
+      }
+    }
+
+    mEntity.setGroupName(CommonUtil.getMd5Code(mUrls));
+
+    return true;
+  }
+
+  /**
+   * 更新单个子任务文件名
+   */
+  private void updateSingleSubFileName(DownloadTaskEntity taskEntity, String newName) {
+    DownloadEntity entity = taskEntity.getEntity();
+    if (!newName.equals(entity.getFileName())) {
+      String oldPath = mEntity.getDirPath() + "/" + entity.getFileName();
+      String newPath = mEntity.getDirPath() + "/" + newName;
+      File oldFile = new File(oldPath);
+      if (oldFile.exists()) {
+        oldFile.renameTo(new File(newPath));
+      }
+      CommonUtil.modifyTaskRecord(oldFile.getPath(), newPath);
+      entity.setDownloadPath(newPath);
+      taskEntity.setKey(newPath);
+      entity.setFileName(newName);
+      entity.update();
+    }
+  }
+
+  /**
+   * 如果用户设置了子任务文件名,检查子任务文件名
+   *
+   * @return {@code true} 合法
+   */
+  private boolean checkSubName() {
+    if (mSubNameTemp == null || mSubNameTemp.isEmpty()) {
+      return true;
+    }
+    if (mUrls.size() != mSubNameTemp.size()) {
+      ALog.e(TAG, "子任务文件名必须和子任务数量一致");
+      return false;
+    }
+
+    return true;
+  }
+}

+ 124 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupTask.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.download.downloader.DownloadGroupUtil;
+import com.arialyy.aria.core.download.downloader.FtpDirDownloadUtil;
+import com.arialyy.aria.core.inf.AbsGroupTask;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.scheduler.ISchedulers;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CheckUtil;
+
+/**
+ * Created by AriaL on 2017/6/27.
+ * 任务组任务
+ */
+public class DownloadGroupTask extends AbsGroupTask<DownloadGroupTaskEntity> {
+  private final String TAG = "DownloadGroupTask";
+  private DownloadGroupListener mListener;
+
+  private DownloadGroupTask(DownloadGroupTaskEntity taskEntity, Handler outHandler) {
+    mTaskEntity = taskEntity;
+    mOutHandler = outHandler;
+    mContext = AriaManager.APP;
+    mListener = new DownloadGroupListener(this, mOutHandler);
+    switch (taskEntity.getRequestType()) {
+      case AbsTaskEntity.D_HTTP:
+        mUtil = new DownloadGroupUtil(mListener, mTaskEntity);
+        break;
+      case AbsTaskEntity.D_FTP_DIR:
+        mUtil = new FtpDirDownloadUtil(mListener, mTaskEntity);
+        break;
+    }
+    Log.d(TAG, "FTP_TASK_MD5:" + mTaskEntity.hashCode());
+  }
+
+  @Override public boolean isRunning() {
+    return mUtil.isRunning();
+  }
+
+  public DownloadGroupEntity getEntity() {
+    return mTaskEntity.getEntity();
+  }
+
+  @Override public void start() {
+    if (mUtil.isRunning()) {
+      ALog.d(TAG, "任务正在下载");
+    } else {
+      mUtil.start();
+    }
+  }
+
+  @Override public void stop() {
+    if (!mUtil.isRunning()) {
+      mListener.onStop(getCurrentProgress());
+    } else {
+      mUtil.stop();
+    }
+  }
+
+  @Override public void cancel() {
+    if (!mUtil.isRunning()) {
+      mListener.onCancel();
+    } else {
+      mUtil.cancel();
+    }
+  }
+
+  @Override public String getTaskName() {
+    return "任务组->" + (TextUtils.isEmpty(mTaskEntity.getEntity().getAlias())
+        ? mTaskEntity.getEntity().getGroupName() : mTaskEntity.getEntity().getAlias());
+  }
+
+  public static class Builder {
+    DownloadGroupTaskEntity taskEntity;
+    Handler outHandler;
+    String targetName;
+
+    public Builder(String targetName, DownloadGroupTaskEntity taskEntity) {
+      CheckUtil.checkTaskEntity(taskEntity);
+      this.targetName = targetName;
+      this.taskEntity = taskEntity;
+    }
+
+    /**
+     * 设置自定义Handler处理下载状态时间
+     *
+     * @param schedulers {@link ISchedulers}
+     */
+    public DownloadGroupTask.Builder setOutHandler(ISchedulers schedulers) {
+      try {
+        outHandler = new Handler(schedulers);
+      } catch (Exception e) {
+        e.printStackTrace();
+        outHandler = new Handler(Looper.getMainLooper(), schedulers);
+      }
+      return this;
+    }
+
+    public DownloadGroupTask build() {
+      DownloadGroupTask task = new DownloadGroupTask(taskEntity, outHandler);
+      task.setTargetName(targetName);
+      return task;
+    }
+  }
+}

+ 63 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadGroupTaskEntity.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import com.arialyy.aria.core.inf.AbsGroupTaskEntity;
+import com.arialyy.aria.orm.ActionPolicy;
+import com.arialyy.aria.orm.annotation.Foreign;
+import com.arialyy.aria.orm.annotation.Ignore;
+import com.arialyy.aria.orm.annotation.Primary;
+import java.util.List;
+
+/**
+ * Created by AriaL on 2017/7/1.
+ * 任务组的任务实体
+ */
+public class DownloadGroupTaskEntity extends AbsGroupTaskEntity<DownloadGroupEntity> {
+
+  @Ignore private DownloadGroupEntity entity;
+
+  @Ignore private List<DownloadTaskEntity> subTaskEntities;
+
+  @Primary
+  @Foreign(parent = DownloadGroupEntity.class, column = "groupName",
+      onUpdate = ActionPolicy.CASCADE, onDelete = ActionPolicy.CASCADE)
+  private String key;
+
+  @Override public DownloadGroupEntity getEntity() {
+    return entity;
+  }
+
+  public void setEntity(DownloadGroupEntity entity) {
+    this.entity = entity;
+  }
+
+  public List<DownloadTaskEntity> getSubTaskEntities() {
+    return subTaskEntities;
+  }
+
+  public void setSubTaskEntities(List<DownloadTaskEntity> subTaskEntities) {
+    this.subTaskEntities = subTaskEntities;
+  }
+
+  @Override public String getKey() {
+    return key;
+  }
+
+  public void setKey(String key) {
+    this.key = key;
+  }
+}

+ 35 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadListener.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Handler;
+import com.arialyy.aria.core.inf.IDownloadListener;
+
+/**
+ * Created by Aria.Lao on 2017/7/20.
+ * 普通任务下载的事件监听器
+ */
+class DownloadListener extends BaseDListener<DownloadEntity, DownloadTaskEntity, DownloadTask>
+    implements IDownloadListener {
+  DownloadListener(DownloadTask task, Handler outHandler) {
+    super(task, outHandler);
+  }
+
+  @Override public void supportBreakpoint(boolean support) {
+
+  }
+
+}

+ 423 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadReceiver.java

@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.command.ICmd;
+import com.arialyy.aria.core.command.normal.CancelAllCmd;
+import com.arialyy.aria.core.command.normal.NormalCmdFactory;
+import com.arialyy.aria.core.common.ProxyHelper;
+import com.arialyy.aria.core.download.wrapper.DGEWrapper;
+import com.arialyy.aria.core.inf.AbsEntity;
+import com.arialyy.aria.core.inf.AbsReceiver;
+import com.arialyy.aria.core.inf.AbsTarget;
+import com.arialyy.aria.core.manager.TEManager;
+import com.arialyy.aria.core.scheduler.DownloadGroupSchedulers;
+import com.arialyy.aria.core.scheduler.DownloadSchedulers;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CheckUtil;
+import com.arialyy.aria.util.CommonUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by lyy on 2016/12/5.
+ * 下载功能接收器
+ */
+public class DownloadReceiver extends AbsReceiver {
+  private final String TAG = "DownloadReceiver";
+
+  /**
+   * 设置最大下载速度,单位:kb
+   * 该方法为实验性功能,清不要轻易在生产环境中使用。
+   *
+   * @param maxSpeed 为0表示不限速
+   */
+  @Deprecated public void setMaxSpeed(int maxSpeed) {
+    AriaManager.getInstance(AriaManager.APP).getDownloadConfig().setMaxSpeed(maxSpeed);
+  }
+
+  /**
+   * 使用下载实体执行下载操作
+   *
+   * @param entity 下载实体
+   */
+  public DownloadTarget load(DownloadEntity entity) {
+    return load(entity, false);
+  }
+
+  /**
+   * 使用下载实体执行下载操作
+   *
+   * @param refreshInfo 是否刷新下载信息
+   * @deprecated 请使用 {@link AbsTarget#resetState()}
+   * <pre>
+   *   <code>
+   *   Aria.download(this)
+   *       .load(URL)
+   *       .setDownloadPath(PATH)
+   *       .resetState()
+   *       .start();
+   *   </code>
+   * </pre>
+   */
+  @Deprecated public DownloadTarget load(DownloadEntity entity, boolean refreshInfo) {
+    CheckUtil.checkDownloadEntity(entity);
+    return new DownloadTarget(entity, targetName, refreshInfo);
+  }
+
+  /**
+   * 加载Http、https单任务下载地址
+   *
+   * @param url 下载地址
+   */
+  public DownloadTarget load(@NonNull String url) {
+    return load(url, false);
+  }
+
+  /**
+   * 加载Http、https单任务下载地址
+   *
+   * @param url 下载地址
+   * @param refreshInfo 是否刷新下载信息,当下载地址改变而保存路径不变,则需要设置该参数为{@code true}
+   * @deprecated 请使用 {@link AbsTarget#resetState()}
+   * <pre>
+   *   <code>
+   *   Aria.download(this)
+   *       .load(URL)
+   *       .setDownloadPath(PATH)
+   *       .resetState()
+   *       .start();
+   *   </code>
+   * </pre>
+   */
+  @Deprecated public DownloadTarget load(@NonNull String url, boolean refreshInfo) {
+    CheckUtil.checkUrlInvalidThrow(url);
+    return new DownloadTarget(url, targetName, refreshInfo);
+  }
+
+  /**
+   * 加载下载地址,如果任务组的中的下载地址改变了,则任务从新的一个任务组
+   *
+   * @param urls 任务组子任务下载地址列表
+   * @deprecated {@link #loadGroup(DownloadGroupEntity)}
+   */
+  @Deprecated
+  public DownloadGroupTarget load(List<String> urls) {
+    return loadGroup(urls);
+  }
+
+  /**
+   * 加载下载地址,如果任务组的中的下载地址改变了,则任务从新的一个任务组
+   */
+  public DownloadGroupTarget loadGroup(List<String> urls) {
+    CheckUtil.checkDownloadUrls(urls);
+    return new DownloadGroupTarget(urls, targetName);
+  }
+
+  /**
+   * 使用下载实体执行FTP下载操作
+   *
+   * @param entity 下载实体
+   */
+  public FtpDownloadTarget loadFtp(DownloadEntity entity) {
+    return loadFtp(entity, false);
+  }
+
+  /**
+   * 使用下载实体执行下载操作
+   *
+   * @param refreshInfo 是否刷新下载信息,当下载地址改变而保存路径不变,则需要设置该参数为{@code true}
+   * @deprecated 请使用 {@link AbsTarget#resetState()}
+   * <pre>
+   *   <code>
+   *   Aria.download(this)
+   *       .load(URL)
+   *       .setDownloadPath(PATH)
+   *       .resetState()
+   *       .start();
+   *   </code>
+   * </pre>
+   */
+  @Deprecated public FtpDownloadTarget loadFtp(DownloadEntity entity, boolean refreshInfo) {
+    CheckUtil.checkDownloadEntity(entity);
+    if (!entity.getUrl().startsWith("ftp")) {
+      throw new IllegalArgumentException("非FTP请求不能使用该方法");
+    }
+    return new FtpDownloadTarget(entity, targetName, refreshInfo);
+  }
+
+  /**
+   * 加载ftp单任务下载地址
+   */
+  public FtpDownloadTarget loadFtp(@NonNull String url) {
+    return loadFtp(url, false);
+  }
+
+  /**
+   * 加载ftp单任务下载地址
+   *
+   * @param refreshInfo 是否刷新下载信息
+   */
+  public FtpDownloadTarget loadFtp(@NonNull String url, boolean refreshInfo) {
+    CheckUtil.checkUrlInvalidThrow(url);
+    return new FtpDownloadTarget(url, targetName, refreshInfo);
+  }
+
+  /**
+   * 使用任务组实体执行任务组的实体执行任务组的下载操作,后续版本会删除该api
+   *
+   * @param groupEntity 如果加载的任务实体没有子项的下载地址,
+   * 那么你需要使用{@link DownloadGroupTarget#setGroupUrl(List)}设置子项的下载地址
+   * @deprecated 请使用 {@link #loadGroup(DownloadGroupEntity)}
+   */
+  @Deprecated
+  public DownloadGroupTarget load(DownloadGroupEntity groupEntity) {
+    return loadGroup(groupEntity);
+  }
+
+  /**
+   * 使用任务组实体执行任务组的实体执行任务组的下载操作
+   *
+   * @param groupEntity 如果加载的任务实体没有子项的下载地址,
+   * 那么你需要使用{@link DownloadGroupTarget#setGroupUrl(List)}设置子项的下载地址
+   */
+  public DownloadGroupTarget loadGroup(DownloadGroupEntity groupEntity) {
+    return new DownloadGroupTarget(groupEntity, targetName);
+  }
+
+  /**
+   * 加载ftp文件夹下载地址
+   */
+  public FtpDirDownloadTarget loadFtpDir(@NonNull String dirUrl) {
+    CheckUtil.checkUrlInvalidThrow(dirUrl);
+    return new FtpDirDownloadTarget(dirUrl, targetName);
+  }
+
+  /**
+   * 将当前类注册到Aria
+   */
+  public DownloadReceiver register() {
+    String className = obj.getClass().getName();
+    Set<String> dCounter = ProxyHelper.getInstance().downloadCounter;
+    Set<String> dgCounter = ProxyHelper.getInstance().downloadGroupCounter;
+    Set<String> dgsCounter = ProxyHelper.getInstance().downloadGroupSubCounter;
+    if (dCounter != null && dCounter.contains(className)) {
+      DownloadSchedulers.getInstance().register(obj);
+    }
+    if ((dgCounter != null && dgCounter.contains(className)) || (dgsCounter != null
+        && dgsCounter.contains(className))) {
+      DownloadGroupSchedulers.getInstance().register(obj);
+    }
+    return this;
+  }
+
+  /**
+   * 取消注册,如果是Activity或fragment,Aria会界面销毁时自动调用该方法。
+   * 如果在activity中一定要调用该方法,那么请在onDestroy()中调用
+   * 如果是Dialog或popupwindow,需要你在撤销界面时调用该方法
+   */
+  @Override public void unRegister() {
+    if (needRmListener) {
+      unRegisterListener();
+    }
+    AriaManager.getInstance(AriaManager.APP).removeReceiver(obj);
+  }
+
+  @Override public void unRegisterListener() {
+    String className = obj.getClass().getName();
+    Set<String> dCounter = ProxyHelper.getInstance().downloadCounter;
+    Set<String> dgCounter = ProxyHelper.getInstance().downloadGroupCounter;
+    Set<String> dgsCounter = ProxyHelper.getInstance().downloadGroupSubCounter;
+    if (dCounter != null && dCounter.contains(className)) {
+      DownloadSchedulers.getInstance().unRegister(obj);
+    }
+    if (dgCounter != null && dgCounter.contains(className) || (dgsCounter != null
+        && dgsCounter.contains(className))) {
+      DownloadGroupSchedulers.getInstance().unRegister(obj);
+    }
+  }
+
+  @Override public void destroy() {
+    targetName = null;
+  }
+
+  /**
+   * 通过下载链接获取下载实体
+   *
+   * @return 如果url错误或查找不到数据,则返回null
+   */
+  public DownloadEntity getDownloadEntity(String downloadUrl) {
+    if (CheckUtil.checkUrl(downloadUrl)) {
+      return null;
+    }
+    return DbEntity.findFirst(DownloadEntity.class, "url=? and isGroupChild='false'", downloadUrl);
+  }
+
+  /**
+   * 通过下载地址和文件保存路径获取下载任务实体
+   *
+   * @param downloadUrl 下载地址
+   * @return 如果url错误或查找不到数据,则返回null
+   */
+  public DownloadTaskEntity getDownloadTask(String downloadUrl) {
+    if (CheckUtil.checkUrl(downloadUrl)) {
+      return null;
+    }
+    return TEManager.getInstance().getTEntity(DownloadTaskEntity.class, downloadUrl);
+  }
+
+  /**
+   * 通过下载链接获取保存在数据库的下载任务组实体
+   *
+   * @param urls 任务组子任务下载地址列表
+   * @return 返回对应的任务组实体;如果查找不到对应的数据或子任务列表为null,返回null
+   */
+  public DownloadGroupTaskEntity getGroupTask(List<String> urls) {
+    if (urls == null || urls.isEmpty()) {
+      ALog.e(TAG, "获取任务组实体失败:任务组子任务下载地址列表为null");
+      return null;
+    }
+    return TEManager.getInstance().getGTEntity(DownloadGroupTaskEntity.class, urls);
+  }
+
+  /**
+   * 获取FTP文件夹下载任务实体
+   *
+   * @param dirUrl FTP文件夹本地下载路径
+   * @return 返回对应的任务组实体;如果查找不到对应的数据或路径为null,返回null
+   */
+  public DownloadGroupTaskEntity getFtpDirTask(String dirUrl) {
+    if (TextUtils.isEmpty(dirUrl)) {
+      ALog.e(TAG, "获取FTP文件夹实体失败:下载路径为null");
+      return null;
+    }
+    return TEManager.getInstance().getFDTEntity(DownloadGroupTaskEntity.class, dirUrl);
+  }
+
+  /**
+   * 下载任务是否存在
+   */
+  @Override public boolean taskExists(String downloadUrl) {
+    return DownloadEntity.findFirst(DownloadEntity.class, "url=?", downloadUrl) != null;
+  }
+
+  /**
+   * 获取所有普通下载任务
+   * 获取未完成的普通任务列表{@link #getAllNotCompletTask()}
+   * 获取已经完成的普通任务列表{@link #getAllCompleteTask()}
+   */
+  @Override public List<DownloadEntity> getTaskList() {
+    return DownloadEntity.findDatas(DownloadEntity.class, "isGroupChild=? and downloadPath!=''",
+        "false");
+  }
+
+  /**
+   * 获取所有未完成的普通下载任务
+   */
+  public List<DownloadEntity> getAllNotCompletTask() {
+    return DownloadEntity.findDatas(DownloadEntity.class,
+        "isGroupChild=? and downloadPath!='' and isComplete=?", "false", "false");
+  }
+
+  /**
+   * 获取所有已经完成的普通任务
+   */
+  public List<DownloadEntity> getAllCompleteTask() {
+    return DownloadEntity.findDatas(DownloadEntity.class,
+        "isGroupChild=? and downloadPath!='' and isComplete=?", "false", "true");
+  }
+
+  /**
+   * 获取任务组列表
+   *
+   * @return 如果没有任务组列表,则返回null
+   */
+  public List<DownloadGroupEntity> getGroupTaskList() {
+    List<DGEWrapper> wrappers = DbEntity.findRelationData(DGEWrapper.class);
+    if (wrappers == null || wrappers.isEmpty()) {
+      return null;
+    }
+    List<DownloadGroupEntity> entities = new ArrayList<>();
+    for (DGEWrapper wrapper : wrappers) {
+      entities.add(wrapper.groupEntity);
+    }
+    return entities;
+  }
+
+  /**
+   * 获取普通任务和任务组的任务列表
+   */
+  public List<AbsEntity> getTotalTaskList() {
+    List<AbsEntity> list = new ArrayList<>();
+    List<DownloadEntity> simpleTask = getTaskList();
+    List<DownloadGroupEntity> groupTask = getGroupTaskList();
+    if (simpleTask != null && !simpleTask.isEmpty()) {
+      list.addAll(simpleTask);
+    }
+    if (groupTask != null && !groupTask.isEmpty()) {
+      list.addAll(groupTask);
+    }
+    return list;
+  }
+
+  /**
+   * 停止所有正在下载的任务,并清空等待队列。
+   */
+  @Override public void stopAllTask() {
+    AriaManager.getInstance(AriaManager.APP)
+        .setCmd(NormalCmdFactory.getInstance()
+            .createCmd(targetName, new DownloadTaskEntity(), NormalCmdFactory.TASK_STOP_ALL,
+                ICmd.TASK_TYPE_DOWNLOAD))
+        .exe();
+  }
+
+  /**
+   * 恢复所有正在下载的任务
+   * 1.如果执行队列没有满,则开始下载任务,直到执行队列满
+   * 2.如果队列执行队列已经满了,则将所有任务添加到等待队列中
+   */
+  public void resumeAllTask() {
+    AriaManager.getInstance(AriaManager.APP)
+        .setCmd(NormalCmdFactory.getInstance()
+            .createCmd(targetName, new DownloadTaskEntity(), NormalCmdFactory.TASK_RESUME_ALL,
+                ICmd.TASK_TYPE_DOWNLOAD))
+        .exe();
+  }
+
+  /**
+   * 删除所有任务
+   *
+   * @param removeFile {@code true} 删除已经下载完成的任务,不仅删除下载记录,还会删除已经下载完成的文件,{@code false}
+   * 如果文件已经下载完成,只删除下载记录
+   */
+  @Override public void removeAllTask(boolean removeFile) {
+    final AriaManager ariaManager = AriaManager.getInstance(AriaManager.APP);
+    CancelAllCmd cancelCmd =
+        (CancelAllCmd) CommonUtil.createNormalCmd(targetName, new DownloadTaskEntity(),
+            NormalCmdFactory.TASK_CANCEL_ALL, ICmd.TASK_TYPE_DOWNLOAD);
+    cancelCmd.removeFile = removeFile;
+    ariaManager.setCmd(cancelCmd).exe();
+    Set<String> keys = ariaManager.getReceiver().keySet();
+    for (String key : keys) {
+      ariaManager.getReceiver().remove(key);
+    }
+  }
+}

+ 105 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadTarget.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.support.annotation.NonNull;
+import com.arialyy.aria.core.common.RequestEnum;
+import com.arialyy.aria.core.delegate.HttpHeaderDelegate;
+import com.arialyy.aria.core.inf.IHttpHeaderTarget;
+import java.util.Map;
+
+/**
+ * Created by lyy on 2016/12/5.
+ * https://github.com/AriaLyy/Aria
+ */
+public class DownloadTarget extends BaseNormalTarget<DownloadTarget>
+    implements IHttpHeaderTarget<DownloadTarget> {
+  private HttpHeaderDelegate<DownloadTarget, DownloadEntity, DownloadTaskEntity> mDelegate;
+
+  DownloadTarget(DownloadEntity entity, String targetName) {
+    this(entity, targetName, false);
+  }
+
+  DownloadTarget(DownloadEntity entity, String targetName, boolean refreshInfo) {
+    this(entity.getUrl(), targetName, refreshInfo);
+  }
+
+  DownloadTarget(String url, String targetName) {
+    this(url, targetName, false);
+  }
+
+  DownloadTarget(String url, String targetName, boolean refreshInfo) {
+    initTarget(url, targetName, refreshInfo);
+    mDelegate = new HttpHeaderDelegate<>(this, mTaskEntity);
+  }
+
+  /**
+   * 是否使用服务器通过content-disposition传递的文件名,内容格式{@code attachment;filename=***}
+   * 如果获取不到服务器文件名,则使用用户设置的文件名
+   *
+   * @param use {@code true} 使用
+   */
+  public DownloadTarget useServerFileName(boolean use) {
+    mTaskEntity.setUseServerFileName(use);
+    return this;
+  }
+
+  /**
+   * 设置文件存储路径
+   * 该api后续版本会删除
+   *
+   * @param downloadPath 文件保存路径
+   * @deprecated {@link #setFilePath(String)} 请使用这个api
+   */
+  @Deprecated public DownloadTarget setDownloadPath(@NonNull String downloadPath) {
+    return setFilePath(downloadPath);
+  }
+
+  /**
+   * 设置文件存储路径,如果需要修改新的文件名,修改路径便可。
+   * 如:原文件路径 /mnt/sdcard/test.zip
+   * 如果需要将test.zip改为game.zip,只需要重新设置文件路径为:/mnt/sdcard/game.zip
+   *
+   * @param filePath 路径必须为文件路径,不能为文件夹路径
+   */
+  public DownloadTarget setFilePath(@NonNull String filePath) {
+    mTempFilePath = filePath;
+    return this;
+  }
+
+  /**
+   * 从header中获取文件描述信息
+   */
+  public String getContentDisposition() {
+    return mEntity.getDisposition();
+  }
+
+  @Override protected int getTargetType() {
+    return HTTP;
+  }
+
+  @Override public DownloadTarget addHeader(@NonNull String key, @NonNull String value) {
+    return mDelegate.addHeader(key, value);
+  }
+
+  @Override public DownloadTarget addHeaders(Map<String, String> headers) {
+    return mDelegate.addHeaders(headers);
+  }
+
+  @Override public DownloadTarget setRequestMode(RequestEnum requestEnum) {
+    return mDelegate.setRequestMode(requestEnum);
+  }
+}

+ 183 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadTask.java

@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.os.Handler;
+import android.os.Looper;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.IUtil;
+import com.arialyy.aria.core.download.downloader.SimpleDownloadUtil;
+import com.arialyy.aria.core.inf.AbsNormalTask;
+import com.arialyy.aria.core.scheduler.ISchedulers;
+import com.arialyy.aria.util.ALog;
+import java.io.File;
+
+/**
+ * Created by lyy on 2016/8/11.
+ * 下载任务类
+ */
+public class DownloadTask extends AbsNormalTask<DownloadTaskEntity> {
+  public static final String TAG = "DownloadTask";
+
+  private DownloadListener mListener;
+  private DownloadEntity mEntity;
+  private IUtil mUtil;
+
+  private DownloadTask(DownloadTaskEntity taskEntity, Handler outHandler) {
+    mTaskEntity = taskEntity;
+    mOutHandler = outHandler;
+    mContext = AriaManager.APP;
+    mListener = new DownloadListener(this, mOutHandler);
+    mUtil = new SimpleDownloadUtil(taskEntity, mListener);
+    mEntity = taskEntity.getEntity();
+  }
+
+  /**
+   * 获取文件保存路径
+   *
+   * @return 如果路径不存在,返回null
+   */
+  public String getDownloadPath() {
+    File file = new File(mEntity.getDownloadPath());
+    if (!file.exists()) {
+      return null;
+    }
+    return mEntity.getDownloadPath();
+  }
+
+  public DownloadEntity getEntity() {
+    return mTaskEntity.getEntity();
+  }
+
+  /**
+   * 获取当前下载任务的下载地址
+   *
+   * @see DownloadTask#getKey()
+   */
+  @Deprecated public String getDownloadUrl() {
+    return mEntity.getUrl();
+  }
+
+  @Override public String getKey() {
+    return mEntity.getUrl();
+  }
+
+  /**
+   * 是否真正下载
+   *
+   * @return {@code true} 真正下载
+   */
+  @Override public boolean isRunning() {
+    return mUtil.isRunning();
+  }
+
+  public DownloadEntity getDownloadEntity() {
+    return mEntity;
+  }
+
+  /**
+   * 暂停任务,并让任务处于等待状态
+   */
+  @Override public void stopAndWait() {
+    stop(true);
+  }
+
+  /**
+   * 设置最大下载速度,单位:kb
+   *
+   * @param maxSpeed 为0表示不限速
+   */
+  public void setMaxSpeed(double maxSpeed) {
+    mUtil.setMaxSpeed(maxSpeed);
+  }
+
+  /**
+   * 开始下载
+   */
+  @Override public void start() {
+    mListener.isWait = false;
+    if (mUtil.isRunning()) {
+      ALog.d(TAG, "任务正在下载");
+    } else {
+      mUtil.start();
+    }
+  }
+
+  /**
+   * 停止下载
+   */
+  @Override public void stop() {
+    stop(false);
+  }
+
+  private void stop(boolean isWait) {
+    mListener.isWait = isWait;
+    if (mUtil.isRunning()) {
+      mUtil.stop();
+    } else {
+      mListener.onStop(mEntity.getCurrentProgress());
+    }
+  }
+
+  /**
+   * 取消下载
+   */
+  @Override public void cancel() {
+    if (mUtil.isRunning()) {
+      mUtil.cancel();
+    } else {
+      mListener.onCancel();
+    }
+  }
+
+  @Override public String getTaskName() {
+    return mEntity.getFileName();
+  }
+
+  public static class Builder {
+    DownloadTaskEntity taskEntity;
+    Handler outHandler;
+    String targetName;
+
+    public Builder(String targetName, DownloadTaskEntity taskEntity) {
+      this.targetName = targetName;
+      this.taskEntity = taskEntity;
+    }
+
+    /**
+     * 设置自定义Handler处理下载状态时间
+     *
+     * @param schedulers {@link ISchedulers}
+     */
+    public Builder setOutHandler(ISchedulers schedulers) {
+      try {
+        outHandler = new Handler(schedulers);
+      } catch (Exception e) {
+        ALog.w(TAG, ALog.getExceptionString(e));
+        outHandler = new Handler(Looper.getMainLooper(), schedulers);
+      }
+      return this;
+    }
+
+    public DownloadTask build() {
+      DownloadTask task = new DownloadTask(taskEntity, outHandler);
+      task.setTargetName(targetName);
+      taskEntity.save();
+      return task;
+    }
+  }
+}

+ 117 - 0
Aria/src/main/java/com/arialyy/aria/core/download/DownloadTaskEntity.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import com.arialyy.aria.core.inf.AbsNormalTaskEntity;
+import com.arialyy.aria.orm.ActionPolicy;
+import com.arialyy.aria.orm.annotation.Foreign;
+import com.arialyy.aria.orm.annotation.Ignore;
+import com.arialyy.aria.orm.annotation.NoNull;
+import com.arialyy.aria.orm.annotation.Primary;
+
+/**
+ * Created by lyy on 2017/1/23.
+ * 下载任务实体和下载实体为一对一关系,下载实体删除,任务实体自动删除
+ */
+public class DownloadTaskEntity extends AbsNormalTaskEntity<DownloadEntity> {
+
+  @Ignore private DownloadEntity entity;
+
+  /**
+   * 任务的url
+   */
+  @NoNull private String url;
+
+  /**
+   * 所属的任务组组名,如果不属于任务组,则为null
+   */
+  @Foreign(parent = DownloadGroupTaskEntity.class, column = "key",
+      onUpdate = ActionPolicy.CASCADE, onDelete = ActionPolicy.CASCADE)
+  private String groupName;
+
+  /**
+   * 是否是chunk模式
+   */
+  private boolean isChunked = false;
+
+  /**
+   * 该任务是否属于任务组
+   */
+  private boolean isGroupTask = false;
+
+  /**
+   * Task实体对应的key
+   */
+  @Primary
+  @Foreign(parent = DownloadEntity.class, column = "downloadPath",
+      onUpdate = ActionPolicy.CASCADE, onDelete = ActionPolicy.CASCADE)
+  private String key;
+
+
+
+  public DownloadTaskEntity() {
+  }
+
+  @Override public DownloadEntity getEntity() {
+    return entity;
+  }
+
+  @Override public String getKey() {
+    return key;
+  }
+
+  @Override public void setKey(String key) {
+    this.key = key;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public String getGroupName() {
+    return groupName;
+  }
+
+  public boolean isChunked() {
+    return isChunked;
+  }
+
+  public boolean isGroupTask() {
+    return isGroupTask;
+  }
+
+  public void setEntity(DownloadEntity entity) {
+    this.entity = entity;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public void setGroupName(String groupName) {
+    this.groupName = groupName;
+  }
+
+  public void setChunked(boolean chunked) {
+    isChunked = chunked;
+  }
+
+  public void setGroupTask(boolean groupTask) {
+    isGroupTask = groupTask;
+  }
+
+
+}

+ 104 - 0
Aria/src/main/java/com/arialyy/aria/core/download/FtpDirDownloadTarget.java

@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.delegate.FtpDelegate;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IFtpTarget;
+import com.arialyy.aria.core.manager.TEManager;
+import com.arialyy.aria.util.ALog;
+
+/**
+ * Created by Aria.Lao on 2017/7/26.
+ * ftp文件夹下载
+ */
+public class FtpDirDownloadTarget extends BaseGroupTarget<FtpDirDownloadTarget>
+    implements IFtpTarget<FtpDirDownloadTarget> {
+  private FtpDelegate<FtpDirDownloadTarget, DownloadGroupEntity, DownloadGroupTaskEntity> mDelegate;
+
+  FtpDirDownloadTarget(String url, String targetName) {
+    mTargetName = targetName;
+    init(url);
+  }
+
+  private void init(String key) {
+    mGroupName = key;
+    mTaskEntity = TEManager.getInstance().getFDTEntity(DownloadGroupTaskEntity.class, key);
+    mTaskEntity.setRequestType(AbsTaskEntity.D_FTP_DIR);
+    mEntity = mTaskEntity.getEntity();
+    if (mEntity != null) {
+      mDirPathTemp = mEntity.getDirPath();
+    }
+    mDelegate = new FtpDelegate<>(this, mTaskEntity);
+  }
+
+  @Override protected int getTargetType() {
+    return GROUP_FTP_DIR;
+  }
+
+  @Override protected boolean checkEntity() {
+    boolean b = getTargetType() == GROUP_FTP_DIR && checkDirPath() && checkUrl();
+    if (b) {
+      mEntity.save();
+      mTaskEntity.save();
+      if (mTaskEntity.getSubTaskEntities() != null) {
+        //初始化子项的登录信息
+        for (DownloadTaskEntity entity : mTaskEntity.getSubTaskEntities()) {
+          entity.getUrlEntity().needLogin = mTaskEntity.getUrlEntity().needLogin;
+          entity.getUrlEntity().account = mTaskEntity.getUrlEntity().account;
+          entity.getUrlEntity().user = mTaskEntity.getUrlEntity().user;
+          entity.getUrlEntity().password = mTaskEntity.getUrlEntity().password;
+        }
+      }
+    }
+    return b;
+  }
+
+  /**
+   * 检查普通任务的下载地址
+   *
+   * @return {@code true}地址合法
+   */
+  private boolean checkUrl() {
+    final String url = mGroupName;
+    if (TextUtils.isEmpty(url)) {
+      ALog.e(TAG, "下载失败,url为null");
+      return false;
+    } else if (!url.startsWith("ftp")) {
+      ALog.e(TAG, "下载失败,url【" + url + "】错误");
+      return false;
+    }
+    int index = url.indexOf("://");
+    if (index == -1) {
+      ALog.e(TAG, "下载失败,url【" + url + "】不合法");
+      return false;
+    }
+    return true;
+  }
+
+  @Override public FtpDirDownloadTarget charSet(String charSet) {
+    return mDelegate.charSet(charSet);
+  }
+
+  @Override public FtpDirDownloadTarget login(String userName, String password) {
+    return mDelegate.login(userName, password);
+  }
+
+  @Override public FtpDirDownloadTarget login(String userName, String password, String account) {
+    return mDelegate.login(userName, password, account);
+  }
+}

+ 92 - 0
Aria/src/main/java/com/arialyy/aria/core/download/FtpDownloadTarget.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download;
+
+import android.support.annotation.NonNull;
+import com.arialyy.aria.core.delegate.FtpDelegate;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IFtpTarget;
+import com.arialyy.aria.util.CommonUtil;
+
+/**
+ * Created by lyy on 2016/12/5.
+ * https://github.com/AriaLyy/Aria
+ */
+public class FtpDownloadTarget extends BaseNormalTarget<FtpDownloadTarget>
+    implements IFtpTarget<FtpDownloadTarget> {
+  private FtpDelegate<FtpDownloadTarget, DownloadEntity, DownloadTaskEntity> mDelegate;
+
+  FtpDownloadTarget(DownloadEntity entity, String targetName, boolean refreshInfo) {
+    this(entity.getUrl(), targetName, refreshInfo);
+  }
+
+  FtpDownloadTarget(String url, String targetName) {
+    this(url, targetName, false);
+  }
+
+  FtpDownloadTarget(String url, String targetName, boolean refreshInfo) {
+    initTarget(url, targetName, refreshInfo);
+    init(refreshInfo);
+  }
+
+  private void init(boolean refreshInfo) {
+    int lastIndex = url.lastIndexOf("/");
+    mEntity.setFileName(url.substring(lastIndex + 1, url.length()));
+    mTaskEntity.setUrlEntity(CommonUtil.getFtpUrlInfo(url));
+    mTaskEntity.setRefreshInfo(refreshInfo);
+    mTaskEntity.setRequestType(AbsTaskEntity.D_FTP);
+
+    mDelegate = new FtpDelegate<>(this, mTaskEntity);
+  }
+
+  /**
+   * 设置文件保存文件夹路径
+   *
+   * @param filePath 文件保存路径
+   * @deprecated {@link #setFilePath(String)} 请使用这个api
+   */
+  @Deprecated
+  public FtpDownloadTarget setDownloadPath(@NonNull String filePath) {
+    return setFilePath(filePath);
+  }
+
+  /**
+   * 设置文件保存文件夹路径
+   * 关于文件名:
+   * 1、如果保存路径是该文件的保存路径,如:/mnt/sdcard/file.zip,则使用路径中的文件名file.zip
+   * 2、如果保存路径是文件夹路径,如:/mnt/sdcard/,则使用FTP服务器该文件的文件名
+   */
+  public FtpDownloadTarget setFilePath(@NonNull String filePath) {
+    mTempFilePath = filePath;
+    return this;
+  }
+
+  @Override protected int getTargetType() {
+    return FTP;
+  }
+
+  @Override public FtpDownloadTarget charSet(String charSet) {
+    return mDelegate.charSet(charSet);
+  }
+
+  @Override public FtpDownloadTarget login(String userName, String password) {
+    return mDelegate.login(userName, password);
+  }
+
+  @Override public FtpDownloadTarget login(String userName, String password, String account) {
+    return mDelegate.login(userName, password, account);
+  }
+}

+ 527 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/AbsGroupUtil.java

@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.IUtil;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import com.arialyy.aria.util.NetUtils;
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by AriaL on 2017/6/30.
+ * 任务组核心逻辑
+ */
+public abstract class AbsGroupUtil implements IUtil {
+  private final String TAG = "AbsGroupUtil";
+  /**
+   * FTP文件夹
+   */
+  protected int FTP_DIR = 0xa1;
+  /**
+   * HTTP 任务组
+   */
+  protected int HTTP_GROUP = 0xa2;
+
+  /**
+   * 任务组所有任务总长度
+   */
+  long mTotalLen = 0;
+  long mCurrentLocation = 0;
+  private ExecutorService mExePool;
+  protected IDownloadGroupListener mListener;
+  protected DownloadGroupTaskEntity mGTEntity;
+  private boolean isRunning = false;
+  private Timer mTimer;
+  /**
+   * 保存所有没有下载完成的任务,key为下载地址
+   */
+  Map<String, DownloadTaskEntity> mExeMap = new HashMap<>();
+
+  /**
+   * 下载失败的映射表,key为下载地址
+   */
+  Map<String, DownloadTaskEntity> mFailMap = new HashMap<>();
+
+  /**
+   * 该任务组对应的所有任务
+   */
+  private Map<String, DownloadTaskEntity> mTasksMap = new HashMap<>();
+
+  /**
+   * 下载器映射表,key为下载地址
+   */
+  private Map<String, Downloader> mDownloaderMap = new HashMap<>();
+
+  /**
+   * 是否需要读取文件长度,{@code true}需要
+   */
+  boolean isNeedLoadFileSize = true;
+  //已经完成的任务数
+  int mCompleteNum = 0;
+  //停止的任务数
+  private int mStopNum = 0;
+  //任务组大小
+  int mGroupSize = 0;
+  private long mUpdateInterval = 1000;
+
+  AbsGroupUtil(IDownloadGroupListener listener, DownloadGroupTaskEntity groupEntity) {
+    mListener = listener;
+    mGTEntity = groupEntity;
+    mExePool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+    mUpdateInterval =
+        AriaManager.getInstance(AriaManager.APP).getDownloadConfig().getUpdateInterval();
+  }
+
+  /**
+   * 获取任务类型
+   *
+   * @return {@link #FTP_DIR}、{@link #HTTP_GROUP}
+   */
+  abstract int getTaskType();
+
+  /**
+   * 更新任务组文件大小
+   */
+  void updateFileSize() {
+    if (isNeedLoadFileSize) {
+      mGTEntity.getEntity().setFileSize(mTotalLen);
+      mGTEntity.getEntity().update();
+    }
+  }
+
+  /**
+   * 启动子任务下载
+   *
+   * @param url 子任务下载地址
+   */
+  public void startSubTask(String url) {
+    if (!checkSubTask(url, "开始")) return;
+    if (!isRunning) {
+      isRunning = true;
+      startTimer();
+    }
+    Downloader d = getDownloader(url, false);
+    if (d != null && !d.isRunning()) {
+      d.setNewTask(false);
+      d.start();
+    }
+  }
+
+  /**
+   * 停止子任务下载
+   *
+   * @param url 子任务下载地址
+   */
+  public void stopSubTask(String url) {
+    if (!checkSubTask(url, "停止")) return;
+    Downloader d = getDownloader(url, false);
+    if (d != null && d.isRunning()) {
+      d.stop();
+    }
+  }
+
+  /**
+   * 删除子任务
+   *
+   * @param url 子任务下载地址
+   */
+  public void cancelSubTask(String url) {
+    Set<String> urls = mTasksMap.keySet();
+    if (!urls.isEmpty() && urls.contains(url)) {
+      DownloadTaskEntity det = mTasksMap.get(url);
+      if (det != null) {
+        mTotalLen -= det.getEntity().getFileSize();
+        mGroupSize--;
+        if (mGroupSize == 0) {
+          closeTimer(false);
+          mListener.onCancel();
+        }
+      }
+      mGTEntity.update();
+    }
+    Downloader d = getDownloader(url, false);
+    if (d != null) {
+      d.cancel();
+    }
+  }
+
+  /**
+   * 检查子任务
+   *
+   * @param url 子任务url
+   * @param type 任务类型
+   * @return {@code true} 任务可以下载
+   */
+  private boolean checkSubTask(String url, String type) {
+    DownloadTaskEntity entity = mTasksMap.get(url);
+    if (entity != null) {
+      if (entity.getState() == IEntity.STATE_COMPLETE) {
+        ALog.w(TAG, "任务【" + url + "】已完成," + type + "失败");
+        return false;
+      }
+    } else {
+      ALog.w(TAG, "任务组中没有该任务【" + url + "】," + type + "失败");
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 通过地址获取下载器
+   *
+   * @param url 子任务下载地址
+   * @param start 是否启动任务
+   */
+  private Downloader getDownloader(String url, boolean start) {
+    Downloader d = mDownloaderMap.get(url);
+    if (d == null) {
+      return createChildDownload(mTasksMap.get(url), start);
+    }
+    return d;
+  }
+
+  @Override public long getFileSize() {
+    return mTotalLen;
+  }
+
+  @Override public long getCurrentLocation() {
+    return mCurrentLocation;
+  }
+
+  @Override public boolean isRunning() {
+    return isRunning;
+  }
+
+  @Override public void cancel() {
+    closeTimer(false);
+    onCancel();
+    if (!mExePool.isShutdown()) {
+      mExePool.shutdown();
+    }
+
+    Set<String> keys = mDownloaderMap.keySet();
+    for (String key : keys) {
+      Downloader dt = mDownloaderMap.get(key);
+      if (dt != null) {
+        dt.cancel();
+      }
+    }
+    clearState();
+    mListener.onCancel();
+  }
+
+  public void onCancel() {
+
+  }
+
+  @Override public void stop() {
+    closeTimer(false);
+    onStop();
+    if (!mExePool.isShutdown()) {
+      mExePool.shutdown();
+    }
+
+    Set<String> keys = mDownloaderMap.keySet();
+    for (String key : keys) {
+      Downloader dt = mDownloaderMap.get(key);
+      if (dt != null) {
+        dt.stop();
+      }
+    }
+  }
+
+  protected void onStop() {
+
+  }
+
+  /**
+   * 预处理操作,由于属性的不同,http任务组在构造函数中就可以完成了
+   * 而FTP文件夹的,需要获取完成所有子任务信息才算预处理完成
+   */
+  protected void onPre() {
+    mListener.onPre();
+    mGroupSize = mGTEntity.getSubTaskEntities().size();
+    mTotalLen = mGTEntity.getEntity().getFileSize();
+    isNeedLoadFileSize = mTotalLen <= 1;
+    for (DownloadTaskEntity te : mGTEntity.getSubTaskEntities()) {
+      File file = new File(te.getKey());
+      if (te.getState() == IEntity.STATE_COMPLETE && file.exists()) {
+        mCompleteNum++;
+        mCurrentLocation += te.getEntity().getFileSize();
+      } else {
+        mExeMap.put(te.getUrl(), te);
+        mCurrentLocation += file.exists() ? te.getEntity().getCurrentProgress() : 0;
+      }
+      if (isNeedLoadFileSize) {
+        mTotalLen += te.getEntity().getFileSize();
+      }
+      mTasksMap.put(te.getUrl(), te);
+    }
+    updateFileSize();
+  }
+
+  @Override public void start() {
+    isRunning = true;
+    clearState();
+    onStart();
+  }
+
+  protected void onStart() {
+
+  }
+
+  @Override public void resume() {
+    start();
+    mListener.onResume(mCurrentLocation);
+  }
+
+  @Override public void setMaxSpeed(double maxSpeed) {
+
+  }
+
+  private void clearState(){
+    mDownloaderMap.clear();
+    mFailMap.clear();
+  }
+
+  private void closeTimer(boolean isRunning) {
+    this.isRunning = isRunning;
+    if (mTimer != null) {
+      mTimer.purge();
+      mTimer.cancel();
+      mTimer = null;
+    }
+  }
+
+  /**
+   * 开始进度流程
+   */
+  void startRunningFlow() {
+    closeTimer(true);
+    mListener.onPostPre(mTotalLen);
+    mListener.onStart(mCurrentLocation);
+    startTimer();
+  }
+
+  private void startTimer() {
+    mTimer = new Timer(true);
+    mTimer.schedule(new TimerTask() {
+      @Override public void run() {
+        if (!isRunning) {
+          closeTimer(false);
+        } else if (mCurrentLocation >= 0) {
+          mListener.onProgress(mCurrentLocation);
+        }
+      }
+    }, 0, mUpdateInterval);
+  }
+
+  /**
+   * 创建子任务下载器,默认创建完成自动启动
+   */
+  Downloader createChildDownload(DownloadTaskEntity taskEntity) {
+    return createChildDownload(taskEntity, true);
+  }
+
+  /**
+   * 创建子任务下载器,启动子任务下载器
+   *
+   * @param start 是否启动下载
+   */
+  private Downloader createChildDownload(DownloadTaskEntity taskEntity, boolean start) {
+    ChildDownloadListener listener = new ChildDownloadListener(taskEntity);
+    Downloader dt = new Downloader(listener, taskEntity);
+    mDownloaderMap.put(taskEntity.getEntity().getUrl(), dt);
+    if (mExePool.isShutdown()) return dt;
+    if (start) {
+      mExePool.execute(dt);
+    }
+    return dt;
+  }
+
+  /**
+   * 子任务事件监听
+   */
+  private class ChildDownloadListener implements IDownloadListener {
+    private DownloadTaskEntity subTaskEntity;
+    private DownloadEntity subEntity;
+    private int RUN_SAVE_INTERVAL = 5 * 1000;  //5s保存一次下载中的进度
+    private long lastSaveTime;
+    private long lastLen = 0;
+
+    ChildDownloadListener(DownloadTaskEntity entity) {
+      subTaskEntity = entity;
+      subEntity = subTaskEntity.getEntity();
+      subEntity.setFailNum(0);
+      lastLen = subEntity.getCurrentProgress();
+      lastSaveTime = System.currentTimeMillis();
+    }
+
+    @Override public void onPre() {
+      saveData(IEntity.STATE_PRE, -1);
+    }
+
+    @Override public void onPostPre(long fileSize) {
+      subEntity.setFileSize(fileSize);
+      subEntity.setConvertFileSize(CommonUtil.formatFileSize(fileSize));
+      saveData(IEntity.STATE_POST_PRE, -1);
+      mListener.onSubPre(subEntity);
+    }
+
+    @Override public void onResume(long resumeLocation) {
+      saveData(IEntity.STATE_POST_PRE, IEntity.STATE_RUNNING);
+      lastLen = resumeLocation;
+      mListener.onSubStart(subEntity);
+    }
+
+    @Override public void onStart(long startLocation) {
+      saveData(IEntity.STATE_POST_PRE, IEntity.STATE_RUNNING);
+      lastLen = startLocation;
+      mListener.onSubStart(subEntity);
+    }
+
+    @Override public void onProgress(long currentLocation) {
+      long speed = currentLocation - lastLen;
+      mCurrentLocation += speed;
+      subEntity.setCurrentProgress(currentLocation);
+      handleSpeed(speed);
+      mListener.onSubRunning(subEntity);
+      if (System.currentTimeMillis() - lastSaveTime >= RUN_SAVE_INTERVAL) {
+        saveData(IEntity.STATE_RUNNING, currentLocation);
+        lastSaveTime = System.currentTimeMillis();
+      }
+      lastLen = currentLocation;
+    }
+
+    @Override public void onStop(long stopLocation) {
+      saveData(IEntity.STATE_STOP, stopLocation);
+      handleSpeed(0);
+      mListener.onSubStop(subEntity);
+      synchronized (AbsGroupUtil.class) {
+        mStopNum++;
+        if (mStopNum + mCompleteNum + mFailMap.size() == mGroupSize) {
+          closeTimer(false);
+          mListener.onStop(mCurrentLocation);
+        }
+      }
+    }
+
+    @Override public void onCancel() {
+      saveData(IEntity.STATE_CANCEL, -1);
+      handleSpeed(0);
+      mListener.onSubCancel(subEntity);
+    }
+
+    @Override public void onComplete() {
+      saveData(IEntity.STATE_COMPLETE, subEntity.getFileSize());
+      handleSpeed(0);
+      mListener.onSubComplete(subEntity);
+      synchronized (ChildDownloadListener.class) {
+        mCompleteNum++;
+        //如果子任务完成的数量和总任务数一致,表示任务组任务已经完成
+        if (mCompleteNum >= mGroupSize) {
+          closeTimer(false);
+          mListener.onComplete();
+        } else if (mFailMap.size() > 0 && mStopNum + mCompleteNum + mFailMap.size() >= mGroupSize) {
+          //如果子任务完成数量加上失败的数量和总任务数一致,则任务组停止下载
+          closeTimer(false);
+          mListener.onStop(mCurrentLocation);
+        }
+      }
+    }
+
+    @Override public void onFail(boolean needRetry) {
+      subEntity.setFailNum(subEntity.getFailNum() + 1);
+      saveData(IEntity.STATE_FAIL, lastLen);
+      handleSpeed(0);
+      reTry(needRetry);
+    }
+
+    /**
+     * 重试下载
+     */
+    private void reTry(boolean needRetry) {
+      synchronized (ChildDownloadListener.class) {
+        if (subEntity.getFailNum() < 5 && needRetry && NetUtils.isConnected(AriaManager.APP)) {
+          reStartTask();
+        } else {
+          mFailMap.put(subTaskEntity.getUrl(), subTaskEntity);
+          mListener.onSubFail(subEntity);
+          //如果失败的任务数大于实际的下载任务数,任务组停止下载
+          if (mFailMap.size() >= mExeMap.size()) {
+            closeTimer(false);
+            if (mFailMap.size() == mGroupSize) {  //所有任务都失败了,则认为该任务组已经失败
+              mListener.onFail(true);
+            } else {
+              mListener.onStop(mCurrentLocation);
+            }
+          }
+        }
+      }
+    }
+
+    private void reStartTask() {
+      Timer timer = new Timer();
+      timer.schedule(new TimerTask() {
+        @Override public void run() {
+          Downloader dt = mDownloaderMap.get(subEntity.getUrl());
+          dt.start();
+        }
+      }, 3000);
+    }
+
+    private void handleSpeed(long speed) {
+      subEntity.setSpeed(speed);
+      subEntity.setConvertSpeed(speed <= 0 ? "" : CommonUtil.formatFileSize(speed) + "/s");
+      subEntity.setPercent((int) (subEntity.getFileSize() <= 0 ? 0
+          : subEntity.getCurrentProgress() * 100 / subEntity.getFileSize()));
+    }
+
+    private void saveData(int state, long location) {
+      subTaskEntity.setState(state);
+      subEntity.setState(state);
+      subEntity.setComplete(state == IEntity.STATE_COMPLETE);
+      if (state == IEntity.STATE_CANCEL) {
+        subEntity.deleteData();
+        return;
+      } else if (subEntity.isComplete()) {
+        subEntity.setCompleteTime(System.currentTimeMillis());
+        subEntity.setCurrentProgress(subEntity.getFileSize());
+      } else if (location > 0) {
+        subEntity.setCurrentProgress(location);
+      }
+      subTaskEntity.update();
+    }
+
+    @Override public void supportBreakpoint(boolean support) {
+
+    }
+  }
+}

+ 167 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/ConnectionHelp.java

@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.SSLContextUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Set;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Created by lyy on 2017/1/18.
+ * 链接帮助类
+ */
+class ConnectionHelp {
+
+  /**
+   * 转换HttpUrlConnect的inputStream流
+   *
+   * @return {@link GZIPInputStream}、{@link InflaterInputStream}
+   * @throws IOException
+   */
+  static InputStream convertInputStream(HttpURLConnection connection) throws IOException {
+    String encoding = connection.getContentEncoding();
+    if (TextUtils.isEmpty(encoding)) {
+      return connection.getInputStream();
+    }
+    if (encoding.contains("gzip")) {
+      return new GZIPInputStream(connection.getInputStream());
+    } else if (encoding.contains("deflate")) {
+      return new InflaterInputStream(connection.getInputStream());
+    } else {
+      return connection.getInputStream();
+    }
+  }
+
+  /**
+   * 处理链接
+   *
+   * @throws IOException
+   */
+  static HttpURLConnection handleConnection(URL url) throws IOException {
+    HttpURLConnection conn;
+    URLConnection urlConn = url.openConnection();
+    if (urlConn instanceof HttpsURLConnection) {
+      conn = (HttpsURLConnection) urlConn;
+      SSLContext sslContext =
+          SSLContextUtil.getSSLContext(SSLContextUtil.CA_ALIAS, SSLContextUtil.CA_PATH);
+      if (sslContext == null) {
+        sslContext = SSLContextUtil.getDefaultSLLContext();
+      }
+      SSLSocketFactory ssf = sslContext.getSocketFactory();
+      ((HttpsURLConnection) conn).setSSLSocketFactory(ssf);
+      ((HttpsURLConnection) conn).setHostnameVerifier(SSLContextUtil.HOSTNAME_VERIFIER);
+    } else {
+      conn = (HttpURLConnection) urlConn;
+    }
+    return conn;
+  }
+
+  /**
+   * 设置头部参数
+   *
+   * @throws ProtocolException
+   */
+  static HttpURLConnection setConnectParam(DownloadTaskEntity entity, HttpURLConnection conn)
+      throws ProtocolException {
+    conn.setRequestMethod(entity.getRequestEnum().name);
+    Set<String> keys = null;
+    if (entity.getHeaders() != null && entity.getHeaders().size() > 0) {
+      keys = entity.getHeaders().keySet();
+      for (String key : keys) {
+        conn.setRequestProperty(key, entity.getHeaders().get(key));
+      }
+    }
+    if (keys == null || !keys.contains("Charset")) {
+      conn.setRequestProperty("Charset", "UTF-8");
+    }
+    if (keys == null || !keys.contains("User-Agent")) {
+      conn.setRequestProperty("User-Agent", getUserAgent());
+    }
+    if (keys == null || !keys.contains("Accept")) {
+      StringBuilder accept = new StringBuilder();
+      accept.append("image/gif, ")
+          .append("image/jpeg, ")
+          .append("image/pjpeg, ")
+          .append("image/webp, ")
+          .append("image/apng, ")
+          .append("application/xml, ")
+          .append("application/xaml+xml, ")
+          .append("application/xhtml+xml, ")
+          .append("application/x-shockwave-flash, ")
+          .append("application/x-ms-xbap, ")
+          .append("application/x-ms-application, ")
+          .append("application/msword, ")
+          .append("application/vnd.ms-excel, ")
+          .append("application/vnd.ms-xpsdocument, ")
+          .append("application/vnd.ms-powerpoint, ")
+          .append("text/plain, ")
+          .append("text/html, ")
+          .append("*/*");
+      conn.setRequestProperty("Accept", accept.toString());
+    }
+    if (keys == null || !keys.contains("Accept-Encoding")) {
+      conn.setRequestProperty("Accept-Encoding", "identity");
+    }
+    if (keys == null || !keys.contains("Accept-Charset")) {
+      conn.setRequestProperty("Accept-Charset", "UTF-8");
+    }
+    if (keys == null || !keys.contains("Connection")) {
+      conn.setRequestProperty("Connection", "Keep-Alive");
+    }
+    //302获取重定向地址
+    conn.setInstanceFollowRedirects(false);
+    return conn;
+  }
+  /**
+   * 返回正确的UserAgent
+   * @return
+   */
+  public   static String getUserAgent(){
+    StringBuilder sb = null;
+    try {
+      String userAgent;
+      sb = new StringBuilder();
+      userAgent = System.getProperty("http.agent");//Dalvik/2.1.0 (Linux; U; Android 6.0.1; vivo X9L Build/MMB29M)
+
+      for (int i = 0, length = userAgent.length(); i < length; i++) {
+        char c = userAgent.charAt(i);
+        if (c <= '\u001f' || c >= '\u007f') {
+          sb.append(String.format("\\u%04x", (int) c));
+        } else {
+          sb.append(c);
+        }
+      }
+
+      ALog.d("Aria_ConnectionHelp", "User-Agent: "+ sb.toString());
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+    return TextUtils.isEmpty(sb) ? "Dalvik/2.1.0 (Linux; U; Android 5.1.1; letv x501 Build/LMY48Z)" : sb.toString();
+  }
+}

+ 149 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/DownloadGroupUtil.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import android.util.SparseArray;
+import com.arialyy.aria.core.common.CompleteInfo;
+import com.arialyy.aria.core.common.IUtil;
+import com.arialyy.aria.core.common.OnFileInfoCallback;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.util.ALog;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by AriaL on 2017/6/30.
+ * 任务组下载工具
+ */
+public class DownloadGroupUtil extends AbsGroupUtil implements IUtil {
+  private final String TAG = "DownloadGroupUtil";
+  private ExecutorService mInfoPool;
+  private int mInitCompleteNum, mInitFailNum;
+
+  /**
+   * 文件信息回调组
+   */
+  private SparseArray<OnFileInfoCallback> mFileInfoCallbacks = new SparseArray<>();
+
+  public DownloadGroupUtil(IDownloadGroupListener listener, DownloadGroupTaskEntity taskEntity) {
+    super(listener, taskEntity);
+    mInfoPool = Executors.newCachedThreadPool();
+    onPre();
+  }
+
+  @Override int getTaskType() {
+    return HTTP_GROUP;
+  }
+
+  @Override public void onCancel() {
+    super.onCancel();
+    if (!mInfoPool.isShutdown()) {
+      mInfoPool.shutdown();
+    }
+  }
+
+  @Override protected void onStop() {
+    super.onStop();
+    if (!mInfoPool.isShutdown()) {
+      mInfoPool.shutdown();
+    }
+  }
+
+  @Override protected void onStart() {
+    super.onStart();
+    if (mCompleteNum == mGroupSize) {
+      mListener.onComplete();
+      return;
+    }
+
+    if (mExeMap.size() == 0) {
+      ALog.e(TAG, "任务组无可执行任务");
+      mListener.onFail(false);
+      return;
+    }
+    Set<String> keys = mExeMap.keySet();
+    for (String key : keys) {
+      DownloadTaskEntity taskEntity = mExeMap.get(key);
+      if (taskEntity != null) {
+        if (taskEntity.getState() != IEntity.STATE_FAIL
+            && taskEntity.getState() != IEntity.STATE_WAIT) {
+          createChildDownload(taskEntity);
+        } else {
+          mInfoPool.execute(createFileInfoThread(taskEntity));
+        }
+      }
+    }
+    if (mCurrentLocation == mTotalLen) {
+      mListener.onComplete();
+    }
+  }
+
+  /**
+   * 创建文件信息获取线程
+   */
+  private HttpFileInfoThread createFileInfoThread(DownloadTaskEntity taskEntity) {
+    OnFileInfoCallback callback = mFileInfoCallbacks.get(taskEntity.hashCode());
+
+    if (callback == null) {
+      callback = new OnFileInfoCallback() {
+        int failNum = 0;
+
+        @Override public void onComplete(String url, CompleteInfo info) {
+          DownloadTaskEntity te = mExeMap.get(url);
+          if (te != null) {
+            if (isNeedLoadFileSize) {
+              mTotalLen += te.getEntity().getFileSize();
+            }
+            createChildDownload(te);
+          }
+          mInitCompleteNum ++;
+
+          if (mInitCompleteNum + mInitFailNum >= mGroupSize || !isNeedLoadFileSize) {
+            startRunningFlow();
+            updateFileSize();
+          }
+        }
+
+        @Override public void onFail(String url, String errorMsg, boolean needRetry) {
+          ALog.e(TAG, "任务【" + url + "】初始化失败。");
+          DownloadTaskEntity te = mExeMap.get(url);
+          if (te != null) {
+            mFailMap.put(url, te);
+            mFileInfoCallbacks.put(te.hashCode(), this);
+            mExeMap.remove(url);
+          }
+          //404链接不重试下载
+          //if (failNum < 3 && !errorMsg.contains("错误码:404") && !errorMsg.contains(
+          //    "UnknownHostException")) {
+          //  mInfoPool.execute(createFileInfoThread(te));
+          //} else {
+          //  mInitFailNum++;
+          //}
+          //failNum++;
+          mInitFailNum ++;
+          if (mInitCompleteNum + mInitFailNum >= mGroupSize || !isNeedLoadFileSize) {
+            startRunningFlow();
+            updateFileSize();
+          }
+        }
+      };
+    }
+    return new HttpFileInfoThread(taskEntity, callback);
+  }
+}

+ 103 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/Downloader.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.AbsFileer;
+import com.arialyy.aria.core.common.AbsThreadTask;
+import com.arialyy.aria.core.common.SubThreadConfig;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.BufferedRandomAccessFile;
+import com.arialyy.aria.util.CommonUtil;
+import com.arialyy.aria.util.ErrorHelp;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Created by AriaL on 2017/7/1.
+ * 文件下载器
+ */
+class Downloader extends AbsFileer<DownloadEntity, DownloadTaskEntity> {
+  private String TAG = "Downloader";
+
+  Downloader(IDownloadListener listener, DownloadTaskEntity taskEntity) {
+    super(listener, taskEntity);
+    mTempFile = new File(mEntity.getDownloadPath());
+    AriaManager manager = AriaManager.getInstance(AriaManager.APP);
+    setUpdateInterval(manager.getDownloadConfig().getUpdateInterval());
+  }
+
+  @Override protected int setNewTaskThreadNum() {
+    return
+        // 小于1m的文件或是任务组的子任务、使用虚拟文件,线程数都是1
+        mEntity.getFileSize() <= SUB_LEN
+            || mTaskEntity.getRequestType() == AbsTaskEntity.D_FTP_DIR
+            || mTaskEntity.getRequestType() == AbsTaskEntity.DG_HTTP
+            || mRecord.isOpenDynamicFile
+            ? 1
+            : AriaManager.getInstance(mContext).getDownloadConfig().getThreadNum();
+  }
+
+  @Override protected boolean handleNewTask() {
+    CommonUtil.createFile(mTempFile.getPath());
+    BufferedRandomAccessFile file = null;
+    try {
+      file = new BufferedRandomAccessFile(new File(mTempFile.getPath()), "rwd", 8192);
+      //设置文件长度
+      file.setLength(mRecord.isOpenDynamicFile ? 1 : mEntity.getFileSize());
+      return true;
+    } catch (IOException e) {
+      failDownload("下载失败【downloadUrl:"
+          + mEntity.getUrl()
+          + "】\n【filePath:"
+          + mEntity.getDownloadPath()
+          + "】\n"
+          + ALog.getExceptionString(e));
+    } finally {
+      if (file != null) {
+        try {
+          file.close();
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override protected AbsThreadTask selectThreadTask(SubThreadConfig<DownloadTaskEntity> config) {
+    switch (mTaskEntity.getRequestType()) {
+      case AbsTaskEntity.D_FTP:
+      case AbsTaskEntity.D_FTP_DIR:
+        return new FtpThreadTask(mConstance, (IDownloadListener) mListener, config);
+      case AbsTaskEntity.D_HTTP:
+        return new HttpThreadTask(mConstance, (IDownloadListener) mListener, config);
+    }
+    return null;
+  }
+
+  private void failDownload(String errorMsg) {
+    closeTimer();
+    ALog.e(TAG, errorMsg);
+    mConstance.isRunning = false;
+    mListener.onFail(false);
+    ErrorHelp.saveError(TAG, "", errorMsg);
+  }
+}

+ 87 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpDirDownloadUtil.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.common.CompleteInfo;
+import com.arialyy.aria.core.common.OnFileInfoCallback;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.util.ErrorHelp;
+import java.util.Set;
+
+/**
+ * Created by Aria.Lao on 2017/7/27.
+ * ftp文件夹下载工具
+ */
+public class FtpDirDownloadUtil extends AbsGroupUtil {
+  private String TAG = "FtpDirDownloadUtil";
+
+  public FtpDirDownloadUtil(IDownloadGroupListener listener, DownloadGroupTaskEntity taskEntity) {
+    super(listener, taskEntity);
+  }
+
+  @Override int getTaskType() {
+    return FTP_DIR;
+  }
+
+  @Override protected void onStart() {
+    super.onStart();
+    if (mGTEntity.getEntity().getFileSize() > 1) {
+      onPre();
+      startDownload();
+    } else {
+      new FtpDirInfoThread(mGTEntity, new OnFileInfoCallback() {
+        @Override public void onComplete(String url, CompleteInfo info) {
+          if (info.code >= 200 && info.code < 300) {
+            onPre();
+            startDownload();
+          }
+        }
+
+        @Override public void onFail(String url, String errorMsg, boolean needRetry) {
+          DownloadTaskEntity te = mExeMap.get(url);
+          if (te != null) {
+            mFailMap.put(url, te);
+            mExeMap.remove(url);
+          }
+          mListener.onFail(needRetry);
+          ErrorHelp.saveError(TAG, "", errorMsg);
+        }
+      }).start();
+    }
+  }
+
+  private void startDownload() {
+    if (mCompleteNum == mGroupSize) {
+      mListener.onComplete();
+      return;
+    }
+    int i = 0;
+    Set<String> keys = mExeMap.keySet();
+    for (String key : keys) {
+      DownloadTaskEntity taskEntity = mExeMap.get(key);
+      if (taskEntity != null) {
+        createChildDownload(taskEntity);
+        i++;
+      }
+    }
+    if (mExeMap.size() == 0) {
+      mListener.onComplete();
+    } else if (i == mExeMap.size()) {
+      startRunningFlow();
+    }
+  }
+}

+ 93 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpDirInfoThread.java

@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.FtpUrlEntity;
+import com.arialyy.aria.core.common.AbsFtpInfoThread;
+import com.arialyy.aria.core.common.CompleteInfo;
+import com.arialyy.aria.core.common.OnFileInfoCallback;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadGroupEntity;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.util.CommonUtil;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import org.apache.commons.net.ftp.FTPFile;
+
+/**
+ * Created by Aria.Lao on 2017/7/25.
+ * 获取ftp文件夹信息
+ */
+class FtpDirInfoThread extends AbsFtpInfoThread<DownloadGroupEntity, DownloadGroupTaskEntity> {
+
+  FtpDirInfoThread(DownloadGroupTaskEntity taskEntity, OnFileInfoCallback callback) {
+    super(taskEntity, callback);
+  }
+
+  @Override protected String setRemotePath() {
+    return mTaskEntity.getUrlEntity().remotePath;
+  }
+
+  @Override protected void handleFile(String remotePath, FTPFile ftpFile) {
+    super.handleFile(remotePath, ftpFile);
+    addEntity(remotePath, ftpFile);
+  }
+
+  @Override protected void onPreComplete(int code) {
+    super.onPreComplete(code);
+    mEntity.setFileSize(mSize);
+    mCallback.onComplete(mEntity.getKey(), new CompleteInfo(code));
+  }
+
+  /**
+   * FTP文件夹的子任务实体 在这生成
+   */
+  private void addEntity(String remotePath, FTPFile ftpFile) {
+    final FtpUrlEntity urlEntity = mTaskEntity.getUrlEntity().clone();
+    DownloadEntity entity = new DownloadEntity();
+    entity.setUrl(
+        urlEntity.protocol + "://" + urlEntity.hostName + ":" + urlEntity.port + "/" + remotePath);
+    entity.setDownloadPath(mEntity.getDirPath() + "/" + remotePath);
+    int lastIndex = remotePath.lastIndexOf("/");
+    String fileName = lastIndex < 0 ? CommonUtil.keyToHashKey(remotePath)
+        : remotePath.substring(lastIndex + 1, remotePath.length());
+    entity.setFileName(new String(fileName.getBytes(), Charset.forName(mTaskEntity.getCharSet())));
+    entity.setGroupName(mEntity.getGroupName());
+    entity.setGroupChild(true);
+    entity.setFileSize(ftpFile.getSize());
+    entity.insert();
+
+    DownloadTaskEntity taskEntity = new DownloadTaskEntity();
+    taskEntity.setKey(entity.getDownloadPath());
+    taskEntity.setUrl(entity.getUrl());
+    taskEntity.setEntity(entity);
+    taskEntity.setGroupTask(true);
+    taskEntity.setGroupName(mEntity.getGroupName());
+    taskEntity.setRequestType(AbsTaskEntity.D_FTP);
+    urlEntity.url = entity.getUrl();
+    urlEntity.remotePath = remotePath;
+    taskEntity.setUrlEntity(urlEntity);
+    taskEntity.insert();
+
+    if (mEntity.getUrls() == null) {
+      mEntity.setUrls(new ArrayList<String>());
+    }
+    mEntity.getSubEntities().add(entity);
+    mTaskEntity.getSubTaskEntities().add(taskEntity);
+  }
+}

+ 46 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpFileInfoThread.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.common.AbsFtpInfoThread;
+import com.arialyy.aria.core.common.CompleteInfo;
+import com.arialyy.aria.core.common.OnFileInfoCallback;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+
+/**
+ * Created by Aria.Lao on 2017/7/25.
+ * 获取ftp文件信息
+ */
+class FtpFileInfoThread extends AbsFtpInfoThread<DownloadEntity, DownloadTaskEntity> {
+
+  FtpFileInfoThread(DownloadTaskEntity taskEntity, OnFileInfoCallback callback) {
+    super(taskEntity, callback);
+  }
+
+  @Override protected String setRemotePath() {
+    return mTaskEntity.getUrlEntity().remotePath;
+  }
+
+  @Override protected void onPreComplete(int code) {
+    super.onPreComplete(code);
+    if (mSize != mTaskEntity.getEntity().getFileSize()) {
+      mTaskEntity.setNewTask(true);
+    }
+    mEntity.setFileSize(mSize);
+    mCallback.onComplete(mEntity.getUrl(), new CompleteInfo(code));
+  }
+}

+ 159 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/FtpThreadTask.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.AbsFtpThreadTask;
+import com.arialyy.aria.core.common.StateConstance;
+import com.arialyy.aria.core.common.SubThreadConfig;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.BufferedRandomAccessFile;
+import java.io.IOException;
+import java.io.InputStream;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPReply;
+
+/**
+ * Created by Aria.Lao on 2017/7/24.
+ * Ftp下载任务
+ */
+class FtpThreadTask extends AbsFtpThreadTask<DownloadEntity, DownloadTaskEntity> {
+  private final String TAG = "FtpThreadTask";
+  private boolean isOpenDynamicFile;
+  /**
+   * 2M的动态长度
+   */
+  private final int LEN_INTERVAL = 1024 * 1024 * 2;
+
+  FtpThreadTask(StateConstance constance, IDownloadListener listener,
+      SubThreadConfig<DownloadTaskEntity> downloadInfo) {
+    super(constance, listener, downloadInfo);
+    AriaManager manager = AriaManager.getInstance(AriaManager.APP);
+    mConnectTimeOut = manager.getDownloadConfig().getConnectTimeOut();
+    mReadTimeOut = manager.getDownloadConfig().getIOTimeOut();
+    mBufSize = manager.getDownloadConfig().getBuffSize();
+    isNotNetRetry = manager.getDownloadConfig().isNotNetRetry();
+    isOpenDynamicFile = STATE.TASK_RECORD.isOpenDynamicFile;
+    setMaxSpeed(manager.getDownloadConfig().getMaxSpeed());
+  }
+
+  @Override public void run() {
+    //当前子线程的下载位置
+    mChildCurrentLocation = mConfig.START_LOCATION;
+    FTPClient client = null;
+    InputStream is = null;
+    BufferedRandomAccessFile file = null;
+    try {
+      ALog.d(TAG, "任务【"
+          + mConfig.TEMP_FILE.getName()
+          + "】线程__"
+          + mConfig.THREAD_ID
+          + "__开始下载【开始位置 : "
+          + mConfig.START_LOCATION
+          + ",结束位置:"
+          + mConfig.END_LOCATION
+          + "】");
+      client = createClient();
+      if (client == null) return;
+      if (mConfig.START_LOCATION > 0) {
+        client.setRestartOffset(mConfig.START_LOCATION);
+      }
+      //发送第二次指令时,还需要再做一次判断
+      int reply = client.getReplyCode();
+      if (!FTPReply.isPositivePreliminary(reply) && reply != FTPReply.COMMAND_OK) {
+        fail(mChildCurrentLocation, "获取文件信息错误,错误码为:" + reply + ",msg:" + client.getReplyString(),
+            null);
+        client.disconnect();
+        return;
+      }
+      String remotePath =
+          new String(mTaskEntity.getUrlEntity().remotePath.getBytes(charSet), SERVER_CHARSET);
+      ALog.i(TAG, "remotePath【" + remotePath + "】");
+      is = client.retrieveFileStream(remotePath);
+      reply = client.getReplyCode();
+      if (!FTPReply.isPositivePreliminary(reply)) {
+        fail(mChildCurrentLocation, "获取流失败,错误码为:" + reply + ",msg:" + client.getReplyString(),
+            null);
+        client.disconnect();
+        return;
+      }
+
+      file = new BufferedRandomAccessFile(mConfig.TEMP_FILE, "rwd", mBufSize);
+      file.seek(mConfig.START_LOCATION);
+      byte[] buffer = new byte[mBufSize];
+      int len;
+
+      while ((len = is.read(buffer)) != -1) {
+        if (STATE.isCancel || STATE.isStop) {
+          break;
+        }
+        if (mSleepTime > 0) Thread.sleep(mSleepTime);
+        if (isOpenDynamicFile) {
+          file.setLength(
+              STATE.CURRENT_LOCATION + LEN_INTERVAL < mEntity.getFileSize() ? STATE.CURRENT_LOCATION
+                  + LEN_INTERVAL : mEntity.getFileSize());
+        }
+        if (mChildCurrentLocation + len >= mConfig.END_LOCATION) {
+          len = (int) (mConfig.END_LOCATION - mChildCurrentLocation);
+          file.write(buffer, 0, len);
+          progress(len);
+          break;
+        } else {
+          file.write(buffer, 0, len);
+          progress(len);
+        }
+      }
+      if (STATE.isCancel || STATE.isStop) return;
+      ALog.i(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】线程__" + mConfig.THREAD_ID + "__下载完毕");
+      writeConfig(true, 1);
+      STATE.COMPLETE_THREAD_NUM++;
+      if (STATE.isComplete()) {
+        STATE.TASK_RECORD.deleteData();
+        STATE.isRunning = false;
+        mListener.onComplete();
+      }
+      if (STATE.isFail()) {
+        STATE.isRunning = false;
+        mListener.onFail(false);
+      }
+    } catch (IOException e) {
+      fail(mChildCurrentLocation, "下载失败【" + mConfig.URL + "】", e);
+    } catch (Exception e) {
+      fail(mChildCurrentLocation, "获取流失败", e);
+    } finally {
+      try {
+        if (file != null) {
+          file.close();
+        }
+        if (is != null) {
+          is.close();
+        }
+        if (client != null && client.isConnected()) {
+          client.disconnect();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  @Override protected String getTaskType() {
+    return "FTP_DOWNLOAD";
+  }
+}

+ 245 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/HttpFileInfoThread.java

@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.CompleteInfo;
+import com.arialyy.aria.core.common.OnFileInfoCallback;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CheckUtil;
+import com.arialyy.aria.util.CommonUtil;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLDecoder;
+
+/**
+ * 下载文件信息获取
+ */
+class HttpFileInfoThread implements Runnable {
+  private final String TAG = "HttpFileInfoThread";
+  private DownloadEntity mEntity;
+  private DownloadTaskEntity mTaskEntity;
+  private int mConnectTimeOut;
+  private OnFileInfoCallback onFileInfoListener;
+
+  HttpFileInfoThread(DownloadTaskEntity taskEntity, OnFileInfoCallback callback) {
+    this.mTaskEntity = taskEntity;
+    mEntity = taskEntity.getEntity();
+    mConnectTimeOut =
+        AriaManager.getInstance(AriaManager.APP).getDownloadConfig().getConnectTimeOut();
+    onFileInfoListener = callback;
+  }
+
+  @Override public void run() {
+    HttpURLConnection conn = null;
+    try {
+      URL url = new URL(CommonUtil.convertUrl(mEntity.getUrl()));
+      conn = ConnectionHelp.handleConnection(url);
+      conn = ConnectionHelp.setConnectParam(mTaskEntity, conn);
+      conn.setRequestProperty("Range", "bytes=" + 0 + "-");
+      conn.setConnectTimeout(mConnectTimeOut);
+      //conn.setChunkedStreamingMode(0);
+      conn.connect();
+      handleConnect(conn);
+    } catch (IOException e) {
+      failDownload("下载失败【downloadUrl:"
+          + mEntity.getUrl()
+          + "】\n【filePath:"
+          + mEntity.getDownloadPath()
+          + "】\n"
+          + ALog.getExceptionString(e), true);
+    } finally {
+      if (conn != null) {
+        conn.disconnect();
+      }
+    }
+  }
+
+  private void handleConnect(HttpURLConnection conn) throws IOException {
+    long len = conn.getContentLength();
+    if (len < 0) {
+      String temp = conn.getHeaderField("Content-Length");
+      len = TextUtils.isEmpty(temp) ? -1 : Long.parseLong(temp);
+      // 某些服务,如果设置了conn.setRequestProperty("Range", "bytes=" + 0 + "-");
+      // 会返回 Content-Range: bytes 0-225427911/225427913
+      if (len < 0) {
+        temp = conn.getHeaderField("Content-Range");
+        if (TextUtils.isEmpty(temp)) {
+          len = -1;
+        } else {
+          int start = temp.indexOf("/");
+          len = Long.parseLong(temp.substring(start + 1, temp.length()));
+        }
+      }
+    }
+    int code = conn.getResponseCode();
+    boolean end = false;
+    if (TextUtils.isEmpty(mEntity.getMd5Code())) {
+      String md5Code = conn.getHeaderField("Content-MD5");
+      mEntity.setMd5Code(md5Code);
+    }
+
+    boolean isChunked = false;
+    final String str = conn.getHeaderField("Transfer-Encoding");
+    if (!TextUtils.isEmpty(str) && str.equals("chunked")) {
+      isChunked = true;
+    }
+    //Map<String, List<String>> headers = conn.getHeaderFields();
+    String disposition = conn.getHeaderField("Content-Disposition");
+    if (mTaskEntity.isUseServerFileName() && !TextUtils.isEmpty(disposition)) {
+      mEntity.setDisposition(CommonUtil.encryptBASE64(disposition));
+      if (disposition.contains(";")) {
+        String[] infos = disposition.split(";");
+        for (String info : infos) {
+          if (info.startsWith("filename") && info.contains("=")) {
+            String[] temp = info.split("=");
+            if (temp.length > 1) {
+              String newName = URLDecoder.decode(temp[1], "utf-8");
+              mEntity.setServerFileName(newName);
+              fileRename(newName);
+              break;
+            }
+          }
+        }
+      }
+    }
+
+    mTaskEntity.setCode(code);
+    if (code == HttpURLConnection.HTTP_PARTIAL) {
+      if (!checkLen(len) && !isChunked) {
+        return;
+      }
+      mEntity.setFileSize(len);
+      mTaskEntity.setSupportBP(true);
+      end = true;
+    } else if (code == HttpURLConnection.HTTP_OK) {
+      if (conn.getHeaderField("Content-Type").equals("text/html")) {
+        BufferedReader reader =
+            new BufferedReader(new InputStreamReader(ConnectionHelp.convertInputStream(conn)));
+        StringBuilder sb = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+          sb.append(line);
+        }
+        reader.close();
+        handleUrlReTurn(conn, CommonUtil.getWindowReplaceUrl(sb.toString()));
+        return;
+      } else if (!checkLen(len) && !isChunked) {
+        return;
+      }
+      mEntity.setFileSize(len);
+      mTaskEntity.setNewTask(true);
+      mTaskEntity.setSupportBP(false);
+      end = true;
+    } else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
+      failDownload("任务【" + mEntity.getUrl() + "】下载失败,错误码:404", true);
+    } else if (code == HttpURLConnection.HTTP_MOVED_TEMP
+        || code == HttpURLConnection.HTTP_MOVED_PERM
+        || code == 307
+        || code == HttpURLConnection.HTTP_SEE_OTHER) {
+      handleUrlReTurn(conn, conn.getHeaderField("Location"));
+    } else {
+      failDownload("任务【" + mEntity.getUrl() + "】下载失败,错误码:" + code, true);
+    }
+    if (end) {
+      mTaskEntity.setChunked(isChunked);
+      mTaskEntity.update();
+      if (onFileInfoListener != null) {
+        CompleteInfo info = new CompleteInfo(code);
+        onFileInfoListener.onComplete(mEntity.getUrl(), info);
+      }
+    }
+  }
+
+  /**
+   * 重命名文件
+   */
+  private void fileRename(String newName) {
+    if (TextUtils.isEmpty(newName)) {
+      ALog.w(TAG, "重命名失败【服务器返回的文件名为空】");
+      return;
+    }
+    File oldFile = new File(mEntity.getDownloadPath());
+    String newPath = oldFile.getParent() + "/" + newName;
+    if (oldFile.exists()) {
+      oldFile.renameTo(new File(newPath));
+    }
+    mEntity.setFileName(newName);
+    mEntity.setDownloadPath(newPath);
+    mTaskEntity.setKey(newPath);
+  }
+
+  /**
+   * 处理30x跳转
+   */
+  private void handleUrlReTurn(HttpURLConnection conn, String newUrl) throws IOException {
+    ALog.d(TAG, "30x跳转,新url为【" + newUrl + "】");
+    if (TextUtils.isEmpty(newUrl) || newUrl.equalsIgnoreCase("null") || !newUrl.startsWith(
+        "http")) {
+      if (onFileInfoListener != null) {
+        onFileInfoListener.onFail(mEntity.getUrl(), "获取重定向链接失败", false);
+      }
+      return;
+    }
+    if (!CheckUtil.checkUrl(newUrl)) {
+      failDownload("下载失败,重定向url错误", false);
+      return;
+    }
+    mTaskEntity.setRedirectUrl(newUrl);
+    mEntity.setRedirect(true);
+    mEntity.setRedirectUrl(newUrl);
+    String cookies = conn.getHeaderField("Set-Cookie");
+    conn = (HttpURLConnection) new URL(newUrl).openConnection();
+    conn = ConnectionHelp.setConnectParam(mTaskEntity, conn);
+    conn.setRequestProperty("Cookie", cookies);
+    conn.setRequestProperty("Range", "bytes=" + 0 + "-");
+    conn.setConnectTimeout(mConnectTimeOut);
+    conn.connect();
+    handleConnect(conn);
+    conn.disconnect();
+  }
+
+  /**
+   * 检查长度是否合法,并且检查新获取的文件长度是否和数据库的文件长度一直,如果不一致,则表示该任务为新任务
+   *
+   * @param len 从服务器获取的文件长度
+   * @return {@code true}合法
+   */
+  private boolean checkLen(long len) {
+    if (len != mEntity.getFileSize()) {
+      mTaskEntity.setNewTask(true);
+    }
+    if (len < 0) {
+      failDownload("任务【" + mEntity.getUrl() + "】下载失败,文件长度小于0", true);
+      return false;
+    }
+    return true;
+  }
+
+  private void failDownload(String errorMsg, boolean needRetry) {
+    ALog.e(TAG, errorMsg);
+    if (onFileInfoListener != null) {
+      onFileInfoListener.onFail(mEntity.getUrl(), errorMsg, needRetry);
+    }
+  }
+}

+ 195 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/HttpThreadTask.java

@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.common.AbsThreadTask;
+import com.arialyy.aria.core.common.StateConstance;
+import com.arialyy.aria.core.common.SubThreadConfig;
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.BufferedRandomAccessFile;
+import com.arialyy.aria.util.CommonUtil;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Created by lyy on 2017/1/18.
+ * 下载线程
+ */
+final class HttpThreadTask extends AbsThreadTask<DownloadEntity, DownloadTaskEntity> {
+  private final String TAG = "HttpThreadTask";
+  /**
+   * 2M的动态长度
+   */
+  private final int LEN_INTERVAL = 1024 * 1024 * 2;
+  private boolean isOpenDynamicFile;
+
+  HttpThreadTask(StateConstance constance, IDownloadListener listener,
+      SubThreadConfig<DownloadTaskEntity> downloadInfo) {
+    super(constance, listener, downloadInfo);
+    AriaManager manager = AriaManager.getInstance(AriaManager.APP);
+    mConnectTimeOut = manager.getDownloadConfig().getConnectTimeOut();
+    mReadTimeOut = manager.getDownloadConfig().getIOTimeOut();
+    mBufSize = manager.getDownloadConfig().getBuffSize();
+    isNotNetRetry = manager.getDownloadConfig().isNotNetRetry();
+    isOpenDynamicFile = STATE.TASK_RECORD.isOpenDynamicFile;
+    setMaxSpeed(manager.getDownloadConfig().getMaxSpeed());
+  }
+
+  @Override public void run() {
+    HttpURLConnection conn = null;
+    BufferedInputStream is = null;
+    BufferedRandomAccessFile file = null;
+    //当前子线程的下载位置
+    mChildCurrentLocation = mConfig.START_LOCATION;
+    try {
+      URL url = new URL(CommonUtil.convertUrl(mConfig.URL));
+      conn = ConnectionHelp.handleConnection(url);
+      if (mConfig.SUPPORT_BP) {
+        ALog.d(TAG, "任务【"
+            + mConfig.TEMP_FILE.getName()
+            + "】线程__"
+            + mConfig.THREAD_ID
+            + "__开始下载【开始位置 : "
+            + mConfig.START_LOCATION
+            + ",结束位置:"
+            + mConfig.END_LOCATION
+            + "】");
+        //在头里面请求下载开始位置和结束位置
+        conn.setRequestProperty("Range",
+            "bytes=" + mConfig.START_LOCATION + "-" + (mConfig.END_LOCATION - 1));
+      } else {
+        ALog.w(TAG, "该下载不支持断点");
+      }
+      conn = ConnectionHelp.setConnectParam(mConfig.TASK_ENTITY, conn);
+      conn.setConnectTimeout(mConnectTimeOut);
+      conn.setReadTimeout(mReadTimeOut);  //设置读取流的等待时间,必须设置该参数
+
+      is = new BufferedInputStream(ConnectionHelp.convertInputStream(conn));
+      //创建可设置位置的文件
+      file = new BufferedRandomAccessFile(mConfig.TEMP_FILE, "rwd", mBufSize);
+      //设置每条线程写入文件的位置
+      file.seek(mConfig.START_LOCATION);
+
+      if (mTaskEntity.isChunked()) {
+        readChunk(is, file);
+      } else {
+        readNormal(is, file);
+      }
+
+      if (STATE.isCancel || STATE.isStop) {
+        return;
+      }
+      handleComplete();
+    } catch (MalformedURLException e) {
+      fail(mChildCurrentLocation, "下载链接异常", e);
+    } catch (IOException e) {
+      fail(mChildCurrentLocation, "下载失败【" + mConfig.URL + "】", e);
+    } catch (Exception e) {
+      fail(mChildCurrentLocation, "获取流失败", e);
+    } finally {
+      try {
+        if (file != null) {
+          file.close();
+        }
+        if (is != null) {
+          is.close();
+        }
+        if (conn != null) {
+          conn.disconnect();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  /**
+   * 读取chunk模式的文件流
+   *
+   * @deprecated 暂时先这样处理,无chun
+   */
+  private void readChunk(InputStream is, BufferedRandomAccessFile file)
+      throws IOException, InterruptedException {
+    readNormal(is, file);
+  }
+
+  /**
+   * 读取普通的文件流
+   */
+  private void readNormal(InputStream is, BufferedRandomAccessFile file)
+      throws IOException, InterruptedException {
+    byte[] buffer = new byte[mBufSize];
+    int len;
+    while ((len = is.read(buffer)) != -1) {
+      if (STATE.isCancel || STATE.isStop) {
+        break;
+      }
+      if (mSleepTime > 0) {
+        Thread.sleep(mSleepTime);
+      }
+      if (isOpenDynamicFile) {
+        file.setLength(
+            STATE.CURRENT_LOCATION + LEN_INTERVAL < mEntity.getFileSize() ? STATE.CURRENT_LOCATION
+                + LEN_INTERVAL : mEntity.getFileSize());
+      }
+      file.write(buffer, 0, len);
+      progress(len);
+    }
+  }
+
+  /**
+   * 处理完成配置文件的更新或事件回调
+   *
+   * @throws IOException
+   */
+  private void handleComplete() throws IOException {
+    //支持断点的处理
+    if (mConfig.SUPPORT_BP) {
+      if (mChildCurrentLocation == mConfig.END_LOCATION) {
+        ALog.i(TAG, "任务【" + mConfig.TEMP_FILE.getName() + "】线程__" + mConfig.THREAD_ID + "__下载完毕");
+        writeConfig(true, 1);
+        STATE.COMPLETE_THREAD_NUM++;
+        if (STATE.isComplete()) {
+          STATE.TASK_RECORD.deleteData();
+          STATE.isRunning = false;
+          mListener.onComplete();
+        }
+      } else {
+        STATE.FAIL_NUM++;
+      }
+      if (STATE.isFail()) {
+        STATE.isRunning = false;
+        mListener.onFail(false);
+      }
+    } else {
+      ALog.i(TAG, "任务下载完成");
+      STATE.isRunning = false;
+      mListener.onComplete();
+    }
+  }
+
+  @Override protected String getTaskType() {
+    return "HTTP_DOWNLOAD";
+  }
+}

+ 68 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/IDownloadGroupListener.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+
+/**
+ * Created by Aria.Lao on 2017/7/20.
+ * 下载任务组事件
+ */
+public interface IDownloadGroupListener extends IDownloadListener {
+
+  /**
+   * 子任务预处理
+   */
+  void onSubPre(DownloadEntity subEntity);
+
+  /**
+   * 子任务支持断点回调
+   *
+   * @param support true,支持;false 不支持
+   */
+  void supportBreakpoint(boolean support, DownloadEntity subEntity);
+
+  /**
+   * 子任务开始下载\恢复下载
+   */
+  void onSubStart(DownloadEntity subEntity);
+
+  /**
+   * 子任务停止下载
+   */
+  void onSubStop(DownloadEntity subEntity);
+
+  /**
+   * 子任务下载完成
+   */
+  void onSubComplete(DownloadEntity subEntity);
+
+  /**
+   * 子任务下载失败
+   */
+  void onSubFail(DownloadEntity subEntity);
+
+  /**
+   * 子任务取消下载
+   */
+  void onSubCancel(DownloadEntity subEntity);
+
+  /**
+   * 子任务执行中
+   */
+  void onSubRunning(DownloadEntity subEntity);
+}

+ 133 - 0
Aria/src/main/java/com/arialyy/aria/core/download/downloader/SimpleDownloadUtil.java

@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.downloader;
+
+import com.arialyy.aria.core.common.CompleteInfo;
+import com.arialyy.aria.core.common.IUtil;
+import com.arialyy.aria.core.common.OnFileInfoCallback;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.core.inf.IDownloadListener;
+import com.arialyy.aria.core.inf.IEntity;
+import com.arialyy.aria.util.ErrorHelp;
+
+/**
+ * Created by lyy on 2015/8/25.
+ * D_HTTP\FTP单任务下载工具
+ */
+public class SimpleDownloadUtil implements IUtil, Runnable {
+  private String TAG = "SimpleDownloadUtil";
+  private IDownloadListener mListener;
+  private Downloader mDownloader;
+  private DownloadTaskEntity mTaskEntity;
+
+  public SimpleDownloadUtil(DownloadTaskEntity entity, IDownloadListener downloadListener) {
+    mTaskEntity = entity;
+    mListener = downloadListener;
+    mDownloader = new Downloader(downloadListener, entity);
+  }
+
+  @Override public long getFileSize() {
+    return mDownloader.getFileSize();
+  }
+
+  /**
+   * 获取当前下载位置
+   */
+  @Override public long getCurrentLocation() {
+    return mDownloader.getCurrentLocation();
+  }
+
+  @Override public boolean isRunning() {
+    return mDownloader.isRunning();
+  }
+
+  /**
+   * 取消下载
+   */
+  @Override public void cancel() {
+    mDownloader.cancel();
+  }
+
+  /**
+   * 停止下载
+   */
+  @Override public void stop() {
+    mDownloader.stop();
+  }
+
+  /**
+   * 多线程断点续传下载文件,开始下载
+   */
+  @Override public void start() {
+    new Thread(this).start();
+  }
+
+  @Override public void resume() {
+    start();
+  }
+
+  public void setMaxSpeed(double maxSpeed) {
+    mDownloader.setMaxSpeed(maxSpeed);
+  }
+
+  private void failDownload(String msg, boolean needRetry) {
+    mListener.onFail(needRetry);
+    ErrorHelp.saveError(TAG, msg, "");
+  }
+
+  @Override public void run() {
+    mListener.onPre();
+    if (mTaskEntity.getEntity().getFileSize() <= 1
+        || mTaskEntity.isRefreshInfo()
+        || mTaskEntity.getRequestType() == AbsTaskEntity.D_FTP
+        || mTaskEntity.getState() == IEntity.STATE_FAIL) {
+      new Thread(createInfoThread()).start();
+    } else {
+      mDownloader.start();
+    }
+  }
+
+  /**
+   * 通过链接类型创建不同的获取文件信息的线程
+   */
+  private Runnable createInfoThread() {
+    switch (mTaskEntity.getRequestType()) {
+      case AbsTaskEntity.D_FTP:
+        return new FtpFileInfoThread(mTaskEntity, new OnFileInfoCallback() {
+          @Override public void onComplete(String url, CompleteInfo info) {
+            mDownloader.start();
+          }
+
+          @Override public void onFail(String url, String errorMsg, boolean needRetry) {
+            failDownload(errorMsg, needRetry);
+          }
+        });
+      case AbsTaskEntity.D_HTTP:
+        return new HttpFileInfoThread(mTaskEntity, new OnFileInfoCallback() {
+          @Override public void onComplete(String url, CompleteInfo info) {
+            mDownloader.start();
+          }
+
+          @Override public void onFail(String url, String errorMsg, boolean needRetry) {
+            failDownload(errorMsg, needRetry);
+          }
+        });
+    }
+    return null;
+  }
+}

+ 44 - 0
Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DGEWrapper.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.wrapper;
+
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadGroupEntity;
+import com.arialyy.aria.orm.AbsWrapper;
+import com.arialyy.aria.orm.annotation.Many;
+import com.arialyy.aria.orm.annotation.One;
+import com.arialyy.aria.orm.annotation.Wrapper;
+import java.util.List;
+
+/**
+ * Created by laoyuyu on 2018/3/30.
+ * 任务组实体和子任务实体的关系
+ */
+@Wrapper
+public class DGEWrapper extends AbsWrapper {
+
+  @One
+  public DownloadGroupEntity groupEntity;
+
+  @Many(parentColumn = "groupName", entityColumn = "groupName")
+  public List<DownloadEntity> subEntity;
+
+  @Override protected void handleConvert() {
+    if (subEntity != null && !subEntity.isEmpty()) {
+      groupEntity.setSubEntities(subEntity);
+    }
+  }
+}

+ 44 - 0
Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DGSTEWrapper.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.wrapper;
+
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.orm.AbsWrapper;
+import com.arialyy.aria.orm.annotation.Many;
+import com.arialyy.aria.orm.annotation.One;
+import com.arialyy.aria.orm.annotation.Wrapper;
+import java.util.List;
+
+/**
+ * Created by laoyuyu on 2018/4/11.
+ * 任务组任务实体和任务组任务实体的子任务实体对应关系
+ */
+@Wrapper
+public class DGSTEWrapper extends AbsWrapper {
+
+  @One
+  public DownloadGroupTaskEntity dgTaskEntity;
+
+  @Many(parentColumn = "key", entityColumn = "groupName")
+  public List<DownloadTaskEntity> subTaskEntity;
+
+  @Override protected void handleConvert() {
+    if (subTaskEntity != null && !subTaskEntity.isEmpty()) {
+      dgTaskEntity.setSubTaskEntities(subTaskEntity);
+    }
+  }
+}

+ 65 - 0
Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DGTEWrapper.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.wrapper;
+
+import com.arialyy.aria.core.download.DownloadGroupEntity;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.inf.AbsTaskEntity;
+import com.arialyy.aria.orm.AbsWrapper;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.orm.annotation.Many;
+import com.arialyy.aria.orm.annotation.One;
+import com.arialyy.aria.orm.annotation.Wrapper;
+import com.arialyy.aria.util.CommonUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by laoyuyu on 2018/3/30.
+ * 任务组实体和任务组任务实体的关系
+ */
+@Wrapper
+public class DGTEWrapper extends AbsWrapper {
+
+  @One
+  public DownloadGroupEntity entity;
+
+  @Many(parentColumn = "groupName", entityColumn = "key")
+  private List<DownloadGroupTaskEntity> taskEntitys;
+
+  public DownloadGroupTaskEntity taskEntity;
+
+  @Override protected void handleConvert() {
+    taskEntity = (taskEntitys == null || taskEntitys.isEmpty()) ? null : taskEntitys.get(0);
+    if (taskEntity != null) {
+      taskEntity.setEntity(entity);
+      List<DTEWrapper> subWrappers =
+          DbEntity.findRelationData(DTEWrapper.class, "DownloadTaskEntity.groupName=?",
+              taskEntity.getKey());
+      if (subWrappers != null && !subWrappers.isEmpty()) {
+        List<DownloadTaskEntity> temp = new ArrayList<>();
+        for (DTEWrapper dw : subWrappers) {
+          if (dw.taskEntity.getRequestType() == AbsTaskEntity.D_FTP) {
+            dw.taskEntity.setUrlEntity(CommonUtil.getFtpUrlInfo(dw.taskEntity.getUrl()));
+          }
+          temp.add(dw.taskEntity);
+        }
+        taskEntity.setSubTaskEntities(temp);
+      }
+    }
+  }
+}

+ 46 - 0
Aria/src/main/java/com/arialyy/aria/core/download/wrapper/DTEWrapper.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.download.wrapper;
+
+import com.arialyy.aria.core.download.DownloadEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.orm.AbsWrapper;
+import com.arialyy.aria.orm.annotation.Many;
+import com.arialyy.aria.orm.annotation.One;
+import com.arialyy.aria.orm.annotation.Wrapper;
+import java.util.List;
+
+/**
+ * Created by laoyuyu on 2018/3/30.
+ */
+@Wrapper
+public class DTEWrapper extends AbsWrapper {
+
+  @One
+  public DownloadEntity entity;
+
+  @Many(parentColumn = "downloadPath", entityColumn = "key")
+  private List<DownloadTaskEntity> taskEntitys = null;
+
+  public DownloadTaskEntity taskEntity;
+
+  @Override public void handleConvert() {
+    taskEntity = (taskEntitys == null || taskEntitys.isEmpty()) ? null : taskEntitys.get(0);
+    if (taskEntity != null) {
+      taskEntity.setEntity(entity);
+    }
+  }
+}

+ 205 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsEntity.java

@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.orm.annotation.Ignore;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ */
+public abstract class AbsEntity extends DbEntity implements IEntity, Parcelable {
+  /**
+   * 速度
+   */
+  @Ignore private long speed = 0;
+  /**
+   * 单位转换后的速度
+   */
+  @Ignore private String convertSpeed;
+  /**
+   * 下载失败计数,每次开始都重置为0
+   */
+  @Ignore private int failNum = 0;
+
+  /**
+   * 扩展字段
+   */
+  private String str;
+  /**
+   * 文件大小
+   */
+  private long fileSize = 1;
+  /**
+   * 转换后的文件大小
+   */
+  private String convertFileSize;
+
+  private int state = STATE_WAIT;
+  /**
+   * 当前下载进度
+   */
+  private long currentProgress = 0;
+  /**
+   * 完成时间
+   */
+  private long completeTime;
+
+  /**
+   * 进度百分比
+   */
+  @Ignore private int percent;
+
+  private boolean isComplete = false;
+
+  public boolean isComplete() {
+    return isComplete;
+  }
+
+  public void setComplete(boolean complete) {
+    isComplete = complete;
+  }
+
+  public String getConvertFileSize() {
+    return convertFileSize;
+  }
+
+  public void setConvertFileSize(String convertFileSize) {
+    this.convertFileSize = convertFileSize;
+  }
+
+  public int getFailNum() {
+    return failNum;
+  }
+
+  public void setFailNum(int failNum) {
+    this.failNum = failNum;
+  }
+
+  public long getSpeed() {
+    return speed;
+  }
+
+  public void setSpeed(long speed) {
+    this.speed = speed;
+  }
+
+  public String getConvertSpeed() {
+    return convertSpeed;
+  }
+
+  public void setConvertSpeed(String convertSpeed) {
+    this.convertSpeed = convertSpeed;
+  }
+
+  public String getStr() {
+    return str;
+  }
+
+  public void setStr(String str) {
+    this.str = str;
+  }
+
+  public long getFileSize() {
+    return fileSize;
+  }
+
+  public void setFileSize(long fileSize) {
+    this.fileSize = fileSize;
+  }
+
+  public int getState() {
+    return state;
+  }
+
+  public void setState(int state) {
+    this.state = state;
+  }
+
+  public long getCurrentProgress() {
+    return currentProgress;
+  }
+
+  public void setCurrentProgress(long currentProgress) {
+    this.currentProgress = currentProgress;
+  }
+
+  public long getCompleteTime() {
+    return completeTime;
+  }
+
+  public void setCompleteTime(long completeTime) {
+    this.completeTime = completeTime;
+  }
+
+  public int getPercent() {
+    return percent;
+  }
+
+  public void setPercent(int percent) {
+    this.percent = percent;
+  }
+
+  /**
+   * 实体唯一标识符
+   */
+  public abstract String getKey();
+
+  /**
+   * 实体驱动的下载任务类型
+   *
+   * @return {@link AbsTaskEntity#D_FTP}、{@link AbsTaskEntity#D_FTP_DIR}、{@link
+   * AbsTaskEntity#U_HTTP}...
+   */
+  public abstract int getTaskType();
+
+  public AbsEntity() {
+  }
+
+  @Override public int describeContents() {
+    return 0;
+  }
+
+  @Override public void writeToParcel(Parcel dest, int flags) {
+    dest.writeLong(this.speed);
+    dest.writeString(this.convertSpeed);
+    dest.writeInt(this.failNum);
+    dest.writeString(this.str);
+    dest.writeLong(this.fileSize);
+    dest.writeString(this.convertFileSize);
+    dest.writeInt(this.state);
+    dest.writeLong(this.currentProgress);
+    dest.writeLong(this.completeTime);
+    dest.writeByte(this.isComplete ? (byte) 1 : (byte) 0);
+    dest.writeInt(this.percent);
+  }
+
+  protected AbsEntity(Parcel in) {
+    this.speed = in.readLong();
+    this.convertSpeed = in.readString();
+    this.failNum = in.readInt();
+    this.str = in.readString();
+    this.fileSize = in.readLong();
+    this.convertFileSize = in.readString();
+    this.state = in.readInt();
+    this.currentProgress = in.readLong();
+    this.completeTime = in.readLong();
+    this.isComplete = in.readByte() != 0;
+    this.percent = in.readInt();
+  }
+}

+ 98 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsGroupEntity.java

@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.arialyy.aria.orm.annotation.Primary;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by AriaL on 2017/6/3.
+ */
+public abstract class AbsGroupEntity extends AbsEntity implements Parcelable {
+  /**
+   * 组名,组名为任务地址相加的url的Md5
+   */
+  @Primary protected String groupName;
+
+  /**
+   * 任务组别名
+   */
+  private String alias;
+
+  /**
+   * 任务组下载文件的文件夹地址
+   */
+  private String dirPath;
+
+  /**
+   * 子任务url地址
+   */
+  private List<String> urls = new ArrayList<>();
+
+  public String getDirPath() {
+    return dirPath;
+  }
+
+  public void setDirPath(String dirPath) {
+    this.dirPath = dirPath;
+  }
+
+  public List<String> getUrls() {
+    return urls;
+  }
+
+  public void setUrls(List<String> urls) {
+    this.urls = urls;
+  }
+
+  public String getGroupName() {
+    return groupName;
+  }
+
+  public String getAlias() {
+    return alias;
+  }
+
+  @Override public String getKey() {
+    return groupName;
+  }
+
+  public void setAlias(String alias) {
+    this.alias = alias;
+  }
+
+  public AbsGroupEntity() {
+  }
+
+  @Override public int describeContents() {
+    return 0;
+  }
+
+  @Override public void writeToParcel(Parcel dest, int flags) {
+    super.writeToParcel(dest, flags);
+    dest.writeString(this.groupName);
+    dest.writeString(this.alias);
+  }
+
+  protected AbsGroupEntity(Parcel in) {
+    super(in);
+    this.groupName = in.readString();
+    this.alias = in.readString();
+  }
+}

+ 65 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsGroupTask.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import com.arialyy.aria.core.download.downloader.AbsGroupUtil;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ * 任务组任务抽象类
+ */
+public abstract class AbsGroupTask<TASK_ENTITY extends AbsGroupTaskEntity>
+    extends AbsTask<TASK_ENTITY> {
+
+  protected AbsGroupUtil mUtil;
+
+  @Override public String getKey() {
+    return mTaskEntity.getEntity().getKey();
+  }
+
+  /**
+   * 启动任务组中的子任务
+   *
+   * @param url 子任务下载地址
+   */
+  public void startSubTask(String url) {
+    if (mUtil != null) {
+      mUtil.startSubTask(url);
+    }
+  }
+
+  /**
+   * 停止任务组中的子任务
+   *
+   * @param url 子任务下载地址
+   */
+  public void stopSubTask(String url) {
+    if (mUtil != null) {
+      mUtil.stopSubTask(url);
+    }
+  }
+
+  /**
+   * 删除子任务组中的子任务
+   *
+   * @param url 子任务下载地址
+   */
+  public void cancelSubTask(String url) {
+    if (mUtil != null) {
+      mUtil.cancelSubTask(url);
+    }
+  }
+}

+ 26 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsGroupTaskEntity.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by lyy on 2017/9/5.
+ */
+public abstract class AbsGroupTaskEntity<ENTITY extends AbsGroupEntity> extends AbsTaskEntity<ENTITY>{
+  @Override public ENTITY getEntity() {
+    return null;
+  }
+}

+ 108 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsNormalEntity.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Created by AriaL on 2017/6/3.
+ */
+public abstract class AbsNormalEntity extends AbsEntity implements Parcelable {
+
+  /**
+   * 服务器地址
+   */
+  private String url;
+
+  /**
+   * 文件名
+   */
+  private String fileName;
+
+  /**
+   * 是否是任务组里面的下载实体
+   */
+  private boolean isGroupChild = false;
+
+  private boolean isRedirect = false; //是否重定向
+  private String redirectUrl; //重定向链接
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public boolean isGroupChild() {
+    return isGroupChild;
+  }
+
+  public void setGroupChild(boolean groupChild) {
+    isGroupChild = groupChild;
+  }
+
+  public String getFileName() {
+    return fileName;
+  }
+
+  public void setFileName(String fileName) {
+    this.fileName = fileName;
+  }
+
+  public boolean isRedirect() {
+    return isRedirect;
+  }
+
+  public void setRedirect(boolean redirect) {
+    isRedirect = redirect;
+  }
+
+  public String getRedirectUrl() {
+    return redirectUrl;
+  }
+
+  public void setRedirectUrl(String redirectUrl) {
+    this.redirectUrl = redirectUrl;
+  }
+
+  public AbsNormalEntity() {
+  }
+
+  @Override public int describeContents() {
+    return 0;
+  }
+
+  @Override public void writeToParcel(Parcel dest, int flags) {
+    super.writeToParcel(dest, flags);
+    dest.writeString(this.url);
+    dest.writeString(this.fileName);
+    dest.writeByte(this.isGroupChild ? (byte) 1 : (byte) 0);
+    dest.writeByte(this.isRedirect ? (byte) 1 : (byte) 0);
+    dest.writeString(this.redirectUrl);
+  }
+
+  protected AbsNormalEntity(Parcel in) {
+    super(in);
+    this.url = in.readString();
+    this.fileName = in.readString();
+    this.isGroupChild = in.readByte() != 0;
+    this.isRedirect = in.readByte() != 0;
+    this.redirectUrl = in.readString();
+  }
+}

+ 43 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsNormalTask.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by lyy on 2017/6/3.
+ */
+public abstract class AbsNormalTask<TASK_ENTITY extends AbsNormalTaskEntity>
+    extends AbsTask<TASK_ENTITY> {
+
+  /**
+   * 暂停任务,并让任务处于等待状态
+   */
+  public void stopAndWait() {
+
+  }
+
+  /**
+   * 最高优先级命令,最高优先级命令有以下属性
+   * 1、在下载队列中,有且只有一个最高优先级任务
+   * 2、最高优先级任务会一直存在,直到用户手动暂停或任务完成
+   * 3、任务调度器不会暂停最高优先级任务
+   * 4、用户手动暂停或任务完成后,第二次重新执行该任务,该命令将失效
+   * 5、如果下载队列中已经满了,则会停止队尾的任务
+   * 6、把任务设置为最高优先级任务后,将自动执行任务,不需要重新调用start()启动任务
+   */
+  public void setHighestPriority(boolean isHighestPriority) {
+    isHeighestTask = isHighestPriority;
+  }
+}

+ 23 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsNormalTaskEntity.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by Aria.Lao on 2017/10/12.
+ * 当任务实体
+ */
+public abstract class AbsNormalTaskEntity<ENTITY extends AbsEntity> extends AbsTaskEntity<ENTITY> {
+}

+ 34 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsReceiver.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by AriaL on 2017/6/27.
+ */
+
+public abstract class AbsReceiver<ENTITY extends AbsEntity> implements IReceiver<ENTITY> {
+  public String targetName;
+  public Object obj;
+  /**
+   * 当dialog、dialogFragment、popupwindow已经被设置了关闭监听时,需要手动移除receiver
+   */
+  public boolean needRmListener = false;
+
+  public void unRegisterListener(){
+
+  }
+}

+ 274 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsTarget.java

@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import android.text.TextUtils;
+import com.arialyy.aria.core.AriaManager;
+import com.arialyy.aria.core.command.ICmd;
+import com.arialyy.aria.core.command.normal.CancelCmd;
+import com.arialyy.aria.core.command.normal.NormalCmdFactory;
+import com.arialyy.aria.core.download.DownloadGroupTaskEntity;
+import com.arialyy.aria.core.download.DownloadTaskEntity;
+import com.arialyy.aria.core.manager.TEManager;
+import com.arialyy.aria.core.upload.UploadTaskEntity;
+import com.arialyy.aria.util.ALog;
+import com.arialyy.aria.util.CommonUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by AriaL on 2017/7/3.
+ */
+public abstract class AbsTarget<TARGET extends AbsTarget, ENTITY extends AbsEntity, TASK_ENTITY extends AbsTaskEntity>
+    implements ITarget<TARGET> {
+  protected String TAG = "";
+  protected ENTITY mEntity;
+  protected TASK_ENTITY mTaskEntity;
+  protected String mTargetName;
+
+  protected AbsTarget() {
+    TAG = CommonUtil.getClassName(this);
+  }
+
+  /**
+   * 重置状态,将任务状态设置为未开始状态
+   * 注意:如果在后续方法调用链中没有调用 {@link #start()}、{@link #stop()}、{@link #cancel()}、{@link #resume()}
+   * 等操作任务的方法,那么你需要调用{@link #save()}才能将修改保存到数据库
+   */
+  public TARGET resetState() {
+    mTaskEntity.getEntity().setState(IEntity.STATE_WAIT);
+    mTaskEntity.setRefreshInfo(true);
+    return (TARGET) this;
+  }
+
+  /**
+   * 删除记录,如果任务正在执行,则会删除正在下载的任务
+   */
+  public void removeRecord() {
+    if (isRunning()) {
+      ALog.d("AbsTarget", "任务正在下载,即将删除任务");
+      cancel();
+    } else {
+      mEntity.deleteData();
+      TEManager.getInstance().removeTEntity(mEntity.getKey());
+    }
+  }
+
+  /**
+   * 获取任务进度,如果任务存在,则返回当前进度
+   *
+   * @return 该任务进度
+   */
+  @Override
+  public long getCurrentProgress() {
+    return mEntity == null ? -1 : mEntity.getCurrentProgress();
+  }
+
+  /**
+   * 获取任务文件大小
+   *
+   * @return 文件大小
+   */
+  @Override public long getSize() {
+    return mEntity == null ? 0 : mEntity.getFileSize();
+  }
+
+  /**
+   * 获取单位转换后的文件大小
+   *
+   * @return 文件大小{@code xxx mb}
+   */
+  @Override public String getConvertSize() {
+    return mEntity == null ? "0b" : CommonUtil.formatFileSize(mEntity.getFileSize());
+  }
+
+  /**
+   * 设置扩展字段,用来保存你的其它数据,如果你的数据比较多,你可以把你的数据转换为JSON字符串,然后再存到Aria中
+   * 注意:如果在后续方法调用链中没有调用 {@link #start()}、{@link #stop()}、{@link #cancel()}、{@link #resume()}
+   * 等操作任务的方法,那么你需要调用{@link #save()}才能将修改保存到数据库
+   *
+   * @param str 扩展数据
+   */
+  public TARGET setExtendField(String str) {
+    if (TextUtils.isEmpty(str)) return (TARGET) this;
+    if (TextUtils.isEmpty(mEntity.getStr()) || !mEntity.getStr().equals(str)) {
+      mEntity.setStr(str);
+    } else {
+      ALog.e(TAG, "设置扩展字段失败,扩展字段为null");
+    }
+
+    return (TARGET) this;
+  }
+
+  /**
+   * 获取存放的扩展字段
+   * 设置扩展字段{@link #setExtendField(String)}
+   */
+  public String getExtendField() {
+    return mEntity.getStr();
+  }
+
+  /**
+   * 获取任务状态
+   *
+   * @return {@link IEntity}
+   */
+  @Override
+  public int getTaskState() {
+    return mEntity.getState();
+  }
+
+  /**
+   * 获取任务进度百分比
+   *
+   * @return 返回任务进度
+   */
+  @Override public int getPercent() {
+    if (mEntity == null) {
+      ALog.e("AbsTarget", "下载管理器中没有该任务");
+      return 0;
+    }
+    if (mEntity.getFileSize() != 0) {
+      return (int) (mEntity.getCurrentProgress() * 100 / mEntity.getFileSize());
+    }
+    return 0;
+  }
+
+  /**
+   * 检查实体是否合法,如果实体合法,将保存实体到数据库,或更新数据库中的实体对象
+   *
+   * @return {@code true} 合法
+   */
+  protected abstract boolean checkEntity();
+
+  protected int checkTaskType() {
+    int taskType = 0;
+    if (mTaskEntity instanceof DownloadTaskEntity) {
+      taskType = ICmd.TASK_TYPE_DOWNLOAD;
+    } else if (mTaskEntity instanceof DownloadGroupTaskEntity) {
+      taskType = ICmd.TASK_TYPE_DOWNLOAD_GROUP;
+    } else if (mTaskEntity instanceof UploadTaskEntity) {
+      taskType = ICmd.TASK_TYPE_UPLOAD;
+    }
+    return taskType;
+  }
+
+  /**
+   * 保存修改
+   */
+  public void save() {
+    if (!checkEntity()) {
+      ALog.e(TAG, "保存修改失败");
+    }
+  }
+
+  /**
+   * 开始任务
+   */
+  @Override public void start() {
+    if (checkEntity()) {
+      AriaManager.getInstance(AriaManager.APP)
+          .setCmd(CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_START,
+              checkTaskType()))
+          .exe();
+    }
+  }
+
+  /**
+   * 停止任务
+   *
+   * @see #stop()
+   */
+  @Deprecated public void pause() {
+    if (checkEntity()) {
+      stop();
+    }
+  }
+
+  @Override public void stop() {
+    if (checkEntity()) {
+      AriaManager.getInstance(AriaManager.APP)
+          .setCmd(CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_STOP,
+              checkTaskType()))
+          .exe();
+    }
+  }
+
+  /**
+   * 恢复任务
+   */
+  @Override public void resume() {
+    if (checkEntity()) {
+      AriaManager.getInstance(AriaManager.APP)
+          .setCmd(CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_START,
+              checkTaskType()))
+          .exe();
+    }
+  }
+
+  /**
+   * 删除任务
+   */
+  @Override public void cancel() {
+    if (checkEntity()) {
+      AriaManager.getInstance(AriaManager.APP)
+          .setCmd(CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_CANCEL,
+              checkTaskType()))
+          .exe();
+    }
+  }
+
+  /**
+   * 任务重试
+   */
+  public void reTry() {
+    if (checkEntity()) {
+      List<ICmd> cmds = new ArrayList<>();
+      int taskType = checkTaskType();
+      cmds.add(
+          CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_STOP,
+              taskType));
+      cmds.add(CommonUtil.createNormalCmd(mTargetName, mTaskEntity, NormalCmdFactory.TASK_START,
+          taskType));
+      AriaManager.getInstance(AriaManager.APP).setCmds(cmds).exe();
+    }
+  }
+
+  /**
+   * 删除任务
+   *
+   * @param removeFile {@code true} 不仅删除任务数据库记录,还会删除已经删除完成的文件
+   * {@code false}如果任务已经完成,只删除任务数据库记录,
+   */
+  public void cancel(boolean removeFile) {
+    if (checkEntity()) {
+      CancelCmd cancelCmd = (CancelCmd) CommonUtil.createNormalCmd(mTargetName, mTaskEntity,
+          NormalCmdFactory.TASK_CANCEL, checkTaskType());
+      cancelCmd.removeFile = removeFile;
+      AriaManager.getInstance(AriaManager.APP).setCmd(cancelCmd).exe();
+    }
+  }
+
+  /**
+   * 重新下载
+   */
+  public void reStart() {
+    if (checkEntity()) {
+      cancel();
+      start();
+    }
+  }
+}

+ 186 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsTask.java

@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import android.content.Context;
+import android.os.Handler;
+import com.arialyy.aria.util.CommonUtil;
+
+/**
+ * Created by AriaL on 2017/6/29.
+ */
+public abstract class AbsTask<TASK_ENTITY extends AbsTaskEntity> implements ITask<TASK_ENTITY> {
+
+  /**
+   * 是否需要重试,默认为true
+   */
+  public boolean needRetry = true;
+  protected TASK_ENTITY mTaskEntity;
+  protected Handler mOutHandler;
+
+  /**
+   * 用于生成该任务对象的hash码
+   */
+  private String mTargetName;
+  protected Context mContext;
+  protected boolean isHeighestTask = false;
+
+  public Handler getOutHandler() {
+    return mOutHandler;
+  }
+
+  /**
+   * 任务是否完成
+   *
+   * @return {@code true} 已经完成,{@code false} 未完成
+   */
+  public boolean isComplete() {
+    return mTaskEntity.getEntity().isComplete();
+  }
+
+  /**
+   * 获取当前下载进度
+   */
+  @Override public long getCurrentProgress() {
+    return mTaskEntity.getEntity().getCurrentProgress();
+  }
+
+  /**
+   * 获取单位转换后的进度
+   *
+   * @return 如:已经下载3mb的大小,则返回{@code 3mb}
+   */
+  @Override public String getConvertCurrentProgress() {
+    if (mTaskEntity.getEntity().getCurrentProgress() == 0) {
+      return "0b";
+    }
+    return CommonUtil.formatFileSize(mTaskEntity.getEntity().getCurrentProgress());
+  }
+
+  /**
+   * 转换单位后的文件长度
+   *
+   * @return 如果文件长度为0,则返回0m,否则返回转换后的长度1b、1kb、1mb、1gb、1tb
+   */
+  @Override public String getConvertFileSize() {
+    if (mTaskEntity.getEntity().getFileSize() == 0) {
+      return "0mb";
+    }
+    return CommonUtil.formatFileSize(mTaskEntity.getEntity().getFileSize());
+  }
+
+  /**
+   * 获取文件大小
+   */
+  @Override public long getFileSize() {
+    return mTaskEntity.getEntity().getFileSize();
+  }
+
+  /**
+   * 获取百分比进度
+   *
+   * @return 返回百分比进度,如果文件长度为0,返回0
+   */
+  @Override public int getPercent() {
+    if (mTaskEntity.getEntity().getFileSize() == 0) {
+      return 0;
+    }
+    return (int) (mTaskEntity.getEntity().getCurrentProgress() * 100 / mTaskEntity.getEntity()
+        .getFileSize());
+  }
+
+  /**
+   * 任务当前状态
+   *
+   * @return {@link IEntity}
+   */
+  public int getState() {
+    return mTaskEntity.getEntity() == null ? IEntity.STATE_OTHER
+        : mTaskEntity.getEntity().getState();
+  }
+
+  /**
+   * 获取保存的扩展字段
+   *
+   * @return 如果实体不存在,则返回null,否则返回扩展字段
+   */
+  @Override public String getExtendField() {
+    return mTaskEntity.getEntity() == null ? null : mTaskEntity.getEntity().getStr();
+  }
+
+  /**
+   * @return 返回原始byte速度,需要你在配置文件中配置
+   * <pre>
+   *   {@code
+   *    <xml>
+   *      <download>
+   *        ...
+   *        <convertSpeed value="false"/>
+   *      </download>
+   *
+   *      或在代码中设置
+   *      Aria.get(this).getDownloadConfig().setConvertSpeed(false);
+   *    </xml>
+   *   }
+   * </pre>
+   * 才能生效
+   */
+  @Override public long getSpeed() {
+    return mTaskEntity.getEntity().getSpeed();
+  }
+
+  /**
+   * @return 返回转换单位后的速度,需要你在配置文件中配置,转换完成后为:1b/s、1kb/s、1mb/s、1gb/s、1tb/s
+   * <pre>
+   *   {@code
+   *    <xml>
+   *      <download>
+   *        ...
+   *        <convertSpeed value="true"/>
+   *      </download>
+   *
+   *      或在代码中设置
+   *      Aria.get(this).getDownloadConfig().setConvertSpeed(true);
+   *    </xml>
+   *   }
+   * </pre>
+   * 才能生效
+   */
+  @Override public String getConvertSpeed() {
+    return mTaskEntity.getEntity().getConvertSpeed();
+  }
+
+  @Override public TASK_ENTITY getTaskEntity() {
+    return mTaskEntity;
+  }
+
+  /**
+   * 获取任务名,也就是文件名
+   */
+  public abstract String getTaskName();
+
+  public String getTargetName() {
+    return mTargetName;
+  }
+
+  @Override public void setTargetName(String targetName) {
+    this.mTargetName = targetName;
+  }
+
+  public boolean isHighestPriorityTask() {
+    return isHeighestTask;
+  }
+}

+ 248 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/AbsTaskEntity.java

@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import com.arialyy.aria.core.FtpUrlEntity;
+import com.arialyy.aria.core.common.RequestEnum;
+import com.arialyy.aria.orm.DbEntity;
+import com.arialyy.aria.orm.annotation.Ignore;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by lyy on 2017/2/23.
+ * 所有任务实体的父类
+ */
+public abstract class AbsTaskEntity<ENTITY extends AbsEntity> extends DbEntity {
+  /**
+   * HTTP单任务载
+   */
+  public static final int D_HTTP = 0x11;
+  /**
+   * HTTP任务组下载
+   */
+  public static final int DG_HTTP = 0x12;
+
+  /**
+   * FTP单文件下载
+   */
+  public static final int D_FTP = 0x13;
+  /**
+   * FTP文件夹下载,为避免登录过多,子任务由单线程进行处理
+   */
+  public static final int D_FTP_DIR = 0x14;
+
+  /**
+   * HTTP单文件上传
+   */
+  public static final int U_HTTP = 0xA1;
+  /**
+   * FTP单文件上传
+   */
+  public static final int U_FTP = 0xA2;
+
+  /**
+   * 账号和密码
+   */
+  @Ignore private FtpUrlEntity urlEntity;
+
+  /**
+   * 刷新信息 {@code true} 重新刷新下载信息
+   */
+  @Ignore private boolean refreshInfo = false;
+
+  /**
+   * 是否是新任务,{@code true} 新任务
+   */
+  @Ignore private boolean isNewTask = false;
+
+  /**
+   * 任务状态,和Entity的state同步
+   */
+  private int state = IEntity.STATE_WAIT;
+
+  /**
+   * 请求类型
+   * {@link AbsTaskEntity#D_HTTP}、{@link AbsTaskEntity#D_FTP}、{@link AbsTaskEntity#D_FTP_DIR}。。。
+   */
+  private int requestType = D_HTTP;
+
+  /**
+   * http 请求头
+   */
+  private Map<String, String> headers = new HashMap<>();
+
+  /**
+   * 字符编码,默认为"utf-8"
+   */
+  private String charSet = "utf-8";
+
+  /**
+   * 网络请求类型
+   */
+  private RequestEnum requestEnum = RequestEnum.GET;
+
+  /**
+   * 是否使用服务器通过content-disposition传递的文件名,内容格式{@code attachment; filename="filename.jpg"}
+   * {@code true} 使用
+   */
+  private boolean useServerFileName = false;
+
+  /**
+   * 重定向链接
+   */
+  private String redirectUrl = "";
+
+  /**
+   * 删除任务时,是否删除已下载完成的文件
+   * 未完成的任务,不管true还是false,都会删除文件
+   * {@code true}  删除任务数据库记录,并且删除已经下载完成的文件
+   * {@code false} 如果任务已经完成,只删除任务数据库记录
+   */
+  @Ignore private boolean removeFile = false;
+
+  /**
+   * 是否支持断点, {@code true} 为支持断点
+   */
+  private boolean isSupportBP = true;
+
+  /**
+   * 状态码
+   */
+  private int code;
+
+  public abstract ENTITY getEntity();
+
+  /**
+   * 获取任务下载状态
+   *
+   * @return {@link IEntity}
+   */
+  public int getState() {
+    return getEntity().getState();
+  }
+
+  public abstract String getKey();
+
+  public abstract void setKey(String key);
+
+  @Override public void update() {
+    if (getEntity() != null) {
+      getEntity().update();
+    }
+    super.update();
+  }
+
+  public FtpUrlEntity getUrlEntity() {
+    return urlEntity;
+  }
+
+  public void setUrlEntity(FtpUrlEntity urlEntity) {
+    this.urlEntity = urlEntity;
+  }
+
+  public boolean isRefreshInfo() {
+    return refreshInfo;
+  }
+
+  public void setRefreshInfo(boolean refreshInfo) {
+    this.refreshInfo = refreshInfo;
+  }
+
+  public boolean isNewTask() {
+    return isNewTask;
+  }
+
+  public void setNewTask(boolean newTask) {
+    isNewTask = newTask;
+  }
+
+  public void setState(int state) {
+    this.state = state;
+  }
+
+  public int getRequestType() {
+    return requestType;
+  }
+
+  public void setRequestType(int requestType) {
+    this.requestType = requestType;
+  }
+
+  public Map<String, String> getHeaders() {
+    return headers;
+  }
+
+  public void setHeaders(Map<String, String> headers) {
+    this.headers = headers;
+  }
+
+  public String getCharSet() {
+    return charSet;
+  }
+
+  public void setCharSet(String charSet) {
+    this.charSet = charSet;
+  }
+
+  public RequestEnum getRequestEnum() {
+    return requestEnum;
+  }
+
+  public void setRequestEnum(RequestEnum requestEnum) {
+    this.requestEnum = requestEnum;
+  }
+
+  public boolean isUseServerFileName() {
+    return useServerFileName;
+  }
+
+  public void setUseServerFileName(boolean useServerFileName) {
+    this.useServerFileName = useServerFileName;
+  }
+
+  public String getRedirectUrl() {
+    return redirectUrl;
+  }
+
+  public void setRedirectUrl(String redirectUrl) {
+    this.redirectUrl = redirectUrl;
+  }
+
+  public boolean isRemoveFile() {
+    return removeFile;
+  }
+
+  public void setRemoveFile(boolean removeFile) {
+    this.removeFile = removeFile;
+  }
+
+  public boolean isSupportBP() {
+    return isSupportBP;
+  }
+
+  public void setSupportBP(boolean supportBP) {
+    isSupportBP = supportBP;
+  }
+
+  public int getCode() {
+    return code;
+  }
+
+  public void setCode(int code) {
+    this.code = code;
+  }
+}

+ 34 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/GroupSendParams.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by lyy on 2017/9/8.
+ * 任务组参数传递
+ */
+public class GroupSendParams<GROUP_TASK extends AbsGroupTask, ENTITY extends AbsNormalEntity> {
+
+  public GROUP_TASK groupTask;
+  public ENTITY entity;
+
+  public GroupSendParams() {
+  }
+
+  public GroupSendParams(GROUP_TASK groupTask, ENTITY entity) {
+    this.groupTask = groupTask;
+    this.entity = entity;
+  }
+}

+ 35 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/IDownloadListener.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * 下载监听
+ */
+public interface IDownloadListener extends IEventListener {
+
+  /**
+   * 预处理完成,准备下载---开始下载之间
+   */
+  void onPostPre(long fileSize);
+
+  /**
+   * 支持断点回调
+   *
+   * @param support true,支持;false 不支持
+   */
+  void supportBreakpoint(boolean support);
+}

+ 60 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/IEntity.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import com.arialyy.aria.orm.annotation.Ignore;
+
+/**
+ * Created by lyy on 2017/2/23.
+ */
+public interface IEntity {
+  /**
+   * 其它状态
+   */
+  @Ignore int STATE_OTHER = -1;
+  /**
+   * 失败状态
+   */
+  @Ignore int STATE_FAIL = 0;
+  /**
+   * 完成状态
+   */
+  @Ignore int STATE_COMPLETE = 1;
+  /**
+   * 停止状态
+   */
+  @Ignore int STATE_STOP = 2;
+  /**
+   * 等待状态
+   */
+  @Ignore int STATE_WAIT = 3;
+  /**
+   * 正在执行
+   */
+  @Ignore int STATE_RUNNING = 4;
+  /**
+   * 预处理
+   */
+  @Ignore int STATE_PRE = 5;
+  /**
+   * 预处理完成
+   */
+  @Ignore int STATE_POST_PRE = 6;
+  /**
+   * 删除任务
+   */
+  @Ignore int STATE_CANCEL = 7;
+}

+ 65 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/IEventListener.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by Aria.Lao on 2017/7/18.
+ * 基础事件
+ */
+public interface IEventListener {
+
+  /**
+   * 预处理,有时有些地址链接比较慢,这时可以先在这个地方出来一些界面上的UI,如按钮的状态
+   */
+  void onPre();
+
+  /**
+   * 开始
+   */
+  void onStart(long startLocation);
+
+  /**
+   * 恢复位置
+   */
+  void onResume(long resumeLocation);
+
+  /**
+   * 下载监听
+   */
+  void onProgress(long currentLocation);
+
+  /**
+   * 停止
+   */
+  void onStop(long stopLocation);
+
+  /**
+   * 下载完成
+   */
+  void onComplete();
+
+  /**
+   * 取消下载
+   */
+  void onCancel();
+
+  /**
+   * 下载失败
+   * @param needRetry 是否需要重试{@code true} 需要
+   */
+  void onFail(boolean needRetry);
+
+}

+ 43 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/IFtpTarget.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+/**
+ * Created by laoyuyu on 2018/3/9.
+ */
+public interface IFtpTarget<TARGET extends ITarget> {
+  /**
+   * 设置字符编码
+   */
+  TARGET charSet(String charSet);
+
+  /**
+   * ftp 用户登录信。
+   *
+   * @param userName ftp用户名
+   * @param password ftp用户密码
+   */
+  TARGET login(String userName, String password);
+
+  /**
+   * ftp 用户登录信息
+   *
+   * @param userName ftp用户名
+   * @param password ftp用户密码
+   * @param account ftp账号
+   */
+  TARGET login(String userName, String password, String account);
+}

+ 52 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/IHttpHeaderTarget.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
+ *
+ * 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 com.arialyy.aria.core.inf;
+
+import android.support.annotation.NonNull;
+import com.arialyy.aria.core.common.RequestEnum;
+import com.arialyy.aria.core.download.DownloadEntity;
+import java.util.Map;
+
+/**
+ * Created by laoyuyu on 2018/3/9.
+ * HTTP Header功能接口
+ */
+public interface IHttpHeaderTarget<TARGET extends ITarget> {
+
+  /**
+   * 给url请求添加Header数据
+   * 如果新的header数据和数据保存的不一致,则更新数据库中对应的header数据
+   *
+   * @param key header对应的key
+   * @param value header对应的value
+   */
+  TARGET addHeader(@NonNull String key, @NonNull String value);
+
+  /**
+   * 给url请求添加一组header数据
+   * 如果新的header数据和数据保存的不一致,则更新数据库中对应的header数据
+   *
+   * @param headers 一组http header数据
+   */
+  TARGET addHeaders(Map<String, String> headers);
+
+  /**
+   * 设置HTTP请求类型
+   *
+   * @param requestEnum {@link RequestEnum}
+   */
+  TARGET setRequestMode(RequestEnum requestEnum);
+}

+ 0 - 0
Aria/src/main/java/com/arialyy/aria/core/inf/IReceiver.java


Някои файлове не бяха показани, защото твърде много файлове са промени