merge develop

This commit is contained in:
James Wells 2021-06-22 20:08:49 -04:00
commit 4ff167e497
No known key found for this signature in database
GPG Key ID: DB1528F6EED16127
18 changed files with 1513 additions and 2019 deletions

View File

@ -1,7 +1,19 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
abstract class GenericEntry { abstract class GenericEntry {
// TODO Should be non-null! // TODO: Should be non-null!
abstract val id: String? abstract val id: String?
open val name: String? = null 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

@ -35,6 +35,9 @@ exceptions:
empty-blocks: empty-blocks:
active: true active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: true
complexity: complexity:
active: true active: true

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
package org.moire.ultrasonic.service; package org.moire.ultrasonic.service;
import timber.log.Timber;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.util.LRUCache; import org.moire.ultrasonic.util.LRUCache;
import org.moire.ultrasonic.util.ShufflePlayBuffer; import org.moire.ultrasonic.util.ShufflePlayBuffer;
@ -16,6 +14,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import kotlin.Lazy; import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject; import static org.koin.java.KoinJavaComponent.inject;
import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING;
@ -342,7 +341,7 @@ public class Downloader
Collections.shuffle(downloadList); Collections.shuffle(downloadList);
if (localMediaPlayer.currentPlaying != null) if (localMediaPlayer.currentPlaying != null)
{ {
downloadList.remove(getCurrentPlayingIndex()); downloadList.remove(localMediaPlayer.currentPlaying);
downloadList.add(0, localMediaPlayer.currentPlaying); downloadList.add(0, localMediaPlayer.currentPlaying);
} }
revision++; revision++;

View File

@ -1,81 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util;
import android.app.Activity;
/**
* @author Sindre Mehus
*/
public abstract class SilentBackgroundTask<T> extends BackgroundTask<T>
{
public SilentBackgroundTask(Activity activity)
{
super(activity);
}
@Override
public void execute()
{
Thread thread = new Thread()
{
@Override
public void run()
{
try
{
final T result = doInBackground();
getHandler().post(new Runnable()
{
@Override
public void run()
{
done(result);
}
});
}
catch (final Throwable t)
{
getHandler().post(new Runnable()
{
@Override
public void run()
{
error(t);
}
});
}
}
};
thread.start();
}
@Override
public void updateProgress(int messageId)
{
}
@Override
public void updateProgress(String message)
{
}
}

View File

@ -49,8 +49,9 @@ class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdap
if (args == null) throw IllegalArgumentException("Required arguments are missing") if (args == null) throw IllegalArgumentException("Required arguments are missing")
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH)
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND)
return listModel.getAlbumList(refresh, refreshListView!!, args) return listModel.getAlbumList(refresh or append, refreshListView!!, args)
} }
/** /**

View File

@ -13,7 +13,8 @@ import org.moire.ultrasonic.util.Util
class AlbumListModel(application: Application) : GenericListModel(application) { class AlbumListModel(application: Application) : GenericListModel(application) {
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData() val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
var lastType: String? = null
private var loadedUntil: Int = 0 private var loadedUntil: Int = 0
fun getAlbumList( fun getAlbumList(
@ -21,8 +22,14 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
swipe: SwipeRefreshLayout?, swipe: SwipeRefreshLayout?,
args: Bundle args: Bundle
): LiveData<List<MusicDirectory.Entry>> { ): LiveData<List<MusicDirectory.Entry>> {
// Don't reload the data if navigating back to the view that was active before.
// This way, we keep the scroll position
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
backgroundLoadFromServer(refresh, swipe, args) if (refresh || albumList.value!!.isEmpty() || albumListType != lastType) {
lastType = albumListType
backgroundLoadFromServer(refresh, swipe, args)
}
return albumList return albumList
} }

View File

@ -30,13 +30,17 @@ import org.moire.ultrasonic.service.MusicService
* Provides ViewModel which contains the list of available Artists * Provides ViewModel which contains the list of available Artists
*/ */
class ArtistListModel(application: Application) : GenericListModel(application) { class ArtistListModel(application: Application) : GenericListModel(application) {
val artists: MutableLiveData<List<Artist>> = MutableLiveData() val artists: MutableLiveData<List<Artist>> = MutableLiveData(listOf())
/** /**
* Retrieves all available Artists in a LiveData * Retrieves all available Artists in a LiveData
*/ */
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout?): LiveData<List<Artist>> { fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout?): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe) // Don't reload the data if navigating back to the view that was active before.
// This way, we keep the scroll position
if (artists.value!!.isEmpty() || refresh) {
backgroundLoadFromServer(refresh, swipe)
}
return artists return artists
} }

View File

