diff --git a/detekt-config.yml b/detekt-config.yml index 6b7db639..9301b0b7 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -63,7 +63,8 @@ style: excludePackageStatements: false excludeImportStatements: false MagicNumber: - ignoreNumbers: ['-1', '0', '1', '2', '100'] + # 100 common in percentage, 1000 in milliseconds + ignoreNumbers: ['-1', '0', '1', '2', '100', '1000'] ignoreEnums: true ignorePropertyDeclaration: true UnnecessaryAbstractClass: diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt index 227465fa..de18fda7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -26,5 +26,5 @@ val mediaPlayerModule = module { single { AudioFocusHandler(get()) } // TODO Ideally this can be cleaned up when all circular references are removed. - single { MediaPlayerController(androidContext(), get(), get(), get(), get(), get()) } + single { MediaPlayerController(get(), get(), get(), get(), get()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 7ffd904a..1706cf7e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -1,612 +1,514 @@ /* - 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 . - - Copyright 2009 (C) Sindre Mehus + * MediaPlayerController.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.service; +package org.moire.ultrasonic.service -import android.content.Context; -import android.content.Intent; -import timber.log.Timber; - -import org.koin.java.KoinJavaComponent; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.MusicDirectory.Entry; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.domain.RepeatMode; -import org.moire.ultrasonic.domain.UserInfo; -import org.moire.ultrasonic.featureflags.Feature; -import org.moire.ultrasonic.featureflags.FeatureStorage; -import org.moire.ultrasonic.util.ShufflePlayBuffer; -import org.moire.ultrasonic.util.Util; - -import java.util.Iterator; -import java.util.List; - -import kotlin.Lazy; - -import static org.koin.java.KoinJavaComponent.inject; +import android.content.Intent +import org.koin.core.component.KoinApiExtension +import org.koin.java.KoinJavaComponent.get +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.PlayerState +import org.moire.ultrasonic.domain.RepeatMode +import org.moire.ultrasonic.featureflags.Feature +import org.moire.ultrasonic.featureflags.FeatureStorage +import org.moire.ultrasonic.service.MediaPlayerService.Companion.executeOnStartedMediaPlayerService +import org.moire.ultrasonic.service.MediaPlayerService.Companion.getInstance +import org.moire.ultrasonic.service.MediaPlayerService.Companion.runningInstance +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.ShufflePlayBuffer +import org.moire.ultrasonic.util.Util +import timber.log.Timber /** * The implementation of the Media Player Controller. * This class contains everything that is necessary for the Application UI * to control the Media Player implementation. - * - * @author Sindre Mehus, Joshua Bahnsen - * @version $Id$ */ -public class MediaPlayerController -{ - private boolean created = false; - private String suggestedPlaylistName; - private boolean keepScreenOn; - - private boolean showVisualization; - private boolean autoPlayStart; - - private final Context context; - private final Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); - private final Lazy activeServerProvider = inject(ActiveServerProvider.class); - - private final DownloadQueueSerializer downloadQueueSerializer; - private final ExternalStorageMonitor externalStorageMonitor; - private final Downloader downloader; - private final ShufflePlayBuffer shufflePlayBuffer; - private final LocalMediaPlayer localMediaPlayer; - - public MediaPlayerController(Context context, DownloadQueueSerializer downloadQueueSerializer, - ExternalStorageMonitor externalStorageMonitor, Downloader downloader, - ShufflePlayBuffer shufflePlayBuffer, LocalMediaPlayer localMediaPlayer) - { - this.context = context; - this.downloadQueueSerializer = downloadQueueSerializer; - this.externalStorageMonitor = externalStorageMonitor; - this.downloader = downloader; - this.shufflePlayBuffer = shufflePlayBuffer; - this.localMediaPlayer = localMediaPlayer; - - Timber.i("MediaPlayerController constructed"); - } - - public void onCreate() - { - if (created) return; - this.externalStorageMonitor.onCreate(this::reset); - - setJukeboxEnabled(activeServerProvider.getValue().getActiveServer().getJukeboxByDefault()); - created = true; - - Timber.i("MediaPlayerController created"); - } - - public void onDestroy() - { - if (!created) return; - externalStorageMonitor.onDestroy(); - context.stopService(new Intent(context, MediaPlayerService.class)); - downloader.onDestroy(); - created = false; - - Timber.i("MediaPlayerController destroyed"); - } - public synchronized void restore(List songs, final int currentPlayingIndex, final int currentPlayingPosition, final boolean autoPlay, boolean newPlaylist) - { - download(songs, false, false, false, false, newPlaylist); - - if (currentPlayingIndex != -1) - { - MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> - { - mediaPlayerService.play(currentPlayingIndex, autoPlayStart); - - if (localMediaPlayer.currentPlaying != null) - { - if (autoPlay && jukeboxMediaPlayer.getValue().isEnabled()) - { - jukeboxMediaPlayer.getValue().skip(downloader.getCurrentPlayingIndex(), currentPlayingPosition / 1000); - } - else - { - if (localMediaPlayer.currentPlaying.isCompleteFileAvailable()) - { - localMediaPlayer.play(localMediaPlayer.currentPlaying, currentPlayingPosition, autoPlay); - } - } - } - autoPlayStart = false; - return null; - } - ); - } - } - - public synchronized void preload() - { - MediaPlayerService.getInstance(context); - } - - public synchronized void play(final int index) - { - MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> { - mediaPlayerService.play(index, true); - return null; - } - ); - } - - public synchronized void play() - { - MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> { - - mediaPlayerService.play(); - return null; - } - ); - } - - public synchronized void resumeOrPlay() - { - MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> { - mediaPlayerService.resumeOrPlay(); - return null; - } - ); - } - - public synchronized void togglePlayPause() - { - if (localMediaPlayer.playerState == PlayerState.IDLE) autoPlayStart = true; - MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> { - mediaPlayerService.togglePlayPause(); - return null; - } - ); - } - - public synchronized void start() - { - MediaPlayerService.executeOnStartedMediaPlayerService(context, (mediaPlayerService) -> { - mediaPlayerService.start(); - return null; - } - ); - } - - public synchronized void seekTo(final int position) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.seekTo(position); - } - - public synchronized void pause() - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.pause(); - } - - public synchronized void stop() - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.stop(); - } - - public synchronized void download(List songs, boolean save, boolean autoPlay, boolean playNext, boolean shuffle, boolean newPlaylist) - { - downloader.download(songs, save, autoPlay, playNext, newPlaylist); - jukeboxMediaPlayer.getValue().updatePlaylist(); - - if (shuffle) shuffle(); - - if (!playNext && !autoPlay && (downloader.downloadList.size() - 1) == downloader.getCurrentPlayingIndex()) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); - } - - if (autoPlay) - { - play(0); - } - else - { - if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size() > 0) - { - localMediaPlayer.currentPlaying = downloader.downloadList.get(0); - localMediaPlayer.currentPlaying.setPlaying(true); - } - - downloader.checkDownloads(); - } - - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } - - public synchronized void downloadBackground(List songs, boolean save) - { - downloader.downloadBackground(songs, save); - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } - - public synchronized void setCurrentPlaying(DownloadFile currentPlaying) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) localMediaPlayer.setCurrentPlaying(currentPlaying); - } - - public synchronized void setCurrentPlaying(int index) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setCurrentPlaying(index); - } - - public synchronized void setPlayerState(PlayerState state) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state); - } - - public void stopJukeboxService() - { - jukeboxMediaPlayer.getValue().stopJukeboxService(); - } - - public synchronized void setShufflePlayEnabled(boolean enabled) - { - shufflePlayBuffer.isEnabled = enabled; - if (enabled) - { - clear(); - downloader.checkDownloads(); - } - } - - public boolean isShufflePlayEnabled() - { - return shufflePlayBuffer.isEnabled; - } - - public synchronized void shuffle() - { - downloader.shuffle(); - - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); - jukeboxMediaPlayer.getValue().updatePlaylist(); - - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); - } - - public RepeatMode getRepeatMode() - { - return Util.getRepeatMode(); - } - - public synchronized void setRepeatMode(RepeatMode repeatMode) - { - Util.setRepeatMode(repeatMode); - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); - } - - public boolean getKeepScreenOn() - { - return keepScreenOn; - } - - public void setKeepScreenOn(boolean keepScreenOn) - { - this.keepScreenOn = keepScreenOn; - } - - public boolean getShowVisualization() - { - return showVisualization; - } - - public void setShowVisualization(boolean showVisualization) - { - this.showVisualization = showVisualization; - } - - public synchronized void clear() - { - clear(true); - } - - public synchronized void clear(boolean serialize) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) { - mediaPlayerService.clear(serialize); - } else { - // If no MediaPlayerService is available, just empty the playlist - downloader.clear(); - if (serialize) { - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, - downloader.getCurrentPlayingIndex(), getPlayerPosition()); - } - } - - jukeboxMediaPlayer.getValue().updatePlaylist(); - } - - public synchronized void clearIncomplete() - { - reset(); - Iterator iterator = downloader.downloadList.iterator(); - - while (iterator.hasNext()) - { - DownloadFile downloadFile = iterator.next(); - if (!downloadFile.isCompleteFileAvailable()) - { - iterator.remove(); - } - } - - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); - jukeboxMediaPlayer.getValue().updatePlaylist(); - } - - public synchronized void remove(DownloadFile downloadFile) - { - if (downloadFile == localMediaPlayer.currentPlaying) - { - reset(); - setCurrentPlaying(null); - } - - downloader.removeDownloadFile(downloadFile); - - downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); - jukeboxMediaPlayer.getValue().updatePlaylist(); - - if (downloadFile == localMediaPlayer.nextPlaying) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); - } - } - - public synchronized void delete(List songs) - { - for (MusicDirectory.Entry song : songs) - { - downloader.getDownloadFileForSong(song).delete(); - } - } - - public synchronized void unpin(List songs) - { - for (MusicDirectory.Entry song : songs) - { - downloader.getDownloadFileForSong(song).unpin(); - } - } - - public synchronized void previous() - { - int index = downloader.getCurrentPlayingIndex(); - if (index == -1) - { - return; - } - - // Restart song if played more than five seconds. - if (getPlayerPosition() > 5000 || index == 0) - { - play(index); - } - else - { - play(index - 1); - } - } - - public synchronized void next() - { - int index = downloader.getCurrentPlayingIndex(); - if (index != -1) - { - switch (getRepeatMode()) - { - case SINGLE: - case OFF: - if (index + 1 >= 0 && index + 1 < downloader.downloadList.size()) { - play(index + 1); - } - break; - case ALL: - play((index + 1) % downloader.downloadList.size()); - break; - default: - break; - } - } - } - - public synchronized void reset() - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) localMediaPlayer.reset(); - } - - public synchronized int getPlayerPosition() - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService == null) return 0; - return mediaPlayerService.getPlayerPosition(); - } - - public synchronized int getPlayerDuration() - { - if (localMediaPlayer.currentPlaying != null) - { - Integer duration = localMediaPlayer.currentPlaying.getSong().getDuration(); - if (duration != null) - { - return duration * 1000; - } - } - - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService == null) return 0; - return mediaPlayerService.getPlayerDuration(); - } - - public PlayerState getPlayerState() { return localMediaPlayer.playerState; } - - public void setSuggestedPlaylistName(String name) - { - this.suggestedPlaylistName = name; - } - - public String getSuggestedPlaylistName() - { - return suggestedPlaylistName; - } - - public boolean isJukeboxEnabled() - { - return jukeboxMediaPlayer.getValue().isEnabled(); - } - - public boolean isJukeboxAvailable() - { - try - { - String username = activeServerProvider.getValue().getActiveServer().getUserName(); - UserInfo user = MusicServiceFactory.getMusicService().getUser(username); - return user.getJukeboxRole(); - } - catch (Exception e) - { - Timber.w(e, "Error getting user information"); - } - - return false; - } - - public void setJukeboxEnabled(boolean jukeboxEnabled) - { - jukeboxMediaPlayer.getValue().setEnabled(jukeboxEnabled); - setPlayerState(PlayerState.IDLE); - - if (jukeboxEnabled) - { - jukeboxMediaPlayer.getValue().startJukeboxService(); - - reset(); - - // Cancel current download, if necessary. - if (downloader.currentDownloading != null) - { - downloader.currentDownloading.cancelDownload(); - } - } - else - { - jukeboxMediaPlayer.getValue().stopJukeboxService(); - } - } - - public void adjustJukeboxVolume(boolean up) - { - jukeboxMediaPlayer.getValue().adjustVolume(up); - } - - public void setVolume(float volume) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) localMediaPlayer.setVolume(volume); - } - - public void updateNotification() - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.updateNotification(localMediaPlayer.playerState, localMediaPlayer.currentPlaying); - } - - public void toggleSongStarred() { - if (localMediaPlayer.currentPlaying == null) - return; - - final Entry song = localMediaPlayer.currentPlaying.getSong(); - - // Trigger an update - localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying); - - song.setStarred(!song.getStarred()); - } - - public void setSongRating(final int rating) - { - if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) - return; - - if (localMediaPlayer.currentPlaying == null) - return; - - final Entry song = localMediaPlayer.currentPlaying.getSong(); - song.setUserRating(rating); - - new Thread(() -> { - try - { - MusicServiceFactory.getMusicService().setRating(song.getId(), rating); - } - catch (Exception e) - { - Timber.e(e); - } - }).start(); - - updateNotification(); - } - - public DownloadFile getCurrentPlaying() { - return localMediaPlayer.currentPlaying; - } - - public int getPlaylistSize() { - return downloader.downloadList.size(); - } - - public int getCurrentPlayingNumberOnPlaylist() { - return downloader.getCurrentPlayingIndex(); - } - - public DownloadFile getCurrentDownloading() { - return downloader.currentDownloading; - } - - public List getPlayList() { - return downloader.downloadList; - } - - public long getPlayListUpdateRevision() { - return downloader.getDownloadListUpdateRevision(); - } - - public long getPlayListDuration() { - return downloader.getDownloadListDuration(); - } - - public DownloadFile getDownloadFileForSong(Entry song) { - return downloader.getDownloadFileForSong(song); - } -} \ No newline at end of file +@KoinApiExtension +@Suppress("TooManyFunctions") +class MediaPlayerController( + private val downloadQueueSerializer: DownloadQueueSerializer, + private val externalStorageMonitor: ExternalStorageMonitor, + private val downloader: Downloader, + private val shufflePlayBuffer: ShufflePlayBuffer, + private val localMediaPlayer: LocalMediaPlayer +) { + + private var created = false + var suggestedPlaylistName: String? = null + var keepScreenOn = false + var showVisualization = false + private var autoPlayStart = false + + private val jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java).value + private val activeServerProvider = inject(ActiveServerProvider::class.java).value + + fun onCreate() { + if (created) return + externalStorageMonitor.onCreate { reset() } + isJukeboxEnabled = activeServerProvider.getActiveServer().jukeboxByDefault + created = true + Timber.i("MediaPlayerController created") + } + + fun onDestroy() { + if (!created) return + val context = UApp.applicationContext() + externalStorageMonitor.onDestroy() + context.stopService(Intent(context, MediaPlayerService::class.java)) + downloader.onDestroy() + created = false + Timber.i("MediaPlayerController destroyed") + } + + @Synchronized + fun restore( + songs: List?, + currentPlayingIndex: Int, + currentPlayingPosition: Int, + autoPlay: Boolean, + newPlaylist: Boolean + ) { + download( + songs, + save = false, + autoPlay = false, + playNext = false, + shuffle = false, + newPlaylist = newPlaylist + ) + if (currentPlayingIndex != -1) { + executeOnStartedMediaPlayerService { mediaPlayerService: MediaPlayerService -> + mediaPlayerService.play(currentPlayingIndex, autoPlayStart) + if (localMediaPlayer.currentPlaying != null) { + if (autoPlay && jukeboxMediaPlayer.isEnabled) { + jukeboxMediaPlayer.skip( + downloader.currentPlayingIndex, + currentPlayingPosition / 1000 + ) + } else { + if (localMediaPlayer.currentPlaying!!.isCompleteFileAvailable) { + localMediaPlayer.play( + localMediaPlayer.currentPlaying, + currentPlayingPosition, + autoPlay + ) + } + } + } + autoPlayStart = false + } + } + } + + @Synchronized + fun preload() { + getInstance() + } + + @Synchronized + fun play(index: Int) { + executeOnStartedMediaPlayerService { service: MediaPlayerService -> + service.play(index, true) + } + } + + @Synchronized + fun play() { + executeOnStartedMediaPlayerService { service: MediaPlayerService -> + service.play() + } + } + + @Synchronized + fun resumeOrPlay() { + executeOnStartedMediaPlayerService { service: MediaPlayerService -> + service.resumeOrPlay() + } + } + + @Synchronized + fun togglePlayPause() { + if (localMediaPlayer.playerState === PlayerState.IDLE) autoPlayStart = true + executeOnStartedMediaPlayerService { service: MediaPlayerService -> + service.togglePlayPause() + } + } + + @Synchronized + fun start() { + executeOnStartedMediaPlayerService { service: MediaPlayerService -> + service.start() + } + } + + @Synchronized + fun seekTo(position: Int) { + val mediaPlayerService = runningInstance + mediaPlayerService?.seekTo(position) + } + + @Synchronized + fun pause() { + val mediaPlayerService = runningInstance + mediaPlayerService?.pause() + } + + @Synchronized + fun stop() { + val mediaPlayerService = runningInstance + mediaPlayerService?.stop() + } + + @Synchronized + @Suppress("LongParameterList") + fun download( + songs: List?, + save: Boolean, + autoPlay: Boolean, + playNext: Boolean, + shuffle: Boolean, + newPlaylist: Boolean + ) { + downloader.download(songs, save, autoPlay, playNext, newPlaylist) + jukeboxMediaPlayer.updatePlaylist() + if (shuffle) shuffle() + val isLastTrack = (downloader.downloadList.size - 1 == downloader.currentPlayingIndex) + + if (!playNext && !autoPlay && isLastTrack) { + val mediaPlayerService = runningInstance + mediaPlayerService?.setNextPlaying() + } + + if (autoPlay) { + play(0) + } else { + if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size > 0) { + localMediaPlayer.currentPlaying = downloader.downloadList[0] + downloader.downloadList[0].setPlaying(true) + } + downloader.checkDownloads() + } + + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition + ) + } + + @Synchronized + fun downloadBackground(songs: List?, save: Boolean) { + downloader.downloadBackground(songs, save) + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition + ) + } + + @Synchronized + fun setCurrentPlaying(index: Int) { + val mediaPlayerService = runningInstance + mediaPlayerService?.setCurrentPlaying(index) + } + + fun stopJukeboxService() { + jukeboxMediaPlayer.stopJukeboxService() + } + + @set:Synchronized + var isShufflePlayEnabled: Boolean + get() = shufflePlayBuffer.isEnabled + set(enabled) { + shufflePlayBuffer.isEnabled = enabled + if (enabled) { + clear() + downloader.checkDownloads() + } + } + + @Synchronized + fun shuffle() { + downloader.shuffle() + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition + ) + jukeboxMediaPlayer.updatePlaylist() + val mediaPlayerService = runningInstance + mediaPlayerService?.setNextPlaying() + } + + @set:Synchronized + var repeatMode: RepeatMode? + get() = Util.getRepeatMode() + set(repeatMode) { + Util.setRepeatMode(repeatMode) + val mediaPlayerService = runningInstance + mediaPlayerService?.setNextPlaying() + } + + @Synchronized + fun clear() { + clear(true) + } + + @Synchronized + fun clear(serialize: Boolean) { + val mediaPlayerService = runningInstance + if (mediaPlayerService != null) { + mediaPlayerService.clear(serialize) + } else { + // If no MediaPlayerService is available, just empty the playlist + downloader.clear() + if (serialize) { + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, playerPosition + ) + } + } + jukeboxMediaPlayer.updatePlaylist() + } + + @Synchronized + fun clearIncomplete() { + reset() + val iterator = downloader.downloadList.iterator() + while (iterator.hasNext()) { + val downloadFile = iterator.next() + if (!downloadFile.isCompleteFileAvailable) { + iterator.remove() + } + } + + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition + ) + + jukeboxMediaPlayer.updatePlaylist() + } + + @Synchronized + fun remove(downloadFile: DownloadFile) { + if (downloadFile == localMediaPlayer.currentPlaying) { + reset() + currentPlaying = null + } + downloader.removeDownloadFile(downloadFile) + + downloadQueueSerializer.serializeDownloadQueue( + downloader.downloadList, + downloader.currentPlayingIndex, + playerPosition + ) + + jukeboxMediaPlayer.updatePlaylist() + + if (downloadFile == localMediaPlayer.nextPlaying) { + val mediaPlayerService = runningInstance + mediaPlayerService?.setNextPlaying() + } + } + + @Synchronized + fun delete(songs: List) { + for (song in songs) { + downloader.getDownloadFileForSong(song).delete() + } + } + + @Synchronized + fun unpin(songs: List) { + for (song in songs) { + downloader.getDownloadFileForSong(song).unpin() + } + } + + @Synchronized + fun previous() { + val index = downloader.currentPlayingIndex + if (index == -1) { + return + } + + // Restart song if played more than five seconds. + @Suppress("MagicNumber") + if (playerPosition > 5000 || index == 0) { + play(index) + } else { + play(index - 1) + } + } + + @Synchronized + operator fun next() { + val index = downloader.currentPlayingIndex + if (index != -1) { + when (repeatMode) { + RepeatMode.SINGLE, RepeatMode.OFF -> { + // Play next if exists + if (index + 1 >= 0 && index + 1 < downloader.downloadList.size) { + play(index + 1) + } + } + RepeatMode.ALL -> { + play((index + 1) % downloader.downloadList.size) + } + else -> { + } + } + } + } + + @Synchronized + fun reset() { + val mediaPlayerService = runningInstance + if (mediaPlayerService != null) localMediaPlayer.reset() + } + + @get:Synchronized + val playerPosition: Int + get() { + val mediaPlayerService = runningInstance ?: return 0 + return mediaPlayerService.playerPosition + } + + @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 + } + + @set:Synchronized + var playerState: PlayerState + get() = localMediaPlayer.playerState + set(state) { + val mediaPlayerService = runningInstance + if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state) + } + + @set:Synchronized + var isJukeboxEnabled: Boolean + get() = jukeboxMediaPlayer.isEnabled + set(jukeboxEnabled) { + jukeboxMediaPlayer.isEnabled = jukeboxEnabled + playerState = PlayerState.IDLE + if (jukeboxEnabled) { + jukeboxMediaPlayer.startJukeboxService() + reset() + + // Cancel current download, if necessary. + if (downloader.currentDownloading != null) { + downloader.currentDownloading.cancelDownload() + } + } else { + jukeboxMediaPlayer.stopJukeboxService() + } + } + + @Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions + val isJukeboxAvailable: Boolean + get() { + try { + val username = activeServerProvider.getActiveServer().userName + val (_, _, _, _, _, _, _, _, _, _, _, _, jukeboxRole) = getMusicService().getUser( + username + ) + return jukeboxRole + } catch (e: Exception) { + Timber.w(e, "Error getting user information") + } + return false + } + + fun adjustJukeboxVolume(up: Boolean) { + jukeboxMediaPlayer.adjustVolume(up) + } + + fun setVolume(volume: Float) { + if (runningInstance != null) localMediaPlayer.setVolume(volume) + } + + private fun updateNotification() { + runningInstance?.updateNotification( + localMediaPlayer.playerState, + localMediaPlayer.currentPlaying + ) + } + + fun toggleSongStarred() { + if (localMediaPlayer.currentPlaying == null) return + val song = localMediaPlayer.currentPlaying!!.song + + // Trigger an update + localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) + song.starred = !song.starred + } + + @Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions + fun setSongRating(rating: Int) { + if (!get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)) return + if (localMediaPlayer.currentPlaying == null) return + val song = localMediaPlayer.currentPlaying!!.song + song.userRating = rating + Thread { + try { + getMusicService().setRating(song.id, rating) + } catch (e: Exception) { + Timber.e(e) + } + }.start() + updateNotification() + } + + @set:Synchronized + var currentPlaying: DownloadFile? + get() = localMediaPlayer.currentPlaying + set(currentPlaying) { + if (runningInstance != null) localMediaPlayer.setCurrentPlaying(currentPlaying) + } + + val playlistSize: Int + get() = downloader.downloadList.size + + val currentPlayingNumberOnPlaylist: Int + get() = downloader.currentPlayingIndex + + val currentDownloading: DownloadFile + get() = downloader.currentDownloading + + val playList: List + get() = downloader.downloadList + + val playListUpdateRevision: Long + get() = downloader.downloadListUpdateRevision + + val playListDuration: Long + get() = downloader.downloadListDuration + + fun getDownloadFileForSong(song: MusicDirectory.Entry?): DownloadFile { + return downloader.getDownloadFileForSong(song) + } + + init { + Timber.i("MediaPlayerController constructed") + } +} 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 325ecf0a..ea0b9c25 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -24,8 +24,10 @@ import android.view.KeyEvent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import org.koin.android.ext.android.inject +import org.koin.core.component.KoinApiExtension import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.RepeatMode @@ -47,6 +49,7 @@ import timber.log.Timber * Android Foreground Service for playing music * while the rest of the Ultrasonic App is in the background. */ +@KoinApiExtension @Suppress("LargeClass") class MediaPlayerService : Service() { private val binder: IBinder = SimpleServiceBinder(this) @@ -906,7 +909,8 @@ class MediaPlayerService : Service() { private val instanceLock = Any() @JvmStatic - fun getInstance(context: Context): MediaPlayerService? { + fun getInstance(): MediaPlayerService? { + val context = UApp.applicationContext() synchronized(instanceLock) { for (i in 0..19) { if (instance != null) return instance @@ -931,18 +935,18 @@ class MediaPlayerService : Service() { @JvmStatic fun executeOnStartedMediaPlayerService( - context: Context, - taskToExecute: (MediaPlayerService?) -> Unit + taskToExecute: (MediaPlayerService) -> Unit ) { val t: Thread = object : Thread() { override fun run() { - val instance = getInstance(context) + val instance = getInstance() if (instance == null) { Timber.e("ExecuteOnStarted.. failed to get a MediaPlayerService instance!") return + } else { + taskToExecute(instance) } - taskToExecute(instance) } } t.start()