Improve downloads binding
This commit is contained in:
@@ -9,6 +9,13 @@ sealed interface DownloadState {
|
|||||||
val manga: Manga
|
val manga: Manga
|
||||||
val cover: Drawable?
|
val cover: Drawable?
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean
|
||||||
|
|
||||||
|
override fun hashCode(): Int
|
||||||
|
|
||||||
|
val isTerminal: Boolean
|
||||||
|
get() = this is Done || this is Cancelled || (this is Error && !canRetry)
|
||||||
|
|
||||||
class Queued(
|
class Queued(
|
||||||
override val startId: Int,
|
override val startId: Int,
|
||||||
override val manga: Manga,
|
override val manga: Manga,
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.download.ui
|
package org.koitharu.kotatsu.download.ui
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
|
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
||||||
@@ -29,6 +21,8 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
private lateinit var serviceConnection: DownloadsConnection
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
||||||
@@ -38,9 +32,12 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
|||||||
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
val connection = DownloadServiceConnection(adapter)
|
serviceConnection = DownloadsConnection(this, this)
|
||||||
bindService(Intent(this, DownloadService::class.java), connection, 0)
|
serviceConnection.items.observe(this) { items ->
|
||||||
lifecycle.addObserver(connection)
|
adapter.items = items
|
||||||
|
binding.textViewHolder.isVisible = items.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
serviceConnection.bind()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
@@ -55,46 +52,6 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class DownloadServiceConnection(
|
|
||||||
private val adapter: DownloadsAdapter,
|
|
||||||
) : ServiceConnection, DefaultLifecycleObserver {
|
|
||||||
|
|
||||||
private var collectJob: Job? = null
|
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
collectJob?.cancel()
|
|
||||||
val binder = (service as? DownloadService.DownloadBinder)
|
|
||||||
collectJob = if (binder == null) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
binder.downloads.collect {
|
|
||||||
setItems(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
collectJob?.cancel()
|
|
||||||
collectJob = null
|
|
||||||
setItems(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
|
||||||
super.onDestroy(owner)
|
|
||||||
collectJob?.cancel()
|
|
||||||
collectJob = null
|
|
||||||
owner.lifecycle.removeObserver(this)
|
|
||||||
unbindService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setItems(items: Collection<DownloadItem>?) {
|
|
||||||
adapter.items = items?.toList().orEmpty()
|
|
||||||
binding.textViewHolder.isVisible = items.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)
|
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||||
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
|
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
||||||
|
|
||||||
|
class DownloadsConnection(
|
||||||
|
private val context: Context,
|
||||||
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
|
) : ServiceConnection {
|
||||||
|
|
||||||
|
private var bindingObserver: BindingLifecycleObserver? = null
|
||||||
|
private var collectJob: Job? = null
|
||||||
|
private val itemsFlow = MutableStateFlow<List<PausingProgressJob<DownloadState>>>(emptyList())
|
||||||
|
|
||||||
|
val items
|
||||||
|
get() = itemsFlow.asFlowLiveData()
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
collectJob?.cancel()
|
||||||
|
val binder = (service as? DownloadService.DownloadBinder)
|
||||||
|
collectJob = if (binder == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
binder.downloads.collect {
|
||||||
|
itemsFlow.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
collectJob?.cancel()
|
||||||
|
collectJob = null
|
||||||
|
itemsFlow.value = itemsFlow.value.filter { it.progressValue.isTerminal }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind() {
|
||||||
|
if (bindingObserver != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bindingObserver = BindingLifecycleObserver().also {
|
||||||
|
lifecycleOwner.lifecycle.addObserver(it)
|
||||||
|
}
|
||||||
|
context.bindService(Intent(context, DownloadService::class.java), this, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unbind() {
|
||||||
|
bindingObserver?.let {
|
||||||
|
lifecycleOwner.lifecycle.removeObserver(it)
|
||||||
|
}
|
||||||
|
bindingObserver = null
|
||||||
|
context.unbindService(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class BindingLifecycleObserver : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
super.onDestroy(owner)
|
||||||
|
unbind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@ class DownloadService : BaseService() {
|
|||||||
val intentFilter = IntentFilter()
|
val intentFilter = IntentFilter()
|
||||||
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
|
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
|
||||||
intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
|
intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
|
||||||
registerReceiver(controlReceiver, intentFilter)
|
ContextCompat.registerReceiver(this, controlReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -155,9 +155,6 @@ class DownloadService : BaseService() {
|
|||||||
!state.isTerminal
|
!state.isTerminal
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DownloadState.isTerminal: Boolean
|
|
||||||
get() = this is DownloadState.Done || this is DownloadState.Cancelled || (this is DownloadState.Error && !canRetry)
|
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
private fun stopSelfIfIdle() {
|
private fun stopSelfIfIdle() {
|
||||||
if (jobs.any { (_, job) -> job.isActive }) {
|
if (jobs.any { (_, job) -> job.isActive }) {
|
||||||
|
|||||||
@@ -14,9 +14,8 @@
|
|||||||
|
|
||||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
android:id="@+id/collapsingToolbarLayout"
|
android:id="@+id/collapsingToolbarLayout"
|
||||||
style="?attr/collapsingToolbarLayoutLargeStyle"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
|
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||||
app:toolbarId="@id/toolbar">
|
app:toolbarId="@id/toolbar">
|
||||||
|
|
||||||
@@ -39,7 +38,8 @@
|
|||||||
android:paddingHorizontal="@dimen/list_spacing"
|
android:paddingHorizontal="@dimen/list_spacing"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
|
tools:listitem="@layout/item_download" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView_holder"
|
android:id="@+id/textView_holder"
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:minHeight="@dimen/manga_list_details_item_height"
|
android:minHeight="@dimen/manga_list_details_item_height"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal">
|
||||||
android:padding="4dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/imageView_cover"
|
android:id="@+id/imageView_cover"
|
||||||
@@ -31,8 +30,8 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
@@ -47,11 +46,11 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="12dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||||
tools:progress="25"/>
|
tools:progress="25" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView_status"
|
android:id="@+id/textView_status"
|
||||||
@@ -59,7 +58,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
@@ -72,7 +71,7 @@
|
|||||||
android:id="@+id/textView_percent"
|
android:id="@+id/textView_percent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
app:layout_constraintBaseline_toBaselineOf="@id/textView_status"
|
app:layout_constraintBaseline_toBaselineOf="@id/textView_status"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@@ -84,7 +83,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="4"
|
android:maxLines="4"
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
@@ -98,6 +97,8 @@
|
|||||||
style="@style/Widget.Material3.Button.TextButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
android:text="@string/try_again"
|
android:text="@string/try_again"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
@@ -111,6 +112,8 @@
|
|||||||
style="@style/Widget.Material3.Button.TextButton"
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
android:text="@android:string/cancel"
|
android:text="@android:string/cancel"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
|||||||
Reference in New Issue
Block a user