diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt index 6c0537d3..1c8e6440 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt @@ -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 { +) : 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) } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt index 63decd3a..8a4a4c1d 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt @@ -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) diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt deleted file mode 100644 index e731b769..00000000 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt +++ /dev/null @@ -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 - } -} diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt index 49045571..d467eaaa 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt @@ -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 } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt new file mode 100644 index 00000000..e147ea13 --- /dev/null +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Identifiable.kt @@ -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 { + val id: String +} diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index 1d0bc146..a0f32560 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -69,7 +69,7 @@ class MusicDirectory { var bookmarkPosition: Int = 0, var userRating: Int? = null, var averageRating: Float? = null - ) : Serializable, GenericEntry(), Comparable { + ) : 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) } } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt index c4f94fb6..e692a5c7 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt @@ -10,4 +10,4 @@ import androidx.room.PrimaryKey data class MusicFolder( @PrimaryKey override val id: String, override val name: String -) : GenericEntry() +) : GenericEntry(id) diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt index fa91d9b9..3ae9afee 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt @@ -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 } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt index a589877e..b38f7f41 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt @@ -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 } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt index adc8b080..bec9bffa 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt @@ -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 = mutableListOf() -) : Serializable, GenericEntry() { +) : Serializable, GenericEntry(id) { override val name: String? get() { if (url != null) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java index 3c14ad65..fc48490c 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java @@ -232,7 +232,7 @@ public class JukeboxMediaPlayer tasks.remove(Start.class); List ids = new ArrayList<>(); - for (DownloadFile file : downloader.getDownloads()) + for (DownloadFile file : downloader.getAll()) { ids.add(file.getSong().getId()); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java index 67ebea09..6137491b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java @@ -8,3 +8,5 @@ public abstract class Supplier { public abstract T get(); } + + diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index 40a68a88..e3a48c70 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -200,7 +200,7 @@ public class CacheCleaner Lazy downloader = inject(Downloader.class); - for (DownloadFile downloadFile : downloader.getValue().getDownloads()) + for (DownloadFile downloadFile : downloader.getValue().getAll()) { filesToNotDelete.add(downloadFile.getPartialFile()); filesToNotDelete.add(downloadFile.getCompleteOrSaveFile()); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index dc9ffe66..30aa0797 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 9cd0a050..aafdea81 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -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() { +class AlbumListFragment : EntryListFragment() { /** * The ViewModel to use to get the data diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt index c9dcbabe..e203a35e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -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, + itemList: List, 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) { - 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 } /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 51d42f65..58ee16a8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -10,7 +10,7 @@ import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists from the media library */ -class ArtistListFragment : GenericListFragment() { +class ArtistListFragment : EntryListFragment() { /** * The ViewModel to use to get the data diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index c014a059..e87477b2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -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 = + compareBy(Collator.getInstance()) { t -> t.name } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index 607eed30..d4079a73 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -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, + itemList: List, 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) { - 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt new file mode 100644 index 00000000..54d26ef9 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -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() { + + /** + * 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> { + 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, + onItemClick: (DownloadFile) -> Unit, + onContextMenuClick: (MenuItem, DownloadFile) -> Boolean, + onMusicFolderUpdate: (String?) -> Unit, + context: Context, + val lifecycleOwner: LifecycleOwner +) : GenericRowAdapter( + 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() + + fun getList(): LiveData> { + return downloader.observableList + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt index 8ac78dc8..1aa1f254 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt @@ -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> : Fragment() { +abstract class GenericListFragment> : 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> @Suppress("CommentOverPrivateProperty") private val musicFolderObserver = { folders: List -> viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId) - Unit } /** @@ -114,7 +114,7 @@ abstract class GenericListFragment> !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> 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> return inflater.inflate(mainLayout, container, false) } + abstract fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean + + abstract fun onItemClick(item: T) +} + +abstract class EntryListFragment> : + GenericListFragment() { @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> 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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt index b825fcac..33f3783e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt @@ -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 in a RecyclerView */ -abstract class GenericRowAdapter( +abstract class GenericRowAdapter( val onItemClick: (T) -> Unit, val onContextMenuClick: (MenuItem, T) -> Boolean, private val onMusicFolderUpdate: (String?) -> Unit ) : ListAdapter(GenericDiffCallback()) { - open var itemList: List = listOf() protected abstract val layout: Int protected abstract val contextMenuLayout: Int @@ -43,15 +43,6 @@ abstract class GenericRowAdapter( var musicFolders: List = 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) { - submitList(data) - itemList = data - } - /** * Sets the content and state of the music folder selector row */ @@ -101,9 +92,9 @@ abstract class GenericRowAdapter( 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( 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( /** * Calculates the differences between data sets */ - class GenericDiffCallback : DiffUtil.ItemCallback() { + class GenericDiffCallback : DiffUtil.ItemCallback() { + @SuppressLint("DiffUtilEquals") override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { return oldItem == newItem } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 2ddf1bc4..86b9aeab 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -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 { +) : 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 = MutableLiveData(0) + val status: MutableLiveData = 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 +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt index 542d9e22..17ec255d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt @@ -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 + val all: List get() { val temp: MutableList = 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 + get() { + val temp: MutableList = 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() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index f86ce5b2..ae1a865a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -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 ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt index 4a89c3c6..651926fe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt @@ -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 diff --git a/ultrasonic/src/main/res/menu/navigation.xml b/ultrasonic/src/main/res/menu/navigation.xml index 91de3df0..956f94ed 100644 --- a/ultrasonic/src/main/res/menu/navigation.xml +++ b/ultrasonic/src/main/res/menu/navigation.xml @@ -25,6 +25,11 @@ a:checkable="true" a:icon="?attr/playlists" a:title="@string/button_bar.playlists" /> + + diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 85b6d58c..e7ed3793 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -121,6 +121,7 @@ Common Deleted playlist %s Failed to delete playlist %s + Downloads Exit Navigation Settings @@ -213,9 +214,9 @@ 1 hour Sort Songs By Disc Sort song list by disc number and track number - Display Bitrate And File Suffix + Display Bitrate and File Suffix Append artist name with bitrate and file suffix - Show Downloads On Play + Show Downloads on Play Transition to download activity when starting playback Gapless Playback Enable gapless playback