Merge pull request #604 from ultrasonic/downloader-ld

Implement a Download view
This commit is contained in:
tzugen 2021-10-23 16:31:42 +02:00 committed by GitHub
commit 7d2923230c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 536 additions and 286 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

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,16 @@
package org.moire.ultrasonic.domain
import androidx.room.Ignore
abstract class GenericEntry : Identifiable {
abstract override val id: String
@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() {
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

@ -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,

View File

@ -106,7 +106,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="128"
line="132"
column="5"/>
</issue>
@ -117,7 +117,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="145"
line="149"
column="5"/>
</issue>
@ -128,7 +128,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="146"
line="150"
column="5"/>
</issue>
@ -381,7 +381,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="109"
line="112"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -393,7 +393,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="109"
line="112"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -409,7 +409,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="109"
line="112"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -429,7 +429,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="80"
line="108"
column="13"/>
</issue>
@ -440,7 +440,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="122"
line="126"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -452,7 +452,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="122"
line="125"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -468,7 +468,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="120"
line="125"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -488,7 +488,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="90"
line="121"
column="13"/>
</issue>
@ -499,7 +499,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="127"
line="131"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -511,7 +511,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="127"
line="130"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -527,7 +527,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="125"
line="130"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -547,7 +547,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="95"
line="126"
column="13"/>
</issue>
@ -558,7 +558,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="128"
line="132"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -570,7 +570,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="128"
line="131"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -586,7 +586,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="126"
line="131"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -604,6 +604,10 @@
file="src/main/res/values-ru/strings.xml"
line="128"
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="127"
column="13"/>
</issue>
<issue
@ -613,7 +617,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="129"
line="133"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -625,7 +629,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="129"
line="132"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -641,7 +645,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="127"
line="132"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -661,7 +665,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="96"
line="128"
column="13"/>
</issue>
@ -672,7 +676,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="130"
line="134"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -684,7 +688,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="130"
line="133"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -700,7 +704,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="128"
line="133"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -720,7 +724,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="97"
line="129"
column="13"/>
</issue>
@ -731,7 +735,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="135"
line="139"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -743,7 +747,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="135"
line="138"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -759,7 +763,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="133"
line="138"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -779,7 +783,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="100"
line="134"
column="13"/>
</issue>
@ -790,7 +794,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="141"
line="145"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -802,7 +806,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="141"
line="144"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -818,7 +822,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="139"
line="144"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -838,7 +842,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="106"
line="140"
column="13"/>
</issue>
@ -849,7 +853,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="154"
line="158"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -861,7 +865,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="154"
line="157"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -877,7 +881,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="152"
line="157"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -895,6 +899,10 @@
file="src/main/res/values-ru/strings.xml"
line="154"
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="153"
column="13"/>
</issue>
<issue
@ -904,7 +912,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="155"
line="159"
column="13"/>
</issue>
@ -915,7 +923,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="156"
line="160"
column="13"/>
</issue>
@ -926,7 +934,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="172"
line="176"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -938,7 +946,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="170"
line="173"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -954,7 +962,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="168"
line="173"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -974,7 +982,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="131"
line="169"
column="13"/>
</issue>
@ -985,7 +993,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="224"
line="228"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -997,7 +1005,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="222"
line="225"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -1013,7 +1021,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="220"
line="225"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -1031,6 +1039,10 @@
file="src/main/res/values-ru/strings.xml"
line="222"
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="219"
column="13"/>
</issue>
<issue
@ -1040,7 +1052,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="293"
line="297"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -1052,7 +1064,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="289"
line="294"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -1068,7 +1080,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="287"
line="294"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -1080,7 +1092,7 @@
column="13"/>
<location
file="src/main/res/values-pt-rBR/strings.xml"
line="289"
line="291"
column="13"/>
<location
file="src/main/res/values-ru/strings.xml"
@ -1088,7 +1100,7 @@
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="213"
line="287"
column="13"/>
</issue>
@ -1099,7 +1111,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="296"
line="300"
column="13"/>
<location
file="src/main/res/values-cs/strings.xml"
@ -1111,7 +1123,7 @@
column="13"/>
<location
file="src/main/res/values-es/strings.xml"
line="292"
line="297"
column="13"/>
<location
file="src/main/res/values-fr/strings.xml"
@ -1127,7 +1139,7 @@
column="13"/>
<location
file="src/main/res/values-nl/strings.xml"
line="290"
line="297"
column="13"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -1139,12 +1151,16 @@
column="13"/>
<location
file="src/main/res/values-pt-rBR/strings.xml"
line="292"
line="294"
column="13"/>
<location
file="src/main/res/values-ru/strings.xml"
line="292"
column="13"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="290"
column="13"/>
</issue>
<issue
@ -1154,7 +1170,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="390"
line="397"
column="13"/>
</issue>
@ -1165,47 +1181,47 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="469"
line="476"
column="14"/>
<location
file="src/main/res/values-cs/strings.xml"
line="450"
column="14"/>
<location
file="src/main/res/values-de/strings.xml"
line="391"
column="14"/>
<location
file="src/main/res/values-es/strings.xml"
line="463"
line="472"
column="14"/>
<location
file="src/main/res/values-fr/strings.xml"
line="463"
line="461"
column="14"/>
<location
file="src/main/res/values-hu/strings.xml"
line="458"
line="456"
column="14"/>
<location
file="src/main/res/values-nl/strings.xml"
line="460"
line="472"
column="14"/>
<location
file="src/main/res/values-pl/strings.xml"
line="404"
line="403"
column="14"/>
<location
file="src/main/res/values-pt/strings.xml"
line="391"
line="390"
column="14"/>
<location
file="src/main/res/values-pt-rBR/strings.xml"
line="463"
line="465"
column="14"/>
<location
file="src/main/res/values-ru/strings.xml"
line="475"
line="473"
column="14"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="454"
column="14"/>
</issue>
@ -1242,94 +1258,6 @@
column="6"/>
</issue>
<issue
id="TypographyDashes"
message="Replace &quot;--&quot; with an &quot;em dash&quot; character (—, &amp;#8212;) ?"
errorLine1=" &lt;string name=&quot;util.no_time&quot;>-:--&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-it/strings.xml"
line="312"
column="33"/>
</issue>
<issue
id="TypographyDashes"
message="Replace &quot;--&quot; with an &quot;em dash&quot; character (—, &amp;#8212;) ?"
errorLine1=" &lt;string name=&quot;util.no_time&quot;>-:--&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-de/strings.xml"
line="323"
column="33"/>
</issue>
<issue
id="TypographyDashes"
message="Replace &quot;--&quot; with an &quot;em dash&quot; character (—, &amp;#8212;) ?"
errorLine1=" &lt;string name=&quot;util.no_time&quot;>-:--&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-pl/strings.xml"
line="323"
column="33"/>
</issue>
<issue
id="TypographyDashes"
message="Replace &quot;--&quot; with an &quot;em dash&quot; character (—, &amp;#8212;) ?"
errorLine1=" &lt;string name=&quot;util.no_time&quot;>-:--&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-cs/strings.xml"
line="326"
column="33"/>
</issue>
<issue
id="TypographyDashes"
message="Replace &quot;--&quot; with an &quot;em dash&quot; character (—, &amp;#8212;) ?"
errorLine1=" &lt;string name=&quot;util.no_time&quot;>-:--&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-hu/strings.xml"
line="340"
column="33"/>
</issue>
<issue
id="TypographyDashes"
message="Replace &quot;--&quot; with an &quot;em dash&quot; character (—, &amp;#8212;) ?"
errorLine1=" &lt;string name=&quot;util.no_time&quot;>-:--&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-nl/strings.xml"
line="340"
column="33"/>
</issue>
<issue
id="TypographyEllipsis"
message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
errorLine1=" &lt;string name=&quot;parser.reading&quot;>Bezig met uitlezen van server...&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-nl/strings.xml"
line="127"
column="35"/>
</issue>
<issue
id="TypographyEllipsis"
message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
errorLine1=" &lt;string name=&quot;service.connecting&quot;>Bezig met verbinden met server; even geduld...&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-nl/strings.xml"
line="152"
column="39"/>
</issue>
<issue
id="IconDuplicates"
message="The following unrelated icon files have identical contents: list_pressed_holo_dark.9.png, list_pressed_holo_light.9.png">
@ -1368,7 +1296,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/share_details.xml"
line="22"
line="30"
column="10"/>
</issue>
@ -2138,7 +2066,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/share_details.xml"
line="22"
line="30"
column="10"/>
</issue>

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

@ -24,7 +24,7 @@ public class SongListAdapter extends ArrayAdapter<DownloadFile>
public View getView(final int position, final View convertView, final ViewGroup parent)
{
DownloadFile downloadFile = getItem(position);
MusicDirectory.Entry entry = downloadFile.getSong();
MusicDirectory.Entry entry = downloadFile.getSong();
SongView view;

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

@ -317,7 +317,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
repeatButton.setOnClickListener {
val repeatMode = mediaPlayerController.repeatMode.next()
mediaPlayerController.repeatMode = repeatMode
onDownloadListChanged()
onPlaylistChanged()
when (repeatMode) {
RepeatMode.OFF -> Util.toast(
context, R.string.download_repeat_off
@ -435,7 +435,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
playlistFlipper.displayedChild = 1
} else {
// Download list and Album art must be updated when Resumed
onDownloadListChanged()
onPlaylistChanged()
onCurrentChanged()
}
val handler = Handler()
@ -642,7 +642,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
}
R.id.menu_remove -> {
mediaPlayerController.removeFromPlaylist(song!!)
onDownloadListChanged()
onPlaylistChanged()
return true
}
R.id.menu_item_screen_on_off -> {
@ -697,7 +697,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
R.id.menu_item_clear_playlist -> {
mediaPlayerController.isShufflePlayEnabled = false
mediaPlayerController.clear()
onDownloadListChanged()
onPlaylistChanged()
return true
}
R.id.menu_item_save_playlist -> {
@ -798,7 +798,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
if (cancel!!.isCancellationRequested) return
val mediaPlayerController = mediaPlayerController
if (currentRevision != mediaPlayerController.playListUpdateRevision) {
onDownloadListChanged()
onPlaylistChanged()
}
if (currentPlaying != mediaPlayerController.currentPlaying) {
onCurrentChanged()
@ -874,7 +874,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
}
}
private fun onDownloadListChanged() {
private fun onPlaylistChanged() {
val mediaPlayerController = mediaPlayerController
val list = mediaPlayerController.playList
emptyTextView.setText(R.string.download_empty)
@ -907,7 +907,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
item.song.title
)
Util.toast(context, songRemoved)
onDownloadListChanged()
onPlaylistChanged()
onCurrentChanged()
}
})

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,9 +1,11 @@
package org.moire.ultrasonic.service
import android.net.wifi.WifiManager
import androidx.lifecycle.MutableLiveData
import java.util.ArrayList
import java.util.PriorityQueue
import java.util.concurrent.Executors
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent
@ -20,7 +22,6 @@ import timber.log.Timber
* This class is responsible for maintaining the playlist and downloading
* its items from the network to the filesystem.
*
* TODO: Implement LiveData
* TODO: Move away from managing the queue with scheduled checks, instead use callbacks when
* Downloads are finished
*/
@ -35,6 +36,8 @@ class Downloader(
private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>()
private val activelyDownloading: MutableList<DownloadFile> = ArrayList()
val observableList: MutableLiveData<List<DownloadFile>> = MutableLiveData<List<DownloadFile>>()
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
private val downloadFileCache = LRUCache<MusicDirectory.Entry, DownloadFile>(100)
@ -58,6 +61,7 @@ class Downloader(
stop()
clearPlaylist()
clearBackground()
observableList.value = listOf()
Timber.i("Downloader destroyed")
}
@ -88,10 +92,21 @@ class Downloader(
}
fun checkDownloads() {
if (executorService == null || executorService!!.isTerminated) {
if (
executorService == null ||
executorService!!.isTerminated ||
executorService!!.isShutdown
) {
start()
} else {
executorService?.execute(downloadChecker)
try {
executorService?.execute(downloadChecker)
} catch (exception: RejectedExecutionException) {
Timber.w(
exception,
"checkDownloads() can't run, maybe the Downloader is shutting down..."
)
}
}
}
@ -112,7 +127,8 @@ class Downloader(
}
// 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
@ -134,6 +150,7 @@ class Downloader(
!activelyDownloading.contains(download) &&
!downloadQueue.contains(download)
) {
listChanged = true
downloadQueue.add(download)
}
}
@ -148,12 +165,21 @@ class Downloader(
if (playlist.indexOf(task) == 1) {
localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING)
}
listChanged = true
}
// Stop Executor service when done downloading
if (activelyDownloading.size == 0) {
stop()
}
if (listChanged) {
updateLiveData()
}
}
private fun updateLiveData() {
observableList.postValue(downloads)
}
private fun startDownloadOnService(task: DownloadFile) {
@ -162,7 +188,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
@ -177,6 +208,8 @@ class Downloader(
}
}
}
return (oldSize != activelyDownloading.size)
}
@get:Synchronized
@ -201,13 +234,34 @@ class Downloader(
}
@get:Synchronized
val downloads: List<DownloadFile?>
val all: List<DownloadFile>
get() {
val temp: MutableList<DownloadFile?> = ArrayList()
temp.addAll(playlist)
val temp: MutableList<DownloadFile> = ArrayList()
temp.addAll(activelyDownloading)
temp.addAll(downloadQueue)
return temp.distinct()
temp.addAll(playlist)
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
@ -216,11 +270,14 @@ class Downloader(
// Cancel all active downloads with a high priority
for (download in activelyDownloading) {
if (download.priority < 100)
if (download.priority < 100) {
download.cancelDownload()
activelyDownloading.remove(download)
}
}
playlistUpdateRevision++
updateLiveData()
}
@Synchronized
@ -230,17 +287,21 @@ class Downloader(
// Cancel all active downloads with a low priority
for (download in activelyDownloading) {
if (download.priority >= 100)
if (download.priority >= 100) {
download.cancelDownload()
activelyDownloading.remove(download)
}
}
}
@Synchronized
fun clearActiveDownloads() {
// Cancel all active downloads with a low priority
// Cancel all active downloads
for (download in activelyDownloading) {
download.cancelDownload()
}
activelyDownloading.clear()
updateLiveData()
}
@Synchronized
@ -250,6 +311,7 @@ class Downloader(
}
playlist.remove(downloadFile)
playlistUpdateRevision++
checkDownloads()
}
@Synchronized

