├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── CHANGELOG.md
├── README.md
├── android-sample
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── billbook
│ │ └── lib
│ │ └── downloader
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── billbook
│ │ │ └── lib
│ │ │ └── downloader
│ │ │ ├── AppModule.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainScreen.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── SampleApp.kt
│ │ │ └── Util.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── billbook
│ └── lib
│ └── downloader
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── buildSrc
├── .gitignore
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── Extentions.kt
├── deploy_docs.sh
├── docs
├── best_practices.md
├── change_logs.md
├── contributing.md
├── css
│ └── site.css
├── downloader_pipeline.md
├── faq.md
├── getting_started.md
├── images
│ └── logo.svg
├── in_android.md
└── optional_settings.md
├── fakedata
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── billbook
│ └── lib
│ └── downloader
│ └── FakeData.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── java-sample
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── billbook
│ └── lib
│ └── case
│ ├── FileExistsInterceptor.kt
│ ├── ReporterEventListener.kt
│ └── UseCase.kt
├── mkdocs.yml
├── okdownloader-android
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── billbook
│ │ └── lib
│ │ └── downloader
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── billbook
│ │ └── lib
│ │ └── downloader
│ │ ├── CopyOnExistsInterceptor.kt
│ │ ├── DownloadRequest.kt
│ │ ├── NetworkInterceptor.kt
│ │ ├── NetworkMonitor.kt
│ │ ├── Observables.kt
│ │ ├── StorageInterceptor.kt
│ │ └── internal
│ │ ├── Database.kt
│ │ └── Record.kt
│ └── test
│ └── java
│ └── com
│ └── billbook
│ └── lib
│ └── downloader
│ └── ExampleUnitTest.kt
├── okdownloader
├── .gitignore
├── build.gradle.kts
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── billbook
│ │ └── lib
│ │ └── downloader
│ │ ├── Download.kt
│ │ ├── DownloadException.kt
│ │ ├── DownloadPool.kt
│ │ ├── Downloader.kt
│ │ ├── ErrorCode.kt
│ │ ├── EventListener.kt
│ │ ├── Interceptor.kt
│ │ └── internal
│ │ ├── core
│ │ ├── BuiltinInterceptor.kt
│ │ ├── Dispatcher.kt
│ │ ├── DownloadCall.kt
│ │ ├── IOExchange.kt
│ │ └── InterceptorChain.kt
│ │ ├── exception
│ │ └── InternalException.kt
│ │ └── util
│ │ ├── Http.kt
│ │ ├── Preconditions.kt
│ │ └── Util.kt
│ └── test
│ └── java
│ └── com
│ └── billbook
│ └── lib
│ └── downloader
│ ├── DownloadUnitTest.kt
│ └── FakeData.kt
├── screencaps
└── 1.png
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /downloads
17 | # Docs
18 | docs/api
19 | site
20 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## Version 1.0.1
4 |
5 | __2023-08-01__
6 |
7 | - New: 支持分块下载(即多线程下载)
8 | - New:支持设置 DownloadPool
9 | - Remove:移除callbackOn API
10 |
11 | ## Version 1.0.0
12 |
13 | __2023-07-25__
14 |
15 | - New:Version 1.0.0 发布
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | OkDownloader
2 | ============
3 |
4 | A downloader library for Java and Android based on OkHttp.
5 |
6 | * Easy to use: Simple API similar to OkHttp.
7 | * Rich in features: Supports synchronous/asynchronous downloads, network restrictions, multithreading, task priorities, resource verification, and more.
8 | * Modern: Written in Kotlin and based on OkHttp.
9 | * Easy to extend: Supports adding interceptors through code and provides extension through the `SPI` mechanism.
10 |
11 | Download
12 | --------
13 |
14 | OkDownloader is available on `mavenCentral()`.
15 |
16 | ```kotlin
17 | implementation("io.github.ydxlt:okdownloader:1.0.0")
18 | ```
19 |
20 | Quick Start
21 | -----------
22 |
23 | Build a downloader instance just like building an OkHttpClient.
24 |
25 | ```kotlin
26 | val downloader = Downloader.Builder().build()
27 | ```
28 |
29 | Start download
30 |
31 | ```kotlin
32 | val request = Download.Request.Builder()
33 | .url(url)
34 | .into(path) // or into(file)
35 | .build()
36 | val call = downloader.newCall(request)
37 | val response = call.execute() // synchronous download
38 | // use response here
39 | ```
40 |
41 | or
42 |
43 | ```kotlin
44 | call.enqueue() // Asynchronous download
45 | ```
46 |
47 | Add callback listeners
48 |
49 | ```kotlin
50 | call.enqueue(object : Download.Callback {
51 | // ...
52 | override fun onSuccess(call: Download.Call, response: Download.Response) {
53 | // do your job
54 | }
55 |
56 | override fun onFailure(call: Download.Call, response: Download.Response) {
57 | // do your job
58 | }
59 | })
60 | ```
61 |
62 | Cancel download
63 |
64 | ```kotlin
65 | call.cancel() // or call.cancelSafely()
66 | ```
67 |
68 | Check out OkDownloader's [full documentation here](https://ydxlt.github.io/okdownloader/getting_started/).
69 |
70 | How to Expand
71 | -------------
72 |
73 | Add interceptors through code
74 |
75 | ```kotlin
76 | val downloader = Downloader.Builder()
77 | .addInterceptor(CustomInterceptor())
78 | .build()
79 | ```
80 |
81 | or
82 |
83 | Declare your interceptors in `META-INF/services/com.billbook.lib.Interceptor` using the `SPI` mechanism.
84 |
85 | ```kotlin
86 | com.example.CustomInterceptor1
87 | com.example.CustomInterceptor2
88 | com.example.CustomInterceptor3
89 | ```
90 |
91 | R8/Proguard
92 | -----------
93 |
94 | OkDownloader is fully compatible with R8 out of the box and doesn't require adding any extra rules. If you use Proguard, you may need to add rules for [OkHttp](https://github.com/square/okhttp/blob/master/okhttp/src/jvmMain/resources/META-INF/proguard/okhttp3.pro) and [Okio](https://github.com/square/okio/blob/master/okio/src/jvmMain/resources/META-INF/proguard/okio.pro).
95 |
96 | License
97 | =======
98 |
99 | ```
100 | Copyright 2023 Billbook, Inc.
101 |
102 | Licensed under the Apache License, Version 2.0 (the "License");
103 | you may not use this file except in compliance with the License.
104 | You may obtain a copy of the License at
105 |
106 | http://www.apache.org/licenses/LICENSE-2.0
107 |
108 | Unless required by applicable law or agreed to in writing, software
109 | distributed under the License is distributed on an "AS IS" BASIS,
110 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
111 | See the License for the specific language governing permissions and
112 | limitations under the License.
113 | ```
114 |
--------------------------------------------------------------------------------
/android-sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/android-sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("com.google.dagger.hilt.android")
5 | kotlin("kapt")
6 | }
7 |
8 | android {
9 | namespace = "com.billbook.lib.downloader"
10 | compileSdk = 33
11 |
12 | defaultConfig {
13 | applicationId = "com.billbook.lib.downloader"
14 | minSdk = 21
15 | targetSdk = 33
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = false
25 | proguardFiles(
26 | getDefaultProguardFile("proguard-android-optimize.txt"),
27 | "proguard-rules.pro"
28 | )
29 | }
30 | }
31 |
32 | buildFeatures {
33 | compose = true
34 | }
35 |
36 | composeOptions {
37 | kotlinCompilerExtensionVersion = "1.4.6"
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation("androidx.core:core-ktx:1.8.0")
43 | implementation("androidx.appcompat:appcompat:1.6.1")
44 | implementation("com.google.android.material:material:1.9.0")
45 | testImplementation("junit:junit:4.13.2")
46 | androidTestImplementation("androidx.test.ext:junit:1.1.5")
47 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
48 | implementation(project(path = ":okdownloader-android"))
49 |
50 | val composeBom = platform("androidx.compose:compose-bom:2023.05.01")
51 | implementation(composeBom)
52 | androidTestImplementation(composeBom)
53 |
54 | // Choose one of the following:
55 | // Material Design 3
56 | implementation("androidx.compose.material3:material3")
57 | // or skip Material Design and build directly on top of foundational components
58 | implementation("androidx.compose.foundation:foundation")
59 | // or only import the main APIs for the underlying toolkit systems,
60 | // such as input and measurement/layout
61 | implementation("androidx.compose.ui:ui")
62 |
63 | // Android Studio Preview support
64 | implementation("androidx.compose.ui:ui-tooling-preview")
65 | debugImplementation("androidx.compose.ui:ui-tooling")
66 |
67 | // UI Tests
68 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
69 | debugImplementation("androidx.compose.ui:ui-test-manifest")
70 |
71 | // Optional - Included automatically by material, only add when you need
72 | // the icons but not the material library (e.g. when using Material3 or a
73 | // custom design system based on Foundation)
74 | implementation("androidx.compose.material:material-icons-core")
75 | // Optional - Add full set of material icons
76 | implementation("androidx.compose.material:material-icons-extended")
77 | // Optional - Add window size utils
78 | implementation("androidx.compose.material3:material3-window-size-class")
79 |
80 | // Optional - Integration with activities
81 | implementation("androidx.activity:activity-compose:1.6.1")
82 | // Optional - Integration with ViewModels
83 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
84 | // Optional - Integration with LiveData
85 | implementation("androidx.compose.runtime:runtime-livedata")
86 | implementation("io.coil-kt:coil:2.4.0")
87 | implementation("io.coil-kt:coil-compose:2.4.0")
88 |
89 | implementation("com.google.dagger:hilt-android:2.44")
90 | kapt("com.google.dagger:hilt-android-compiler:2.44")
91 | implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
92 |
93 | implementation(project(path = ":fakedata"))
94 | }
--------------------------------------------------------------------------------
/android-sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/android-sample/src/androidTest/java/com/billbook/lib/downloader/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.billbook.lib.downloader
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.billbook.lib.downloader", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/android-sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/android-sample/src/main/java/com/billbook/lib/downloader/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.billbook.lib.downloader
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | /**
12 | * @author xluotong@gmail.com
13 | */
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | class AppModule {
17 |
18 | @Singleton
19 | @Provides
20 | fun provideDownloader(@ApplicationContext context: Context): Downloader {
21 | return Downloader.Builder()
22 | .addInterceptor(CopyOnExistsInterceptor(context, 1000))
23 | .addInterceptor(NetworkInterceptor(context))
24 | .build()
25 | }
26 | }
--------------------------------------------------------------------------------
/android-sample/src/main/java/com/billbook/lib/downloader/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.billbook.lib.downloader
2 |
3 | import android.os.Bundle
4 | import androidx.activity.compose.setContent
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.core.view.WindowCompat
7 | import androidx.fragment.app.FragmentActivity
8 | import dagger.hilt.android.AndroidEntryPoint
9 |
10 | /**
11 | * @author xluotong@gmail.com
12 | */
13 | @AndroidEntryPoint
14 | class MainActivity : FragmentActivity() {
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | WindowCompat.setDecorFitsSystemWindows(window, false)
19 | setContent {
20 | MaterialTheme {
21 | MainScreen(beans = FakeData.resources)
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/android-sample/src/main/java/com/billbook/lib/downloader/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.billbook.lib.downloader
2 |
3 | import android.text.format.Formatter
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.foundation.lazy.items
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material3.Button
17 | import androidx.compose.material3.LinearProgressIndicator
18 | import androidx.compose.material3.MaterialTheme
19 | import androidx.compose.material3.Scaffold
20 | import androidx.compose.material3.SnackbarHost
21 | import androidx.compose.material3.SnackbarHostState
22 | import androidx.compose.material3.Text
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.collectAsState
25 | import androidx.compose.runtime.getValue
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.ui.Alignment
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.draw.clip
30 | import androidx.compose.ui.layout.ContentScale
31 | import androidx.compose.ui.platform.LocalContext
32 | import androidx.compose.ui.text.style.TextOverflow
33 | import androidx.compose.ui.tooling.preview.Preview
34 | import androidx.compose.ui.unit.dp
35 | import androidx.hilt.navigation.compose.hiltViewModel
36 | import coil.compose.AsyncImage
37 |
38 | /**
39 | * @author xluotong@gmail.com
40 | */
41 | @Composable
42 | internal fun MainScreen(
43 | beans: List,
44 | viewModel: MainViewModel = hiltViewModel()
45 | ) {
46 | val states by viewModel.states.collectAsState()
47 | val snackbarState = remember { SnackbarHostState() }
48 | Scaffold(snackbarHost = { SnackbarHost(hostState = snackbarState) }) {
49 | LazyColumn(modifier = Modifier.padding(it)) {
50 | items(items = beans) { item ->
51 | val state = states.getOrElse(item.url) { DownloadState.IDLE }
52 | ListItem(
53 | item = item,
54 | state = state,
55 | onClick = {
56 | when (state) {
57 | DownloadState.IDLE, is DownloadState.ERROR -> {
58 | viewModel.download(item)
59 | }
60 |
61 | is DownloadState.DOWNLOADING -> {
62 | viewModel.pause(item)
63 | }
64 |
65 | DownloadState.FINISH -> {
66 | viewModel.redownload(item)
67 | }
68 |
69 | else -> {}
70 | }
71 | }
72 | )
73 | }
74 | }
75 | }
76 | }
77 |
78 | @Composable
79 | private fun ListItem(item: ResourceBean, state: DownloadState, onClick: () -> Unit) {
80 | Row(
81 | modifier = Modifier
82 | .fillMaxWidth()
83 | .padding(horizontal = 16.dp, vertical = 16.dp)
84 | ) {
85 | AsyncImage(
86 | modifier = Modifier
87 | .size(60.dp)
88 | .clip(RoundedCornerShape(16.dp)),
89 | model = item.icon,
90 | contentDescription = "Icon",
91 | contentScale = ContentScale.Crop
92 | )
93 | Spacer(modifier = Modifier.width(10.dp))
94 | Column(
95 | modifier = Modifier
96 | .weight(1f)
97 | .height(70.dp)
98 | ) {
99 | Row(verticalAlignment = Alignment.CenterVertically) {
100 | Column(modifier = Modifier.weight(1f)) {
101 | Text(text = item.name, style = MaterialTheme.typography.bodyMedium)
102 | Spacer(modifier = Modifier.height(6.dp))
103 | Text(
104 | text = "size: ${Formatter.formatFileSize(LocalContext.current, item.size)}",
105 | style = MaterialTheme.typography.labelSmall
106 | )
107 | }
108 | Spacer(modifier = Modifier.width(10.dp))
109 | Button(
110 | modifier = Modifier
111 | .align(Alignment.CenterVertically)
112 | .width(110.dp),
113 | onClick = onClick,
114 | enabled = (state is DownloadState.RETRYING || state is DownloadState.CHECKING || state is DownloadState.WAIT).not()
115 | ) {
116 | Text(
117 | text = when (state) {
118 | DownloadState.IDLE -> "Download"
119 | DownloadState.FINISH -> "Success"
120 | is DownloadState.ERROR -> "Retry"
121 | DownloadState.RETRYING -> "Retrying"
122 | DownloadState.CHECKING -> "Checking"
123 | DownloadState.WAIT -> "Waiting"
124 | is DownloadState.DOWNLOADING -> "Pause"
125 | },
126 | style = MaterialTheme.typography.bodySmall,
127 | overflow = TextOverflow.Ellipsis,
128 | maxLines = 1
129 | )
130 | }
131 | }
132 | when (state) {
133 | is DownloadState.DOWNLOADING -> {
134 | Spacer(modifier = Modifier.height(8.dp))
135 | Row(verticalAlignment = Alignment.CenterVertically) {
136 | LinearProgressIndicator(
137 | modifier = Modifier.weight(1f),
138 | progress = state.progress
139 | )
140 | Spacer(modifier = Modifier.width(8.dp))
141 | Text(
142 | text = "${String.format("%.2f", state.speed)} M/s",
143 | style = MaterialTheme.typography.labelSmall
144 | )
145 | }
146 | }
147 |
148 | is DownloadState.ERROR -> {
149 | Spacer(modifier = Modifier.height(10.dp))
150 | Text(
151 | text = "error code: ${state.code}",
152 | style = MaterialTheme.typography.labelSmall
153 | )
154 | }
155 |
156 | DownloadState.FINISH -> {
157 | Text(
158 | text = "click to download again",
159 | style = MaterialTheme.typography.labelSmall
160 | )
161 | }
162 |
163 | else -> {}
164 | }
165 | }
166 | }
167 | }
168 |
169 | @Preview
170 | @Composable
171 | private fun MainScreenPreview() {
172 | MainScreen(beans = FakeData.resources)
173 | }
--------------------------------------------------------------------------------
/android-sample/src/main/java/com/billbook/lib/downloader/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.billbook.lib.downloader
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.update
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.sync.Mutex
15 | import kotlinx.coroutines.sync.withLock
16 | import java.io.File
17 | import javax.inject.Inject
18 |
19 | private val Context.downloadDir: File get() = File(this.filesDir, "download")
20 | private const val TAG = "MainViewModel"
21 |
22 | /**
23 | * @author xluotong@gmail.com
24 | */
25 | @HiltViewModel
26 | class MainViewModel @Inject constructor(
27 | @ApplicationContext private val context: Context,
28 | private val downloader: Downloader
29 | ) : ViewModel() {
30 |
31 | private val _states: MutableStateFlow