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
abstract class GenericEntry {
// TODO Should be non-null!
// TODO: Should be non-null!
abstract val id: String?
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:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: true
complexity:
active: true

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
package org.moire.ultrasonic.service;
import timber.log.Timber;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.util.LRUCache;
import org.moire.ultrasonic.util.ShufflePlayBuffer;
@ -16,6 +14,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING;
@ -342,7 +341,7 @@ public class Downloader
Collections.shuffle(downloadList);
if (localMediaPlayer.currentPlaying != null)
{
downloadList.remove(getCurrentPlayingIndex());
downloadList.remove(localMediaPlayer.currentPlaying);
downloadList.add(0, localMediaPlayer.currentPlaying);
}
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")
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) {
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData(listOf())
var lastType: String? = null
private var loadedUntil: Int = 0
fun getAlbumList(
@ -21,8 +22,14 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
swipe: SwipeRefreshLayout?,
args: Bundle
): 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
}

View File

@ -30,13 +30,17 @@ import org.moire.ultrasonic.service.MusicService
* Provides ViewModel which contains the list of available Artists
*/
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
*/
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
}

View File

@ -16,20 +16,24 @@ import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
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.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>(
abstract class GenericRowAdapter<T : GenericEntry>(
val onItemClick: (T) -> Unit,
val onContextMenuClick: (MenuItem, T) -> Boolean,
private val onMusicFolderUpdate: (String?) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
) : ListAdapter<T, RecyclerView.ViewHolder>(GenericDiffCallback()) {
open var itemList: List<T> = listOf()
protected abstract val layout: Int
protected abstract val contextMenuLayout: Int
@ -40,11 +44,12 @@ abstract class GenericRowAdapter<T>(
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>) {
submitList(data)
itemList = data
notifyDataSetChanged()
}
/**
@ -136,5 +141,17 @@ abstract class GenericRowAdapter<T>(
companion object {
internal const val TYPE_HEADER = 0
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)
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) {
override fun createPlaylist(id: String?, name: String?, entries: List<MusicDirectory.Entry>) {
cachedPlaylists.clear()
musicService.createPlaylist(id, name, entries)
}

View File

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

View File

@ -386,12 +386,6 @@ class MediaPlayerController(
@get:Synchronized
val playerDuration: Int
get() {
if (localMediaPlayer.currentPlaying != null) {
val duration = localMediaPlayer.currentPlaying!!.song.duration
if (duration != null) {
return duration * 1000
}
}
val mediaPlayerService = runningInstance ?: return 0
return mediaPlayerService.playerDuration
}
@ -454,6 +448,19 @@ class MediaPlayerController(
if (localMediaPlayer.currentPlaying == null) return
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
localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
song.starred = !song.starred

View File

@ -73,7 +73,7 @@ interface MusicService {
fun getPlaylists(refresh: Boolean): List<Playlist>
@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)
fun deletePlaylist(id: String)

View File

@ -221,7 +221,7 @@ class OfflineMusicService : MusicService, KoinComponent {
@Suppress("TooGenericExceptionCaught")
@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 =
FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name)
val fw = FileWriter(playlistFile)

View File

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