Migrate from Koin to Dagger/Hilt
This commit is contained in:
@@ -5,6 +5,11 @@ import android.net.ConnectivityManager
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
@@ -26,30 +31,30 @@ import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
import org.koitharu.kotatsu.utils.ext.waitForNetwork
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||
import java.io.File
|
||||
|
||||
private const val MAX_DOWNLOAD_ATTEMPTS = 3
|
||||
private const val DOWNLOAD_ERROR_DELAY = 500L
|
||||
private const val SLOWDOWN_DELAY = 200L
|
||||
|
||||
class DownloadManager(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val context: Context,
|
||||
class DownloadManager @AssistedInject constructor(
|
||||
@Assisted private val coroutineScope: CoroutineScope,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val settings: AppSettings,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
private val connectivityManager = context.getSystemService(
|
||||
Context.CONNECTIVITY_SERVICE
|
||||
Context.CONNECTIVITY_SERVICE,
|
||||
) as ConnectivityManager
|
||||
private val coverWidth = context.resources.getDimensionPixelSize(
|
||||
androidx.core.R.dimen.compat_notification_large_icon_max_width
|
||||
androidx.core.R.dimen.compat_notification_large_icon_max_width,
|
||||
)
|
||||
private val coverHeight = context.resources.getDimensionPixelSize(
|
||||
androidx.core.R.dimen.compat_notification_large_icon_max_height
|
||||
androidx.core.R.dimen.compat_notification_large_icon_max_height,
|
||||
)
|
||||
private val semaphore = Semaphore(settings.downloadsParallelism)
|
||||
|
||||
@@ -59,7 +64,7 @@ class DownloadManager(
|
||||
startId: Int,
|
||||
): ProgressJob<DownloadState> {
|
||||
val stateFlow = MutableStateFlow<DownloadState>(
|
||||
DownloadState.Queued(startId = startId, manga = manga, cover = null)
|
||||
DownloadState.Queued(startId = startId, manga = manga, cover = null),
|
||||
)
|
||||
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId)
|
||||
return ProgressJob(job, stateFlow)
|
||||
@@ -71,7 +76,8 @@ class DownloadManager(
|
||||
outState: MutableStateFlow<DownloadState>,
|
||||
startId: Int,
|
||||
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
|
||||
@Suppress("NAME_SHADOWING") var manga = manga
|
||||
@Suppress("NAME_SHADOWING")
|
||||
var manga = manga
|
||||
val chaptersIdsSet = chaptersIds?.toMutableSet()
|
||||
val cover = loadCover(manga)
|
||||
outState.value = DownloadState.Queued(startId, manga, cover)
|
||||
@@ -87,7 +93,7 @@ class DownloadManager(
|
||||
if (manga.source == MangaSource.LOCAL) {
|
||||
manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance")
|
||||
}
|
||||
val repo = MangaRepository(manga.source)
|
||||
val repo = mangaRepositoryFactory.create(manga.source)
|
||||
outState.value = DownloadState.Preparing(startId, manga, cover)
|
||||
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
||||
output = CbzMangaOutput.get(destination, data)
|
||||
@@ -100,7 +106,7 @@ class DownloadManager(
|
||||
data.chapters
|
||||
} else {
|
||||
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
|
||||
}
|
||||
},
|
||||
) { "Chapters list must not be null" }
|
||||
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
|
||||
check(chaptersIdsSet.isNullOrEmpty()) {
|
||||
@@ -134,7 +140,9 @@ class DownloadManager(
|
||||
}
|
||||
|
||||
outState.value = DownloadState.Progress(
|
||||
startId, data, cover,
|
||||
startId,
|
||||
data,
|
||||
cover,
|
||||
totalChapters = chapters.size,
|
||||
currentChapter = chapterIndex,
|
||||
totalPages = pages.size,
|
||||
@@ -203,27 +211,13 @@ class DownloadManager(
|
||||
.data(manga.coverUrl)
|
||||
.referer(manga.publicUrl)
|
||||
.size(coverWidth, coverHeight)
|
||||
.build()
|
||||
.build(),
|
||||
).drawable
|
||||
}.getOrNull()
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(coroutineScope: CoroutineScope) = DownloadManager(
|
||||
coroutineScope = coroutineScope,
|
||||
context = context,
|
||||
imageLoader = imageLoader,
|
||||
okHttp = okHttp,
|
||||
cache = cache,
|
||||
localMangaRepository = localMangaRepository,
|
||||
settings = settings,
|
||||
)
|
||||
fun create(coroutineScope: CoroutineScope): DownloadManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,29 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
import org.koitharu.kotatsu.utils.bindServiceWithLifecycle
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val adapter = DownloadsAdapter(lifecycleScope, get())
|
||||
val adapter = DownloadsAdapter(lifecycleScope, coil)
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.adapter = adapter
|
||||
bindServiceWithLifecycle(
|
||||
@@ -42,11 +48,11 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
||||
binding.recyclerView.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
binding.toolbar.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,4 +60,4 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,32 +11,34 @@ import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.set
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseService
|
||||
import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.download.domain.DownloadManager
|
||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||
import org.koitharu.kotatsu.download.domain.WakeLockNode
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.throttle
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DownloadService : BaseService() {
|
||||
|
||||
private lateinit var downloadManager: DownloadManager
|
||||
private lateinit var notificationSwitcher: ForegroundNotificationSwitcher
|
||||
|
||||
@Inject
|
||||
lateinit var downloadManagerFactory: DownloadManager.Factory
|
||||
|
||||
private val jobs = LinkedHashMap<Int, ProgressJob<DownloadState>>()
|
||||
private val jobCount = MutableStateFlow(0)
|
||||
private val controlReceiver = ControlReceiver()
|
||||
@@ -48,7 +50,7 @@ class DownloadService : BaseService() {
|
||||
notificationSwitcher = ForegroundNotificationSwitcher(this)
|
||||
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
|
||||
downloadManager = get<DownloadManager.Factory>().create(
|
||||
downloadManager = downloadManagerFactory.create(
|
||||
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)),
|
||||
)
|
||||
DownloadNotification.createChannel(this)
|
||||
@@ -122,7 +124,7 @@ class DownloadService : BaseService() {
|
||||
(job.progressValue as? DownloadState.Done)?.let {
|
||||
sendBroadcast(
|
||||
Intent(ACTION_DOWNLOAD_COMPLETE)
|
||||
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false))
|
||||
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)),
|
||||
)
|
||||
}
|
||||
notificationSwitcher.detach(
|
||||
@@ -131,7 +133,7 @@ class DownloadService : BaseService() {
|
||||
null
|
||||
} else {
|
||||
notification.create(job.progressValue, -1L)
|
||||
}
|
||||
},
|
||||
)
|
||||
stopSelf(startId)
|
||||
}
|
||||
@@ -182,27 +184,23 @@ class DownloadService : BaseService() {
|
||||
if (chaptersIds?.isEmpty() == true) {
|
||||
return
|
||||
}
|
||||
confirmDataTransfer(context) {
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
|
||||
if (chaptersIds != null) {
|
||||
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
|
||||
if (chaptersIds != null) {
|
||||
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun start(context: Context, manga: Collection<Manga>) {
|
||||
if (manga.isEmpty()) {
|
||||
return
|
||||
}
|
||||
confirmDataTransfer(context) {
|
||||
for (item in manga) {
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
for (item in manga) {
|
||||
val intent = Intent(context, DownloadService::class.java)
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,24 +223,5 @@ class DownloadService : BaseService() {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun confirmDataTransfer(context: Context, callback: () -> Unit) {
|
||||
val settings = GlobalContext.get().get<AppSettings>()
|
||||
if (context.connectivityManager.isActiveNetworkMetered && settings.isTrafficWarningEnabled) {
|
||||
CheckBoxAlertDialog.Builder(context)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.network_consumption_warning)
|
||||
.setCheckBoxText(R.string.dont_ask_again)
|
||||
.setCheckBoxChecked(false)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string._continue) { _, doNotAsk ->
|
||||
settings.isTrafficWarningEnabled = !doNotAsk
|
||||
callback()
|
||||
}.create()
|
||||
.show()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user