├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── layout_download_item.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── app │ │ │ │ └── nikhil │ │ │ │ └── coroutinedownloader │ │ │ │ ├── ui │ │ │ │ ├── ViewState.kt │ │ │ │ ├── base │ │ │ │ │ ├── ViewModelFactory.kt │ │ │ │ │ └── BaseActivity.kt │ │ │ │ └── main │ │ │ │ │ ├── MainViewModel.kt │ │ │ │ │ └── MainActivity.kt │ │ │ │ ├── injection │ │ │ │ ├── scope │ │ │ │ │ ├── ActivityScope.kt │ │ │ │ │ └── ViewModelKey.kt │ │ │ │ ├── qualifier │ │ │ │ │ └── IOScope.kt │ │ │ │ ├── module │ │ │ │ │ ├── ServiceBindingModule.kt │ │ │ │ │ ├── ActivityBindingModule.kt │ │ │ │ │ ├── ViewModelBindingModule.kt │ │ │ │ │ └── AppModule.kt │ │ │ │ └── component │ │ │ │ │ └── AppComponent.kt │ │ │ │ ├── models │ │ │ │ ├── DownloadState.kt │ │ │ │ ├── DownloadItem.kt │ │ │ │ └── DownloadProgress.kt │ │ │ │ ├── usecase │ │ │ │ ├── BaseSuspendUseCase.kt │ │ │ │ └── DownloadUseCase.kt │ │ │ │ ├── exceptions │ │ │ │ ├── FileAlreadyDownloadingException.kt │ │ │ │ ├── FileExistsException.kt │ │ │ │ └── UserCancelledJobException.kt │ │ │ │ ├── utils │ │ │ │ ├── Constants.kt │ │ │ │ ├── KotlinExtensions.kt │ │ │ │ ├── NumberUtils.kt │ │ │ │ ├── NotificationUtils.kt │ │ │ │ ├── DownloadItemRecyclerAdapter.kt │ │ │ │ └── FileUtils.kt │ │ │ │ ├── database │ │ │ │ ├── DownloadDatabase.kt │ │ │ │ ├── DatabaseDAO.kt │ │ │ │ ├── Converters.kt │ │ │ │ └── CentralRepository.kt │ │ │ │ ├── downloadutils │ │ │ │ ├── DownloadManager.kt │ │ │ │ ├── DownloadService.kt │ │ │ │ └── DownloadManagerImpl.kt │ │ │ │ └── MainApplication.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── app │ │ │ └── nikhil │ │ │ └── coroutinedownloader │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── app │ │ └── nikhil │ │ └── coroutinedownloader │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── images ├── paused.png └── completed.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── encodings.xml ├── vcs.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── modules.xml ├── misc.xml └── runConfigurations.xml ├── README.md ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── gradlew └── LICENSE /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /images/paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/images/paused.png -------------------------------------------------------------------------------- /images/completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/images/completed.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilbansal97/CoroutineDownloader/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/ui/ViewState.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.ui 2 | 3 | sealed class ViewState { 4 | class Downloading 5 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/scope/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.scope 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | annotation class ActivityScope -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/models/DownloadState.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.models 2 | 3 | enum class DownloadState { 4 | PENDING, 5 | PAUSED, 6 | COMPLETED, 7 | DOWNLOADING 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/usecase/BaseSuspendUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.usecase 2 | 3 | interface BaseSuspendUseCase { 4 | suspend fun perform(param: U): T 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/exceptions/FileAlreadyDownloadingException.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.exceptions 2 | 3 | class FileAlreadyDownloadingException : Exception("File already downloading!") -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4A6572 4 | #344955 5 | #F9AA33 6 | 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/exceptions/FileExistsException.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.exceptions 2 | 3 | class FileExistsException : Exception() { 4 | override val message: String 5 | get() = "File Already Exists" 6 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 16 10:41:07 IST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/qualifier/IOScope.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.qualifier 2 | 3 | import javax.inject.Qualifier 4 | import kotlin.annotation.AnnotationRetention.RUNTIME 5 | 6 | @Retention(RUNTIME) 7 | @Qualifier 8 | annotation class IOScope -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 8dp 5 | 12dp 6 | 16sp 7 | 6dp 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.utils 2 | 3 | object Constants { 4 | const val REQUEST_CODE_EXTERNAL_PERMISSIONS = 1001 5 | const val ACTION_DOWNLOAD = "ACTION_DOWNLOAD" 6 | const val DATABASE_NAME = "download-items-info-db" 7 | const val DATABASE_VERSION = 1 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/exceptions/UserCancelledJobException.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.exceptions 2 | 3 | import kotlinx.coroutines.CancellationException 4 | 5 | class UserCancelledJobException : CancellationException() { 6 | override val message: String? 7 | get() = "User cancelled the Job" 8 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/utils/KotlinExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.utils 2 | 3 | import android.content.ContentResolver 4 | import android.content.ContentValues 5 | import android.net.Uri 6 | import java.net.URI 7 | 8 | fun Uri.toURI(): URI = URI.create(toString()) 9 | 10 | fun ContentResolver.safeInsert(uri: Uri, contentValues: ContentValues): Uri = 11 | insert(uri, contentValues) ?: Uri.EMPTY -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/module/ServiceBindingModule.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.module 2 | 3 | import com.app.nikhil.coroutinedownloader.downloadutils.DownloadService 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | abstract class ServiceBindingModule { 9 | 10 | @ContributesAndroidInjector 11 | abstract fun bindDownloadService(): DownloadService 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/scope/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.scope 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | import kotlin.annotation.AnnotationTarget.FUNCTION 7 | import kotlin.reflect.KClass 8 | 9 | @Target(FUNCTION) 10 | @Retention(RUNTIME) 11 | @MapKey 12 | annotation class ViewModelKey (val value: KClass) -------------------------------------------------------------------------------- /app/src/test/java/com/app/nikhil/coroutinedownloader/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Coroutine Downloader 3 | Download URL 4 | Download 5 | https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_30mb.mp4 6 | Downloading 7 | Completed 8 | Pause 9 | Resume 10 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/models/DownloadItem.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.models 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Ignore 5 | import androidx.room.PrimaryKey 6 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel 7 | 8 | @Entity(tableName = "DownloadItemsTable") 9 | data class DownloadItem( 10 | @PrimaryKey 11 | val url: String, 12 | val fileName: String, 13 | var downloadProgress: DownloadProgress = DownloadProgress.EMPTY 14 | ) { 15 | @Ignore 16 | lateinit var channel: ConflatedBroadcastChannel 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/ui/base/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.ui.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | 8 | class ViewModelFactory @Inject constructor( 9 | private val map: Map, 10 | @JvmSuppressWildcards Provider> 11 | ) : 12 | ViewModelProvider.Factory { 13 | 14 | override fun create(modelClass: Class): T { 15 | return map[modelClass]?.get() as T 16 | } 17 | } -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/database/DownloadDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.app.nikhil.coroutinedownloader.models.DownloadItem 7 | import com.app.nikhil.coroutinedownloader.utils.Constants 8 | 9 | @Database( 10 | entities = [DownloadItem::class], version = Constants.DATABASE_VERSION, exportSchema = false 11 | ) 12 | @TypeConverters(Converters::class) 13 | abstract class DownloadDatabase : RoomDatabase() { 14 | abstract fun getDao(): DatabaseDAO 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/downloadutils/DownloadManager.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.downloadutils 2 | 3 | import com.app.nikhil.coroutinedownloader.models.DownloadItem 4 | import com.app.nikhil.coroutinedownloader.models.DownloadProgress 5 | import kotlinx.coroutines.channels.BroadcastChannel 6 | 7 | interface DownloadManager { 8 | suspend fun pause(downloadItem: DownloadItem) 9 | suspend fun resumeQueue() 10 | suspend fun pauseQueue() 11 | 12 | fun download(url: String): DownloadItem 13 | fun onProgressChanged(url: String, function: (item: DownloadProgress) -> Unit) 14 | fun disposeDownload(url: String) 15 | fun disposeAll() 16 | fun getChannel(url: String): BroadcastChannel? 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader 2 | 3 | import com.app.nikhil.coroutinedownloader.injection.component.DaggerAppComponent 4 | import dagger.android.AndroidInjector 5 | import dagger.android.support.DaggerApplication 6 | import timber.log.Timber 7 | import timber.log.Timber.DebugTree 8 | 9 | class MainApplication : DaggerApplication() { 10 | 11 | override fun applicationInjector(): AndroidInjector { 12 | return DaggerAppComponent.builder() 13 | .create(this) 14 | } 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | 19 | initTimber() 20 | } 21 | 22 | private fun initTimber() { 23 | if (BuildConfig.DEBUG) { 24 | Timber.plant(DebugTree()) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/app/nikhil/coroutinedownloader/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.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.getTargetContext() 22 | assertEquals("com.app.nikhil.coroutinedownloader", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/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 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/models/DownloadProgress.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.models 2 | 3 | import android.net.Uri 4 | 5 | data class DownloadProgress( 6 | var megaBytesDownloaded: String, 7 | var percentage: Int, 8 | var percentageDisplay: String, 9 | var totalMegaBytes: String, 10 | var bytesDownloaded: Long, 11 | var totalBytes: Long, 12 | var state: DownloadState, 13 | var uri: String 14 | ) { 15 | companion object { 16 | val EMPTY: DownloadProgress 17 | get() = DownloadProgress( 18 | megaBytesDownloaded = "0", 19 | percentage = 0, 20 | percentageDisplay = "0", 21 | totalMegaBytes = "0", 22 | bytesDownloaded = 0L, 23 | totalBytes = 0L, 24 | state = DownloadState.PENDING, 25 | uri = Uri.EMPTY.toString() 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/module/ActivityBindingModule.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.module 2 | 3 | import com.app.nikhil.coroutinedownloader.injection.scope.ActivityScope 4 | import com.app.nikhil.coroutinedownloader.ui.main.MainActivity 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.android.ContributesAndroidInjector 8 | import dagger.android.support.DaggerAppCompatActivity 9 | 10 | @Module 11 | abstract class ActivityBindingModule { 12 | 13 | @ActivityScope 14 | @ContributesAndroidInjector(modules = [MainActivityModule::class]) 15 | internal abstract fun bindMainActivity(): MainActivity 16 | } 17 | 18 | @Module 19 | abstract class MainActivityModule { 20 | 21 | @Binds 22 | @ActivityScope 23 | abstract fun bindActivity(mainActivity: MainActivity): DaggerAppCompatActivity 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/module/ViewModelBindingModule.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.module 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.app.nikhil.coroutinedownloader.injection.scope.ViewModelKey 6 | import com.app.nikhil.coroutinedownloader.ui.base.ViewModelFactory 7 | import com.app.nikhil.coroutinedownloader.ui.main.MainViewModel 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.multibindings.IntoMap 11 | 12 | @Module 13 | abstract class ViewModelBindingModule { 14 | 15 | @Binds 16 | @IntoMap 17 | @ViewModelKey(MainViewModel::class) 18 | abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel 19 | 20 | @Binds 21 | abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/utils/NumberUtils.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.utils 2 | 3 | import java.math.RoundingMode 4 | import java.text.DecimalFormat 5 | 6 | object NumberUtils { 7 | 8 | private const val MEGA_BYTES_MULTIPLIER = 0.000001 9 | private const val DECIMAL_PERCENT_FORMAT = "#.##" 10 | private val percentageFormat = 11 | DecimalFormat(DECIMAL_PERCENT_FORMAT).apply { 12 | roundingMode = 13 | RoundingMode.CEILING 14 | } 15 | 16 | fun getDisplayPercentage( 17 | bytesRead: Long, 18 | totalBytes: Long 19 | ): String = percentageFormat.format((bytesRead.toDouble() / totalBytes.toDouble()) * 100) 20 | 21 | fun getPercentage( 22 | bytesRead: Long, 23 | totalBytes: Long 24 | ): Int = ((bytesRead.toDouble() / totalBytes.toDouble()) * 100).toInt() 25 | 26 | fun convertBytesToMB(bytes: Long): String = 27 | percentageFormat.format(bytes * MEGA_BYTES_MULTIPLIER) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/database/DatabaseDAO.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.database 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.Dao 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.app.nikhil.coroutinedownloader.models.DownloadItem 9 | 10 | @Dao 11 | interface DatabaseDAO { 12 | 13 | @Query("SELECT * FROM DownloadItemsTable") 14 | suspend fun getAll(): List 15 | 16 | @Query("SELECT * FROM DownloadItemsTable WHERE url = :downloadUrl") 17 | suspend fun getItem(downloadUrl: String): DownloadItem 18 | 19 | @Insert(onConflict = OnConflictStrategy.REPLACE) 20 | suspend fun insert(downloadItem: DownloadItem) 21 | 22 | @Insert 23 | suspend fun insertAll(downloadItemList: List) 24 | 25 | @Query("SELECT * FROM DownloadItemsTable") 26 | fun getAllItemsLive(): LiveData> 27 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoroutineDownloader 2 | 3 | Download Manager written purely in Kotlin. The app uses coroutines and channels to manage the downloading in background and Okio to buffer. 4 | 5 | > 🚧 Contains the basic logic to download the file. The app does not use the `DownloadManager` class hence the notifications are broken. Feel free to pick it up 😛 6 | 7 | ## Screenshot 8 | 9 | 10 | 11 | 12 | 13 | ## License 14 | 15 | Copyright 2019 Nikhil Bansal 16 | 17 | Licensed under the Apache License, Version 2.0 (the "License"); 18 | you may not use this file except in compliance with the License. 19 | You may obtain a copy of the License at 20 | 21 | http://www.apache.org/licenses/LICENSE-2.0 22 | 23 | Unless required by applicable law or agreed to in writing, software 24 | distributed under the License is distributed on an "AS IS" BASIS, 25 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | See the License for the specific language governing permissions and 27 | limitations under the License. 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/usecase/DownloadUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.usecase 2 | 3 | import com.app.nikhil.coroutinedownloader.database.CentralRepository 4 | import com.app.nikhil.coroutinedownloader.downloadutils.DownloadManager 5 | import com.app.nikhil.coroutinedownloader.models.DownloadItem 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import javax.inject.Inject 10 | 11 | class DownloadUseCase @Inject constructor( 12 | private val downloadManager: DownloadManager, 13 | private val centralRepository: CentralRepository 14 | ) : BaseSuspendUseCase { 15 | 16 | private val downloadScope = CoroutineScope(Dispatchers.IO) 17 | 18 | override suspend fun perform(param: String): DownloadItem { 19 | val item = downloadManager.download(param) 20 | // Launch a separate coroutine since this will start a database transaction which is 21 | // synchronous and hence blocking. 22 | downloadScope.launch { centralRepository.startSavingDownloadProgress(item) } 23 | return item 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/database/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.database 2 | 3 | import androidx.room.TypeConverter 4 | import com.app.nikhil.coroutinedownloader.models.DownloadProgress 5 | import com.app.nikhil.coroutinedownloader.models.DownloadState 6 | import com.google.gson.Gson 7 | 8 | class Converters { 9 | 10 | private val gson = Gson() 11 | 12 | @TypeConverter 13 | fun fromDownloadState(state: DownloadState): Int = state.ordinal 14 | 15 | @TypeConverter 16 | fun toDownloadState(value: Int): DownloadState { 17 | return when (value) { 18 | DownloadState.PENDING.ordinal -> DownloadState.PENDING 19 | DownloadState.DOWNLOADING.ordinal -> DownloadState.DOWNLOADING 20 | DownloadState.COMPLETED.ordinal -> DownloadState.COMPLETED 21 | else -> DownloadState.PAUSED 22 | } 23 | } 24 | 25 | @TypeConverter 26 | fun fromDownloadProgress(progress: DownloadProgress): String { 27 | return gson.toJson(progress) 28 | } 29 | 30 | @TypeConverter 31 | fun toDownloadProgress(progressString: String): DownloadProgress { 32 | return gson.fromJson(progressString, DownloadProgress::class.java) 33 | } 34 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | .idea/*.xml 45 | .idea/* 46 | 47 | # Keystore files 48 | # Uncomment the following line if you do not want to check your keystore files in. 49 | #*.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | 57 | # Freeline 58 | freeline.py 59 | freeline/ 60 | freeline_project_description.json 61 | 62 | # fastlane 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | fastlane/readme.md 68 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/component/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.component 2 | 3 | import android.content.Context 4 | import com.app.nikhil.coroutinedownloader.MainApplication 5 | import com.app.nikhil.coroutinedownloader.injection.module.ActivityBindingModule 6 | import com.app.nikhil.coroutinedownloader.injection.module.AppModule 7 | import com.app.nikhil.coroutinedownloader.injection.module.ServiceBindingModule 8 | import com.app.nikhil.coroutinedownloader.injection.module.ViewModelBindingModule 9 | import dagger.BindsInstance 10 | import dagger.Component 11 | import dagger.android.AndroidInjector 12 | import dagger.android.support.AndroidSupportInjectionModule 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | @Component( 17 | modules = [ 18 | AndroidSupportInjectionModule::class, 19 | AppModule::class, 20 | ViewModelBindingModule::class, 21 | ActivityBindingModule::class, 22 | ServiceBindingModule::class] 23 | ) 24 | interface AppComponent : AndroidInjector { 25 | 26 | /* 27 | * Customize the builder generated by the dagger compiler 28 | */ 29 | @Component.Builder 30 | abstract class Builder : AndroidInjector.Builder() { 31 | @BindsInstance 32 | abstract fun appContext(context: Context) 33 | 34 | override fun seedInstance(instance: MainApplication) { 35 | appContext(instance.applicationContext) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/injection/module/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.injection.module 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.app.nikhil.coroutinedownloader.database.DownloadDatabase 6 | import com.app.nikhil.coroutinedownloader.downloadutils.DownloadManager 7 | import com.app.nikhil.coroutinedownloader.downloadutils.DownloadManagerImpl 8 | import com.app.nikhil.coroutinedownloader.injection.qualifier.IOScope 9 | import com.app.nikhil.coroutinedownloader.utils.Constants 10 | import com.app.nikhil.coroutinedownloader.utils.FileUtils 11 | import com.app.nikhil.coroutinedownloader.utils.NotificationUtils 12 | import dagger.Module 13 | import dagger.Provides 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.Dispatchers 16 | import okhttp3.OkHttpClient 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | class AppModule { 21 | 22 | @Provides 23 | @Singleton 24 | fun provideFileUtils(context: Context): FileUtils = FileUtils(context) 25 | 26 | @Provides 27 | @Singleton 28 | fun provideOkHttpClient(): OkHttpClient = OkHttpClient() 29 | 30 | @Provides 31 | @Singleton 32 | fun provideDatabase(context: Context): DownloadDatabase { 33 | return Room.databaseBuilder(context, DownloadDatabase::class.java, Constants.DATABASE_NAME).build() 34 | } 35 | 36 | @Provides 37 | @Singleton 38 | fun provideNotificationUtils(context: Context): NotificationUtils { 39 | return NotificationUtils(context) 40 | } 41 | 42 | @Provides 43 | @Singleton 44 | fun provideDownloader( 45 | okHttpClient: OkHttpClient, 46 | fileUtils: FileUtils, 47 | @IOScope scope: CoroutineScope 48 | ): DownloadManager = DownloadManagerImpl(okHttpClient, fileUtils, scope) 49 | 50 | @Provides 51 | @IOScope 52 | fun provideIOCoroutineScope(): CoroutineScope { 53 | return CoroutineScope(Dispatchers.IO) 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/utils/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.utils 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.content.Context.NOTIFICATION_SERVICE 8 | import android.os.Build.VERSION 9 | import android.os.Build.VERSION_CODES 10 | import androidx.core.app.NotificationCompat 11 | import com.app.nikhil.coroutinedownloader.R 12 | import javax.inject.Inject 13 | 14 | class NotificationUtils @Inject constructor(private val context: Context) { 15 | 16 | companion object { 17 | private const val CHANNEL_ID = "CHANNEL-001" 18 | private const val CHANNEL_NAME = "CoroutineDownloader Channel" 19 | } 20 | 21 | private var currentId = 2 22 | 23 | private val manager: NotificationManager by lazy { 24 | context.getSystemService( 25 | NOTIFICATION_SERVICE 26 | ) as NotificationManager 27 | } 28 | 29 | private val builder: NotificationCompat.Builder by lazy { 30 | val builder = NotificationCompat.Builder(context) 31 | .setSmallIcon(R.mipmap.ic_launcher) 32 | 33 | if (VERSION.SDK_INT >= VERSION_CODES.O) { 34 | builder.apply { 35 | setChannelId(CHANNEL_ID) 36 | setOngoing(true) 37 | } 38 | } 39 | return@lazy builder 40 | } 41 | 42 | fun createNotification( 43 | text: String 44 | ): Notification { 45 | builder.setContentText(text) 46 | if (VERSION.SDK_INT >= VERSION_CODES.O) { 47 | manager.createNotificationChannel( 48 | NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) 49 | ) 50 | } 51 | return builder.build() 52 | } 53 | 54 | fun getSimpleBuilder(): NotificationCompat.Builder = builder 55 | 56 | fun showNotification(notification: Notification) { 57 | manager.notify(currentId++, notification) 58 | } 59 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | defaultConfig { 10 | applicationId "com.app.nikhil.coroutinedownloader" 11 | minSdkVersion 21 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | dataBinding { 24 | enabled = true 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 31 | 32 | implementation coreDeps.appCompat 33 | implementation coreDeps.androidKtxCore 34 | implementation coreDeps.constraintLayout 35 | implementation coreDeps.recyclerView 36 | implementation coreDeps.materialComponents 37 | implementation coreDeps.viewModelLifecycle 38 | 39 | implementation coreDeps.room 40 | implementation coreDeps.roomKtx 41 | kapt coreDeps.roomProcessor 42 | 43 | implementation coreDeps.okio 44 | implementation coreDeps.okhttp 45 | implementation coreDeps.timber 46 | implementation coreDeps.gson 47 | 48 | implementation coreDeps.coroutineCore 49 | implementation coreDeps.coroutineAndroid 50 | 51 | implementation coreDeps.dagger 52 | implementation coreDeps.daggerAndroid 53 | implementation coreDeps.daggerSupport 54 | implementation coreDeps.daggerSupport 55 | kapt coreDeps.daggerAndroidProcessor 56 | kapt coreDeps.daggerProcessor 57 | 58 | testImplementation testDeps.junit 59 | androidTestImplementation testDeps.testRunner 60 | androidTestImplementation testDeps.espressoCore 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/app/nikhil/coroutinedownloader/database/CentralRepository.kt: -------------------------------------------------------------------------------- 1 | package com.app.nikhil.coroutinedownloader.database 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.app.nikhil.coroutinedownloader.models.DownloadItem 5 | import com.app.nikhil.coroutinedownloader.models.DownloadState.PAUSED 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.channels.consumeEach 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.withContext 10 | import timber.log.Timber 11 | import javax.inject.Inject 12 | 13 | class CentralRepository @Inject constructor(private val database: DownloadDatabase) { 14 | 15 | suspend fun getAllDownloadItems(): List { 16 | return withContext(Dispatchers.IO) { 17 | database.getDao().getAll() 18 | } 19 | } 20 | 21 | fun getAllDownloadItemsLive(): LiveData> { 22 | return database.getDao().getAllItemsLive() 23 | } 24 | 25 | suspend fun saveAllDownloadItems(downloadItemList: List) { 26 | withContext(Dispatchers.IO) { 27 | for (item: DownloadItem in downloadItemList) { 28 | item.downloadProgress.state = PAUSED 29 | } 30 | database.getDao().insertAll(downloadItemList) 31 | } 32 | } 33 | 34 | suspend fun saveDownloadItem(item: DownloadItem) { 35 | withContext(Dispatchers.IO) { 36 | database.getDao().insert(item) 37 | } 38 | } 39 | 40 | suspend fun startSavingDownloadProgress(item: DownloadItem) { 41 | withContext(Dispatchers.IO) { 42 | Timber.d("[CDM] Starting a SQLite transaction") 43 | database.runInTransaction { 44 | this.launch { 45 | Timber.d("[CDM] Starting consuming the channel") 46 | item.channel.consumeEach { downloadProgress -> 47 | Timber.d("[CDM] received download progress ${downloadProgress.percentageDisplay}") 48 | database.getDao().insert(item) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 12 | 13 | 20 | 21 | 28 | 29 | 34 | 35 | 36 | 37 |