View File

@ -421,14 +421,13 @@ class MediaPlayerController(
}
}
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
val isJukeboxAvailable: Boolean
get() {
try {
val username = activeServerProvider.getActiveServer().userName
return getMusicService().getUser(username).jukeboxRole
} catch (e: Exception) {
Timber.w(e, "Error getting user information")
} catch (all: Exception) {
Timber.w(all, "Error getting user information")
}
return false
}

View File

@ -15,7 +15,9 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
@ -159,7 +161,10 @@ class MediaPlayerService : Service() {
}
fun notifyDownloaderStopped() {
stopIfIdle()
// TODO It would be nice to know if the service really can be stopped instead of just
// checking if it is idle once...
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ stopIfIdle() }, 1000)
}
@Synchronized
@ -356,13 +361,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
)
}
@ -740,14 +745,19 @@ class MediaPlayerService : Service() {
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
private const val NOTIFICATION_ID = 3033
@Volatile
private var instance: MediaPlayerService? = null
private val instanceLock = Any()
@JvmStatic
fun getInstance(): MediaPlayerService? {
val context = UApp.applicationContext()
synchronized(instanceLock) {
for (i in 0..19) {
// Try for twenty times to retrieve a running service,
// sleep 100 millis between each try,
// and run the block that creates a service only synchronized.
for (i in 0..19) {
if (instance != null) return instance
synchronized(instanceLock) {
if (instance != null) return instance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(
@ -756,10 +766,10 @@ class MediaPlayerService : Service() {
} else {
context.startService(Intent(context, MediaPlayerService::class.java))
}
Util.sleepQuietly(50L)
}
return instance
Util.sleepQuietly(100L)
}
return instance
}
@JvmStatic

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>