Improve downloads binding

This commit is contained in:
Koitharu
2023-02-14 08:04:17 +02:00
parent b6bfef6b50
commit 652351f79a
6 changed files with 108 additions and 68 deletions

View File

@@ -9,6 +9,13 @@ sealed interface DownloadState {
val manga: Manga
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(
override val startId: Int,
override val manga: Manga,

View File

@@ -1,27 +1,19 @@
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.Bundle
import android.os.IBinder
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
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.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService
import javax.inject.Inject
@AndroidEntryPoint
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@@ -29,6 +21,8 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@Inject
lateinit var coil: ImageLoader
private lateinit var serviceConnection: DownloadsConnection
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
@@ -38,9 +32,12 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
val connection = DownloadServiceConnection(adapter)
bindService(Intent(this, DownloadService::class.java), connection, 0)
lifecycle.addObserver(connection)
serviceConnection = DownloadsConnection(this, this)
serviceConnection.items.observe(this) { items ->
adapter.items = items
binding.textViewHolder.isVisible = items.isNullOrEmpty()
}
serviceConnection.bind()
}
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 {
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)

View File

@@ -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()
}
}
}

View File

@@ -66,7 +66,7 @@ class DownloadService : BaseService() {
val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
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 {
@@ -155,9 +155,6 @@ class DownloadService : BaseService() {
!state.isTerminal
}
private val DownloadState.isTerminal: Boolean
get() = this is DownloadState.Done || this is DownloadState.Cancelled || (this is DownloadState.Error && !canRetry)
@MainThread
private fun stopSelfIfIdle() {
if (jobs.any { (_, job) -> job.isActive }) {

View File

@@ -14,9 +14,8 @@
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:toolbarId="@id/toolbar">
@@ -39,7 +38,8 @@
android:paddingHorizontal="@dimen/list_spacing"
android:scrollbars="vertical"
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
android:id="@+id/textView_holder"

View File

@@ -11,8 +11,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="@dimen/manga_list_details_item_height"
android:orientation="horizontal"
android:padding="4dp">
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
@@ -31,8 +30,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
@@ -47,11 +46,11 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:progress="25"/>
tools:progress="25" />
<TextView
android:id="@+id/textView_status"
@@ -59,7 +58,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
@@ -72,7 +71,7 @@
android:id="@+id/textView_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_status"
app:layout_constraintEnd_toEndOf="parent"
@@ -84,7 +83,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:maxLines="4"
android:textAppearance="?attr/textAppearanceBodySmall"
@@ -98,6 +97,8 @@
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:text="@string/try_again"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
@@ -111,6 +112,8 @@
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:text="@android:string/cancel"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"