Add Download Fragment which show active downloads

This commit is contained in:
tzugen 2021-10-14 19:25:25 +02:00
parent 9bc19ec044
commit 1d5b335f97
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
29 changed files with 389 additions and 112 deletions

View File

@ -11,9 +11,9 @@ data class Artist(
override var coverArt: String? = null,
override var albumCount: Long? = null,
override var closeness: Int = 0
) : ArtistOrIndex(id), Comparable<Artist> {
) : ArtistOrIndex(id) {
override fun compareTo(other: Artist): Int {
fun compareTo(other: Artist): Int {
when {
this.closeness == other.closeness -> {
return 0
@ -26,4 +26,6 @@ data class Artist(
}
}
}
override fun compareTo(other: Identifiable) = compareTo(other as Artist)
}

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.domain
import androidx.room.Ignore
open class ArtistOrIndex(
abstract class ArtistOrIndex(
@Ignore
override var id: String,
@Ignore
@ -15,4 +15,4 @@ open class ArtistOrIndex(
open var albumCount: Long? = null,
@Ignore
open var closeness: Int = 0
) : GenericEntry()
) : GenericEntry(id)

View File

@ -1,23 +0,0 @@
package org.moire.ultrasonic.domain
import androidx.room.Ignore
open class GenericEntry {
// TODO Should be non-null!
@Ignore
open val id: String? = null
@Ignore
open val name: String? = null
// These are just a formality and will never be called,
// because Kotlin data classes will have autogenerated equals() and hashCode() functions
override operator fun equals(other: Any?): Boolean {
return this === other
}
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + (name?.hashCode() ?: 0)
return result
}
}

View File

@ -7,8 +7,8 @@ import java.io.Serializable
@Entity
data class Genre(
@PrimaryKey val index: String,
override val name: String
) : Serializable, GenericEntry() {
val name: String
) : Serializable {
companion object {
private const val serialVersionUID = -3943025175219134028L
}

View File

@ -0,0 +1,17 @@
package org.moire.ultrasonic.domain
import androidx.room.Ignore
open class GenericEntry(
@Ignore override val id: String
) : Identifiable {
@Ignore
open val name: String? = null
override fun compareTo(other: Identifiable): Int {
return this.id.toInt().compareTo(other.id.toInt())
}
}
interface Identifiable : Comparable<Identifiable> {
val id: String
}

View File

@ -69,7 +69,7 @@ class MusicDirectory {
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null
) : Serializable, GenericEntry(), Comparable<Entry> {
) : Serializable, GenericEntry(id) {
fun setDuration(duration: Long) {
this.duration = duration.toInt()
}
@ -78,7 +78,7 @@ class MusicDirectory {
private const val serialVersionUID = -3339106650010798108L
}
override fun compareTo(other: Entry): Int {
fun compareTo(other: Entry): Int {
when {
this.closeness == other.closeness -> {
return 0
@ -91,5 +91,7 @@ class MusicDirectory {
}
}
}
override fun compareTo(other: Identifiable) = compareTo(other as Entry)
}
}

View File

@ -10,4 +10,4 @@ import androidx.room.PrimaryKey
data class MusicFolder(
@PrimaryKey override val id: String,
override val name: String
) : GenericEntry()
) : GenericEntry(id)

View File

@ -10,7 +10,7 @@ data class Playlist @JvmOverloads constructor(
val songCount: String = "",
val created: String = "",
val public: Boolean? = null
) : Serializable, GenericEntry() {
) : Serializable, GenericEntry(id) {
companion object {
private const val serialVersionUID = -4160515427075433798L
}

View File

@ -8,7 +8,7 @@ data class PodcastsChannel(
val url: String?,
val description: String?,
val status: String?
) : Serializable, GenericEntry() {
) : Serializable, GenericEntry(id) {
companion object {
private const val serialVersionUID = -4160515427075433798L
}

View File

@ -4,7 +4,7 @@ import java.io.Serializable
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Share(
override var id: String? = null,
override var id: String,
var url: String? = null,
var description: String? = null,
var username: String? = null,
@ -13,7 +13,7 @@ data class Share(
var expires: String? = null,
var visitCount: Long? = null,
private val entries: MutableList<Entry> = mutableListOf()
) : Serializable, GenericEntry() {
) : Serializable, GenericEntry(id) {
override val name: String?
get() {
if (url != null) {

View File

@ -232,7 +232,7 @@ public class JukeboxMediaPlayer
tasks.remove(Start.class);
List<String> ids = new ArrayList<>();
for (DownloadFile file : downloader.getDownloads())
for (DownloadFile file : downloader.getAll())
{
ids.add(file.getSong().getId());
}

View File

@ -8,3 +8,5 @@ public abstract class Supplier<T>
{
public abstract T get();
}

View File

@ -200,7 +200,7 @@ public class CacheCleaner
Lazy<Downloader> downloader = inject(Downloader.class);
for (DownloadFile downloadFile : downloader.getValue().getDownloads())
for (DownloadFile downloadFile : downloader.getValue().getAll())
{
filesToNotDelete.add(downloadFile.getPartialFile());
filesToNotDelete.add(downloadFile.getCompleteOrSaveFile());

View File

@ -108,6 +108,7 @@ class NavigationActivity : AppCompatActivity() {
R.id.mediaLibraryFragment,
R.id.searchFragment,
R.id.playlistsFragment,
R.id.downloadsFragment,
R.id.sharesFragment,
R.id.bookmarksFragment,
R.id.chatFragment,

View File

@ -14,7 +14,7 @@ import org.moire.ultrasonic.util.Constants
* Displays a list of Albums from the media library
* TODO: Check refresh is working
*/
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
class AlbumListFragment : EntryListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
/**
* The ViewModel to use to get the data

View File

@ -28,7 +28,7 @@ import timber.log.Timber
* Creates a Row in a RecyclerView which contains the details of an Album
*/
class AlbumRowAdapter(
albumList: List<MusicDirectory.Entry>,
itemList: List<MusicDirectory.Entry>,
onItemClick: (MusicDirectory.Entry) -> Unit,
onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
private val imageLoader: ImageLoader,
@ -40,27 +40,23 @@ class AlbumRowAdapter(
onMusicFolderUpdate
) {
init {
super.submitList(itemList)
}
private val starDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_full)
private val starHollowDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
override var itemList = albumList
// Set our layout files
override val layout = R.layout.album_list_item
override val contextMenuLayout = R.menu.artist_context_menu
// Sets the data to be displayed in the RecyclerView
override fun setData(data: List<MusicDirectory.Entry>) {
itemList = data
super.notifyDataSetChanged()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val listPosition = if (selectFolderHeader != null) position - 1 else position
val entry = itemList[listPosition]
val entry = currentList[listPosition]
holder.album.text = entry.title
holder.artist.text = entry.artist
holder.details.setOnClickListener { onItemClick(entry) }
@ -78,9 +74,9 @@ class AlbumRowAdapter(
override fun getItemCount(): Int {
if (selectFolderHeader != null)
return itemList.size + 1
return currentList.size + 1
else
return itemList.size
return currentList.size
}
/**

View File

@ -10,7 +10,7 @@ import org.moire.ultrasonic.util.Constants
/**
* Displays the list of Artists from the media library
*/
class ArtistListFragment : GenericListFragment<ArtistOrIndex, ArtistRowAdapter>() {
class ArtistListFragment : EntryListFragment<ArtistOrIndex, ArtistRowAdapter>() {
/**
* The ViewModel to use to get the data

View File

@ -23,6 +23,7 @@ import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.text.Collator
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.service.MusicService
@ -63,6 +64,11 @@ class ArtistListModel(application: Application) : GenericListModel(application)
result = musicService.getIndexes(musicFolderId, refresh)
}
artists.postValue(result.toMutableList())
artists.postValue(result.toMutableList().sortedWith(comparator))
}
companion object {
val comparator: Comparator<ArtistOrIndex> =
compareBy(Collator.getInstance()) { t -> t.name }
}
}

View File

@ -11,7 +11,6 @@ import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
import java.text.Collator
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.imageloader.ImageLoader
@ -22,7 +21,7 @@ import org.moire.ultrasonic.util.Settings
* Creates a Row in a RecyclerView which contains the details of an Artist
*/
class ArtistRowAdapter(
artistList: List<ArtistOrIndex>,
itemList: List<ArtistOrIndex>,
onItemClick: (ArtistOrIndex) -> Unit,
onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
private val imageLoader: ImageLoader,
@ -34,32 +33,26 @@ class ArtistRowAdapter(
),
SectionedAdapter {
override var itemList = artistList
init {
super.submitList(itemList)
}
// Set our layout files
override val layout = R.layout.artist_list_item
override val contextMenuLayout = R.menu.artist_context_menu
/**
* Sets the data to be displayed in the RecyclerView
*/
override fun setData(data: List<ArtistOrIndex>) {
itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
super.notifyDataSetChanged()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val listPosition = if (selectFolderHeader != null) position - 1 else position
holder.textView.text = itemList[listPosition].name
holder.textView.text = currentList[listPosition].name
holder.section.text = getSectionForArtist(listPosition)
holder.layout.setOnClickListener { onItemClick(itemList[listPosition]) }
holder.layout.setOnClickListener { onItemClick(currentList[listPosition]) }
holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
holder.coverArtId = itemList[listPosition].coverArt
holder.coverArtId = currentList[listPosition].coverArt
if (Settings.shouldShowArtistPicture) {
holder.coverArt.visibility = View.VISIBLE
val key = FileUtil.getArtistArtKey(itemList[listPosition].name, false)
val key = FileUtil.getArtistArtKey(currentList[listPosition].name, false)
imageLoader.loadImage(
view = holder.coverArt,
id = holder.coverArtId,
@ -81,18 +74,18 @@ class ArtistRowAdapter(
// scrolled up to the "Select Folder" row
if (listPosition < 0) listPosition = 0
return getSectionFromName(itemList[listPosition].name ?: " ")
return getSectionFromName(currentList[listPosition].name ?: " ")
}
private fun getSectionForArtist(artistPosition: Int): String {
if (artistPosition == 0)
return getSectionFromName(itemList[artistPosition].name ?: " ")
return getSectionFromName(currentList[artistPosition].name ?: " ")
val previousArtistSection = getSectionFromName(
itemList[artistPosition - 1].name ?: " "
currentList[artistPosition - 1].name ?: " "
)
val currentArtistSection = getSectionFromName(
itemList[artistPosition].name ?: " "
currentList[artistPosition].name ?: " "
)
return if (previousArtistSection == currentArtistSection) "" else currentArtistSection

View File

@ -0,0 +1,225 @@
package org.moire.ultrasonic.fragment
import android.app.Application
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.DownloadStatus
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SongView
class DownloadsFragment : GenericListFragment<DownloadFile, DownloadRowAdapter>() {
/**
* The ViewModel to use to get the data
*/
override val listModel: DownloadListModel by viewModels()
/**
* The id of the main layout
*/
override val mainLayout: Int = R.layout.generic_list
/**
* The id of the refresh view
*/
override val refreshListId: Int = R.id.generic_list_refresh
/**
* The id of the RecyclerView
*/
override val recyclerViewId = R.id.generic_list_recycler
/**
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
// FIXME
override val itemClickTarget: Int = R.id.trackCollectionFragment
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<DownloadFile>> {
return listModel.getList()
}
/**
* Provide the Adapter for the RecyclerView with a lazy delegate
*/
override val viewAdapter: DownloadRowAdapter by lazy {
DownloadRowAdapter(
liveDataItems.value ?: listOf(),
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
onMusicFolderUpdate,
requireContext(),
viewLifecycleOwner
)
}
override fun onContextMenuItemSelected(menuItem: MenuItem, item: DownloadFile): Boolean {
// Do nothing
return true
}
override fun onItemClick(item: DownloadFile) {
// Do nothing
}
override fun setTitle(title: String?) {
FragmentTitle.setTitle(this, Util.appContext().getString(R.string.menu_downloads))
}
}
class DownloadRowAdapter(
itemList: List<DownloadFile>,
onItemClick: (DownloadFile) -> Unit,
onContextMenuClick: (MenuItem, DownloadFile) -> Boolean,
onMusicFolderUpdate: (String?) -> Unit,
context: Context,
val lifecycleOwner: LifecycleOwner
) : GenericRowAdapter<DownloadFile>(
onItemClick,
onContextMenuClick,
onMusicFolderUpdate
) {
init {
super.submitList(itemList)
}
private val starDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_full)
private val starHollowDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_hollow)
// Set our layout files
override val layout = R.layout.song_list_item
override val contextMenuLayout = R.menu.artist_context_menu
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val downloadFile = currentList[position]
val entry = downloadFile.song
holder.title.text = entry.title
holder.artist.text = entry.artist
holder.star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
// Observe download status
downloadFile.status.observe(
lifecycleOwner,
{
updateDownloadStatus(downloadFile, holder)
}
)
downloadFile.progress.observe(
lifecycleOwner,
{
updateDownloadStatus(downloadFile, holder)
}
)
}
}
private fun updateDownloadStatus(
downloadFile: DownloadFile,
holder: ViewHolder
) {
var image: Drawable? = null
when (downloadFile.status.value) {
DownloadStatus.DONE -> {
image = if (downloadFile.isSaved) SongView.pinImage else SongView.downloadedImage
holder.status.text = null
}
DownloadStatus.DOWNLOADING -> {
holder.status.text = Util.formatPercentage(downloadFile.progress.value!!)
image = SongView.downloadingImage
}
else -> {
holder.status.text = null
}
}
// TODO: Migrate the image animation stuff from SongView into this class
//
// if (image != null) {
// holder.status.setCompoundDrawablesWithIntrinsicBounds(
// image, null, image, null
// )
// }
//
// if (image === SongView.downloadingImage) {
// val frameAnimation = image as AnimationDrawable
//
// frameAnimation.setVisible(true, true)
// frameAnimation.start()
// }
}
/**
* Holds the view properties of an Item row
*/
class ViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
var check: CheckedTextView = view.findViewById(R.id.song_check)
var rating: LinearLayout = view.findViewById(R.id.song_rating)
var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
var star: ImageView = view.findViewById(R.id.song_star)
var drag: ImageView = view.findViewById(R.id.song_drag)
var track: TextView = view.findViewById(R.id.song_track)
var title: TextView = view.findViewById(R.id.song_title)
var artist: TextView = view.findViewById(R.id.song_artist)
var duration: TextView = view.findViewById(R.id.song_duration)
var status: TextView = view.findViewById(R.id.song_status)
init {
drag.isVisible = false
star.isVisible = false
fiveStar1.isVisible = false
fiveStar2.isVisible = false
fiveStar3.isVisible = false
fiveStar4.isVisible = false
fiveStar5.isVisible = false
check.isVisible = false
}
}
/**
* Creates an instance of our ViewHolder class
*/
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
return ViewHolder(view)
}
}
class DownloadListModel(application: Application) : GenericListModel(application) {
private val downloader by inject<Downloader>()
fun getList(): LiveData<List<DownloadFile>> {
return downloader.observableList
}
}

View File

@ -18,6 +18,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@ -31,7 +32,7 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
* @param T: The type of data which will be used (must extend GenericEntry)
* @param TA: The Adapter to use (must extend GenericRowAdapter)
*/
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
abstract class GenericListFragment<T : Identifiable, TA : GenericRowAdapter<T>> : Fragment() {
internal val activeServerProvider: ActiveServerProvider by inject()
internal val serverSettingsModel: ServerSettingsModel by viewModel()
internal val imageLoaderProvider: ImageLoaderProvider by inject()
@ -90,7 +91,6 @@ abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>>
@Suppress("CommentOverPrivateProperty")
private val musicFolderObserver = { folders: List<MusicFolder> ->
viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
Unit
}
/**
@ -114,7 +114,7 @@ abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>>
!listModel.isOffline() && !Settings.shouldUseId3Tags
}
fun setTitle(title: String?) {
open fun setTitle(title: String?) {
if (title == null) {
FragmentTitle.setTitle(
this,
@ -143,7 +143,7 @@ abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>>
liveDataItems = getLiveData(arguments)
// Register an observer to update our UI when the data changes
liveDataItems.observe(viewLifecycleOwner, { newItems -> viewAdapter.setData(newItems) })
liveDataItems.observe(viewLifecycleOwner, { newItems -> viewAdapter.submitList(newItems) })
// Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
@ -176,8 +176,15 @@ abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>>
return inflater.inflate(mainLayout, container, false)
}
abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean
abstract fun onItemClick(item: T)
}
abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
GenericListFragment<T, TA>() {
@Suppress("LongMethod")
fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
val isArtist = (item is Artist)
when (menuItem.itemId) {
@ -263,7 +270,7 @@ abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>>
return true
}
open fun onItemClick(item: T) {
override fun onItemClick(item: T) {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)

View File

@ -7,6 +7,7 @@
package org.moire.ultrasonic.fragment
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MenuInflater
import android.view.MenuItem
@ -21,20 +22,19 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.view.SelectMusicFolderView
/*
* An abstract Adapter, which can be extended to display a List of <T> in a RecyclerView
*/
abstract class GenericRowAdapter<T : GenericEntry>(
abstract class GenericRowAdapter<T : Identifiable>(
val onItemClick: (T) -> Unit,
val onContextMenuClick: (MenuItem, T) -> Boolean,
private val onMusicFolderUpdate: (String?) -> Unit
) : ListAdapter<T, RecyclerView.ViewHolder>(GenericDiffCallback()) {
open var itemList: List<T> = listOf()
protected abstract val layout: Int
protected abstract val contextMenuLayout: Int
@ -43,15 +43,6 @@ abstract class GenericRowAdapter<T : GenericEntry>(
var musicFolders: List<MusicFolder> = listOf()
var selectedFolder: String? = null
/**
* Sets the data to be displayed in the RecyclerView,
* using DiffUtil to efficiently calculate the minimum required changes..
*/
open fun setData(data: List<T>) {
submitList(data)
itemList = data
}
/**
* Sets the content and state of the music folder selector row
*/
@ -101,9 +92,9 @@ abstract class GenericRowAdapter<T : GenericEntry>(
override fun getItemCount(): Int {
if (selectFolderHeader != null)
return itemList.size + 1
return currentList.size + 1
else
return itemList.size
return currentList.size
}
override fun getItemViewType(position: Int): Int {
@ -119,7 +110,7 @@ abstract class GenericRowAdapter<T : GenericEntry>(
downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline()
popup.setOnMenuItemClickListener { menuItem ->
onContextMenuClick(menuItem, itemList[position])
onContextMenuClick(menuItem, currentList[position])
}
popup.show()
return true
@ -145,7 +136,8 @@ abstract class GenericRowAdapter<T : GenericEntry>(
/**
* Calculates the differences between data sets
*/
class GenericDiffCallback<T : GenericEntry> : DiffUtil.ItemCallback<T>() {
class GenericDiffCallback<T : Identifiable> : DiffUtil.ItemCallback<T>() {
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
}

View File

@ -19,6 +19,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@ -35,7 +36,7 @@ import timber.log.Timber
class DownloadFile(
val song: MusicDirectory.Entry,
private val save: Boolean
) : KoinComponent, Comparable<DownloadFile> {
) : KoinComponent, Identifiable {
val partialFile: File
val completeFile: File
private val saveFile: File = FileUtil.getSongFile(song)
@ -61,6 +62,7 @@ class DownloadFile(
private val activeServerProvider: ActiveServerProvider by inject()
val progress: MutableLiveData<Int> = MutableLiveData(0)
val status: MutableLiveData<DownloadStatus> = MutableLiveData(DownloadStatus.IDLE)
init {
partialFile = File(saveFile.parent, FileUtil.getPartialFile(saveFile.name))
@ -204,11 +206,13 @@ class DownloadFile(
val musicService = getMusicService()
override fun execute() {
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
if (saveFile.exists()) {
Timber.i("%s already exists. Skipping.", saveFile)
status.postValue(DownloadStatus.DONE)
return
}
@ -222,9 +226,12 @@ class DownloadFile(
} else {
Timber.i("%s already exists. Skipping.", completeFile)
}
status.postValue(DownloadStatus.DONE)
return
}
status.postValue(DownloadStatus.DOWNLOADING)
// Some devices seem to throw error on partial file which doesn't exist
val needsDownloading: Boolean
val duration = song.duration
@ -267,6 +274,7 @@ class DownloadFile(
outputStream.close()
if (isCancelled) {
status.postValue(DownloadStatus.ABORTED)
throw Exception(String.format("Download of '%s' was cancelled", song))
}
@ -275,6 +283,8 @@ class DownloadFile(
}
downloadAndSaveCoverArt()
status.postValue(DownloadStatus.DONE)
}
if (isPlaying) {
@ -293,7 +303,11 @@ class DownloadFile(
Util.delete(saveFile)
if (!isCancelled) {
isFailed = true
if (retryCount > 0) {
if (retryCount > 1) {
status.postValue(DownloadStatus.RETRYING)
--retryCount
} else if (retryCount == 1) {
status.postValue(DownloadStatus.FAILED)
--retryCount
}
Timber.w(all, "Failed to download '%s'.", song)
@ -389,11 +403,20 @@ class DownloadFile(
}
}
override fun compareTo(other: DownloadFile): Int {
override fun compareTo(other: Identifiable) = compareTo(other as DownloadFile)
fun compareTo(other: DownloadFile): Int {
return priority.compareTo(other.priority)
}
override val id: String
get() = song.id
companion object {
const val MAX_RETRIES = 5
}
}
enum class DownloadStatus {
IDLE, DOWNLOADING, RETRYING, FAILED, ABORTED, DONE
}

View File

@ -1,7 +1,6 @@
package org.moire.ultrasonic.service
import android.net.wifi.WifiManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import java.util.ArrayList
import java.util.PriorityQueue
@ -115,11 +114,9 @@ class Downloader(
return
}
// Flag to know if changes have occured
var listChanged = false
// Check the active downloads for failures or completions and remove them
cleanupActiveDownloads()
// Store the result in a flag to know if changes have occurred
var listChanged = cleanupActiveDownloads()
// Check if need to preload more from playlist
val preloadCount = Settings.preloadCount
@ -165,7 +162,7 @@ class Downloader(
}
if (listChanged) {
observableList.value = downloads
observableList.postValue(downloads)
}
}
@ -175,7 +172,12 @@ class Downloader(
}
}
private fun cleanupActiveDownloads() {
/**
* Return true if modifications were made
*/
private fun cleanupActiveDownloads(): Boolean {
val oldSize = activelyDownloading.size
activelyDownloading.retainAll {
when {
it.isDownloading -> true
@ -190,6 +192,8 @@ class Downloader(
}
}
}
return (oldSize != activelyDownloading.size)
}
@get:Synchronized
@ -214,7 +218,7 @@ class Downloader(
}
@get:Synchronized
val downloads: List<DownloadFile>
val all: List<DownloadFile>
get() {
val temp: MutableList<DownloadFile> = ArrayList()
temp.addAll(activelyDownloading)
@ -223,6 +227,27 @@ class Downloader(
return temp.distinct().sorted()
}
/*
* Returns a list of all DownloadFiles that are currently downloading or waiting for download,
* including undownloaded files from the playlist.
*/
@get:Synchronized
val downloads: List<DownloadFile>
get() {
val temp: MutableList<DownloadFile> = ArrayList()
temp.addAll(activelyDownloading)
temp.addAll(downloadQueue)
temp.addAll(
playlist.filter {
when (it.status.value) {
DownloadStatus.DOWNLOADING -> true
else -> false
}
}
)
return temp.distinct().sorted()
}
@Synchronized
fun clearPlaylist() {
playlist.clear()

View File

@ -356,13 +356,13 @@ class MediaPlayerService : Service() {
Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song)
Util.broadcastA2dpMetaDataChange(
this@MediaPlayerService, playerPosition, currentPlaying,
downloader.downloads.size, downloader.currentPlayingIndex + 1
downloader.all.size, downloader.currentPlayingIndex + 1
)
} else {
Util.broadcastNewTrackInfo(this@MediaPlayerService, null)
Util.broadcastA2dpMetaDataChange(
this@MediaPlayerService, playerPosition, null,
downloader.downloads.size, downloader.currentPlayingIndex + 1
downloader.all.size, downloader.currentPlayingIndex + 1
)
}

View File

@ -351,9 +351,9 @@ class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent
companion object {
private var starHollowDrawable: Drawable? = null
private var starDrawable: Drawable? = null
private var pinImage: Drawable? = null
private var downloadedImage: Drawable? = null
private var downloadingImage: Drawable? = null
var pinImage: Drawable? = null
var downloadedImage: Drawable? = null
var downloadingImage: Drawable? = null
private var playingImage: Drawable? = null
private var theme: String? = null
private var inflater: LayoutInflater? = null

View File

@ -25,6 +25,11 @@
a:checkable="true"
a:icon="?attr/playlists"
a:title="@string/button_bar.playlists" />
<item
a:id="@+id/downloadsFragment"
a:checkable="true"
a:icon="?attr/downloaded"
a:title="@string/menu.downloads" />
<item
a:id="@+id/sharesFragment"
a:checkable="true"

View File

@ -60,6 +60,9 @@
android:id="@+id/playlistsToSelectAlbum"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/downloadsFragment"
android:name="org.moire.ultrasonic.fragment.DownloadsFragment" />
<fragment
android:id="@+id/sharesFragment"
android:name="org.moire.ultrasonic.fragment.SharesFragment" >

View File

@ -121,6 +121,7 @@
<string name="menu.common">Common</string>
<string name="menu.deleted_playlist">Deleted playlist %s</string>
<string name="menu.deleted_playlist_error">Failed to delete playlist %s</string>
<string name="menu.downloads">Downloads</string>
<string name="menu.exit">Exit</string>
<string name="menu.navigation">Navigation</string>
<string name="menu.settings">Settings</string>
@ -213,9 +214,9 @@
<string name="settings.directory_cache_time_60">1 hour</string>
<string name="settings.disc_sort">Sort Songs By Disc</string>
<string name="settings.disc_sort_summary">Sort song list by disc number and track number</string>
<string name="settings.display_bitrate">Display Bitrate And File Suffix</string>
<string name="settings.display_bitrate">Display Bitrate and File Suffix</string>
<string name="settings.display_bitrate_summary">Append artist name with bitrate and file suffix</string>
<string name="settings.download_transition">Show Downloads On Play</string>
<string name="settings.download_transition">Show Downloads on Play</string>
<string name="settings.download_transition_summary">Transition to download activity when starting playback</string>
<string name="settings.gapless_playback">Gapless Playback</string>
<string name="settings.gapless_playback_summary">Enable gapless playback</string>