@ -16,20 +16,24 @@ import android.widget.ImageView
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.view.SelectMusicFolderView import org.moire.ultrasonic.view.SelectMusicFolderView
/* /*
* An abstract Adapter, which can be extended to display a List of <T> in a RecyclerView * An abstract Adapter, which can be extended to display a List of <T> in a RecyclerView
*/ */
abstract class GenericRowAdapter<T>( abstract class GenericRowAdapter<T : GenericEntry>(
val onItemClick: (T) -> Unit, val onItemClick: (T) -> Unit,
val onContextMenuClick: (MenuItem, T) -> Boolean, val onContextMenuClick: (MenuItem, T) -> Boolean,
private val onMusicFolderUpdate: (String?) -> Unit private val onMusicFolderUpdate: (String?) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : ListAdapter<T, RecyclerView.ViewHolder>(GenericDiffCallback()) {
open var itemList: List<T> = listOf() open var itemList: List<T> = listOf()
protected abstract val layout: Int protected abstract val layout: Int
protected abstract val contextMenuLayout: Int protected abstract val contextMenuLayout: Int
@ -40,11 +44,12 @@ abstract class GenericRowAdapter<T>(
var selectedFolder: String? = null var selectedFolder: String? = null
/** /**
* Sets the data to be displayed in the RecyclerView * Sets the data to be displayed in the RecyclerView,
* using DiffUtil to efficiently calculate the minimum required changes..
*/ */
open fun setData(data: List<T>) { open fun setData(data: List<T>) {
submitList(data)
itemList = data itemList = data
notifyDataSetChanged()
} }
/** /**
@ -136,5 +141,17 @@ abstract class GenericRowAdapter<T>(
companion object { companion object {
internal const val TYPE_HEADER = 0 internal const val TYPE_HEADER = 0
internal const val TYPE_ITEM = 1 internal const val TYPE_ITEM = 1
/**
* Calculates the differences between data sets
*/
class GenericDiffCallback<T : GenericEntry> : DiffUtil.ItemCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id
}
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -198,7 +198,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) { override fun createPlaylist(id: String?, name: String?, entries: List<MusicDirectory.Entry>) {
cachedPlaylists.clear() cachedPlaylists.clear()
musicService.createPlaylist(id, name, entries) musicService.createPlaylist(id, name, entries)
} }

View File

@ -20,6 +20,7 @@ import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.os.PowerManager.PARTIAL_WAKE_LOCK import android.os.PowerManager.PARTIAL_WAKE_LOCK
import android.os.PowerManager.WakeLock import android.os.PowerManager.WakeLock
import androidx.lifecycle.MutableLiveData
import java.io.File import java.io.File
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Locale import java.util.Locale
@ -29,7 +30,6 @@ import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.audiofx.VisualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.fragment.PlayerFragment
import org.moire.ultrasonic.util.CancellableTask import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.StreamProxy import org.moire.ultrasonic.util.StreamProxy
@ -79,10 +79,12 @@ class LocalMediaPlayer(
private var proxy: StreamProxy? = null private var proxy: StreamProxy? = null
private var bufferTask: CancellableTask? = null private var bufferTask: CancellableTask? = null
private var positionCache: PositionCache? = null private var positionCache: PositionCache? = null
private var secondaryProgress = -1
private val pm = context.getSystemService(POWER_SERVICE) as PowerManager private val pm = context.getSystemService(POWER_SERVICE) as PowerManager
private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name) private val wakeLock: WakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, this.javaClass.name)
val secondaryProgress: MutableLiveData<Int> = MutableLiveData(0)
fun init() { fun init() {
Thread { Thread {
Thread.currentThread().name = "MediaPlayerThread" Thread.currentThread().name = "MediaPlayerThread"
@ -361,7 +363,6 @@ class LocalMediaPlayer(
downloadFile.updateModificationDate() downloadFile.updateModificationDate()
mediaPlayer.setOnCompletionListener(null) mediaPlayer.setOnCompletionListener(null)
secondaryProgress = -1 // Ensure seeking in non StreamProxy playback works
setPlayerState(PlayerState.IDLE) setPlayerState(PlayerState.IDLE)
setAudioAttributes(mediaPlayer) setAudioAttributes(mediaPlayer)
@ -392,28 +393,28 @@ class LocalMediaPlayer(
setPlayerState(PlayerState.PREPARING) setPlayerState(PlayerState.PREPARING)
mediaPlayer.setOnBufferingUpdateListener { mp, percent -> mediaPlayer.setOnBufferingUpdateListener { mp, percent ->
val progressBar = PlayerFragment.getProgressBar()
val song = downloadFile.song val song = downloadFile.song
if (percent == 100) { if (percent == 100) {
mp.setOnBufferingUpdateListener(null) mp.setOnBufferingUpdateListener(null)
} }
secondaryProgress = (percent.toDouble() / 100.toDouble() * progressBar.max).toInt() // The secondary progress is an indicator of how far the song is cached.
if (song.transcodedContentType == null && Util.getMaxBitRate() == 0) { if (song.transcodedContentType == null && Util.getMaxBitRate() == 0) {
progressBar?.secondaryProgress = secondaryProgress val progress = (percent.toDouble() / 100.toDouble() * playerDuration).toInt()
secondaryProgress.postValue(progress)
} }
} }
mediaPlayer.setOnPreparedListener { mediaPlayer.setOnPreparedListener {
Timber.i("Media player prepared") Timber.i("Media player prepared")
setPlayerState(PlayerState.PREPARED) setPlayerState(PlayerState.PREPARED)
val progressBar = PlayerFragment.getProgressBar()
if (progressBar != null && downloadFile.isWorkDone) { // Populate seek bar secondary progress if we have a complete file for consistency
// Populate seek bar secondary progress if we have a complete file for consistency if (downloadFile.isWorkDone) {
PlayerFragment.getProgressBar().secondaryProgress = 100 * progressBar.max secondaryProgress.postValue(playerDuration)
} }
synchronized(this@LocalMediaPlayer) { synchronized(this@LocalMediaPlayer) {
if (position != 0) { if (position != 0) {
Timber.i("Restarting player from position %d", position) Timber.i("Restarting player from position %d", position)

View File

@ -386,12 +386,6 @@ class MediaPlayerController(
@get:Synchronized @get:Synchronized
val playerDuration: Int val playerDuration: Int
get() { get() {
if (localMediaPlayer.currentPlaying != null) {
val duration = localMediaPlayer.currentPlaying!!.song.duration
if (duration != null) {
return duration * 1000
}
}
val mediaPlayerService = runningInstance ?: return 0 val mediaPlayerService = runningInstance ?: return 0
return mediaPlayerService.playerDuration return mediaPlayerService.playerDuration
} }
@ -454,6 +448,19 @@ class MediaPlayerController(
if (localMediaPlayer.currentPlaying == null) return if (localMediaPlayer.currentPlaying == null) return
val song = localMediaPlayer.currentPlaying!!.song val song = localMediaPlayer.currentPlaying!!.song
Thread {
val musicService = getMusicService()
try {
if (song.starred) {
musicService.unstar(song.id, null, null)
} else {
musicService.star(song.id, null, null)
}
} catch (all: Exception) {
Timber.e(all)
}
}.start()
// Trigger an update // Trigger an update
localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
song.starred = !song.starred song.starred = !song.starred

View File

@ -73,7 +73,7 @@ interface MusicService {
fun getPlaylists(refresh: Boolean): List<Playlist> fun getPlaylists(refresh: Boolean): List<Playlist>
@Throws(Exception::class) @Throws(Exception::class)
fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) fun createPlaylist(id: String?, name: String?, entries: List<MusicDirectory.Entry>)
@Throws(Exception::class) @Throws(Exception::class)
fun deletePlaylist(id: String) fun deletePlaylist(id: String)

View File

@ -221,7 +221,7 @@ class OfflineMusicService : MusicService, KoinComponent {
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
@Throws(Exception::class) @Throws(Exception::class)
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) { override fun createPlaylist(id: String?, name: String?, entries: List<MusicDirectory.Entry>) {
val playlistFile = val playlistFile =
FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name) FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name)
val fw = FileWriter(playlistFile) val fw = FileWriter(playlistFile)

View File

@ -295,12 +295,20 @@ open class RESTMusicService(
return response.body()!!.playlists.toDomainEntitiesList() return response.body()!!.playlists.toDomainEntitiesList()
} }
/**
* Either ID or String is required.
* ID is required when updating
* String is required when creating
*/
@Throws(Exception::class) @Throws(Exception::class)
override fun createPlaylist( override fun createPlaylist(
id: String, id: String?,
name: String, name: String?,
entries: List<MusicDirectory.Entry> entries: List<MusicDirectory.Entry>
) { ) {
if (id == null && name == null)
throw IllegalArgumentException("Either id or name is required.")
val pSongIds: MutableList<String> = ArrayList(entries.size) val pSongIds: MutableList<String> = ArrayList(entries.size)
for ((id1) in entries) { for ((id1) in entries) {

View File

@ -0,0 +1,32 @@
/*
* SilentBackgroundTask.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import android.app.Activity
/**
* @author Sindre Mehus
*/
abstract class SilentBackgroundTask<T>(activity: Activity?) : BackgroundTask<T>(activity) {
override fun execute() {
val thread: Thread = object : Thread() {
override fun run() {
try {
val result = doInBackground()
handler.post { done(result) }
} catch (all: Throwable) {
handler.post { error(all) }
}
}
}
thread.start()
}
override fun updateProgress(messageId: Int) {}
override fun updateProgress(message: String) {}
}