Skip to content

Commit

Permalink
feature: 支持简单的 Retrofit 下载,带进度的上传
Browse files Browse the repository at this point in the history
  • Loading branch information
tangwanchao committed Feb 2, 2023
1 parent f255d38 commit 195e842
Show file tree
Hide file tree
Showing 15 changed files with 340 additions and 4 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down
1 change: 1 addition & 0 deletions download/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
68 changes: 68 additions & 0 deletions download/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'maven-publish'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.2"

defaultConfig {
minSdkVersion 21
targetSdkVersion 30

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

// 用于打包源代码的任务
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
from android.sourceSets.main.java.srcDirs
}

afterEvaluate {
publishing {
publications {
// Creates a Maven publication called "release".
release(MavenPublication) {
// Applies the component for the release build variant.
from components.release
artifact androidSourcesJar
// You can then customize attributes of the publication as shown below.
groupId = 'com.github.tangwanchao'
artifactId = 'source-download'
version = '2.0.0'
}
}
}
}

dependencies {

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'

compileOnly project(path: ':source')
}
Empty file added download/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions download/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package me.twc.source.download

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("me.twc.source.download.test", appContext.packageName)
}
}
5 changes: 5 additions & 0 deletions download/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.twc.source.download">

</manifest>
47 changes: 47 additions & 0 deletions download/src/main/java/me/twc/source/download/DownloadCallback.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package me.twc.source.download

import okhttp3.ResponseBody
import java.io.File

/**
* @author 唐万超
* @date 2023/02/02
*/
interface DownloadCallback {
/**
* 获取下载结果
*
*
* 下载配置示例:
*
* @Streaming
* @GET
* suspend fun downloadWithUrl(
* @Url url: String
* ): ResponseBody
*
*/
fun genResponseBody(): ResponseBody

/**
* 下载后文件保存位置
*/
fun genSaveFile(): File? = null

/**
* 开始下载
*/
fun onStart() {}

/**
* 下载中
*/
fun onDownload(downloadLength: Int, totalLength: Long) {}

/**
* 下载失败
*/
fun onError(th: Throwable) {
th.printStackTrace()
}
}
54 changes: 54 additions & 0 deletions download/src/main/java/me/twc/source/download/Downloader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package me.twc.source.download

import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream

/**
* @author 唐万超
* @date 2023/02/02
*/
@Suppress("MemberVisibilityCanBePrivate")
class Downloader(
val context: Context,
val callback: DownloadCallback
) {
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun startDownload() = withContext(Dispatchers.IO) {
var outputFile: File?
var fos: FileOutputStream? = null
var bs: InputStream? = null
try {
val responseBody = callback.genResponseBody()
bs = responseBody.byteStream()
outputFile = callback.genSaveFile() ?: genSaveFile()
fos = FileOutputStream(outputFile)
callback.onStart()
val totalLength = responseBody.contentLength()
var downloadLength = 0
val buffer = ByteArray(8 * 1024)
var bytes = bs.read(buffer)
while (bytes >= 0) {
fos.write(buffer, 0, bytes)
downloadLength += bytes
callback.onDownload(downloadLength, totalLength)
bytes = bs.read(buffer)
}
} catch (th: Throwable) {
outputFile = null
callback.onError(th)
} finally {
// 使用 use 关闭流
fos?.use { }
bs?.use { }
}
return@withContext outputFile
}

private fun genSaveFile(): File {
return File(context.cacheDir, "downloader_${System.currentTimeMillis()}.apk")
}
}
50 changes: 50 additions & 0 deletions download/src/main/java/me/twc/source/download/ProgressListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package me.twc.source.download

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import me.twc.source.BuildConfig

/**
* @author 唐万超
* @date 2021/01/27
*/
interface ProgressListener {
fun onProgress(bytesWritten: Long, contentLength: Long)
}

class SimpleProgressListener(
lifecycleOwner: LifecycleOwner,
observer: Observer<Int>
) : ProgressListener {

private var mPreTime = 0L
private var mComplete = false
private var mIgnoreFirstComplete = false
private val mLiveData = MutableLiveData<Int>()
init {
mLiveData.observe(lifecycleOwner,observer)
}

override fun onProgress(bytesWritten: Long, contentLength: Long) {
if(BuildConfig.DEBUG && !mIgnoreFirstComplete){
mIgnoreFirstComplete = bytesWritten == contentLength
return
}
if (mComplete) return
val percent = bytesWritten.toFloat() / contentLength.toFloat()
if (percent == 1f) {
mComplete = true
return onProgress(100)
}
val currTime = System.currentTimeMillis()
if (currTime - mPreTime >= 16L){
onProgress((percent * 100).toInt())
mPreTime = currTime
}
}

private fun onProgress(percent: Int){
mLiveData.postValue(percent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package me.twc.source.download

import okhttp3.MediaType
import okhttp3.RequestBody
import okio.*

/**
* @author 唐万超
* @date 2021/01/27
*/
class ProgressRequestBody(
private val mDelegate: RequestBody,
private val mListener: ProgressListener
) : RequestBody() {

override fun contentType(): MediaType? {
return mDelegate.contentType()
}

override fun contentLength(): Long {
return try {
mDelegate.contentLength()
} catch (ioe: IOException) {
-1
}

}

override fun writeTo(sink: BufferedSink) {
val progressSink = ProgressSink(sink)
val bufferSink = progressSink.buffer()
mDelegate.writeTo(bufferSink)
bufferSink.flush()
}


private inner class ProgressSink(delegate:Sink): ForwardingSink(delegate){

private var bytesWritten:Long = 0L

override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
mListener.onProgress(bytesWritten, contentLength())
}
}
}
17 changes: 17 additions & 0 deletions download/src/test/java/me/twc/source/download/ExampleUnitTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package me.twc.source.download

import org.junit.Test

import org.junit.Assert.*

/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ include ':app'
rootProject.name = "RetrofitSource"
include ':observer'
include ':paging'
include ':download'
6 changes: 3 additions & 3 deletions source/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
// http
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.8.1'
api 'com.squareup.retrofit2:retrofit:2.9.0'
api 'com.squareup.retrofit2:converter-gson:2.9.0'
api 'com.squareup.okhttp3:logging-interceptor:4.8.1'
// 协程
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
Expand Down

0 comments on commit 195e842

Please sign in to comment.