From 0820763c7fba7554c0cea6ebde401632929770e9 Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 23 Jun 2020 18:40:44 +0200 Subject: [PATCH] Refactored playback related things --- .../ultrasonic/activity/BookmarkActivity.java | 2 +- .../ultrasonic/activity/DownloadActivity.java | 36 +- .../ultrasonic/activity/SearchActivity.java | 2 +- .../activity/SelectAlbumActivity.java | 2 +- .../activity/SubsonicTabActivity.java | 5 +- .../receiver/A2dpIntentReceiver.java | 17 +- .../moire/ultrasonic/service/BiConsumer.java | 6 + .../ultrasonic/service/DownloadFile.java | 5 +- .../ultrasonic/service/DownloadService.java | 24 - .../service/DownloadServiceImpl.java | 325 +--- .../DownloadServiceLifecycleSupport.java | 10 +- .../moire/ultrasonic/service/Downloader.java | 454 +++++ .../ultrasonic/service/JukeboxService.java | 13 +- .../service/MediaPlayerService.java | 1588 +++-------------- .../org/moire/ultrasonic/service/Player.java | 1070 +++++++++++ .../moire/ultrasonic/util/CacheCleaner.java | 7 +- .../ultrasonic/util/ShufflePlayBuffer.java | 20 +- .../java/org/moire/ultrasonic/util/Util.java | 18 +- .../org/moire/ultrasonic/view/SongView.java | 11 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 14 +- 20 files changed, 1903 insertions(+), 1726 deletions(-) create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/Player.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java index a7b7a52d..703422ec 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java @@ -310,7 +310,7 @@ public class BookmarkActivity extends SubsonicTabActivity for (MusicDirectory.Entry song : selection) { - DownloadFile downloadFile = getDownloadService().forSong(song); + DownloadFile downloadFile = downloader.getValue().getDownloadFileForSong(song); if (downloadFile.isWorkDone()) { deleteEnabled = true; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java index 28db6a4a..2cc55a02 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java @@ -293,7 +293,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Boolean doInBackground() throws Throwable { - if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) + if (downloader.getValue().getCurrentPlayingIndex() < downloader.getValue().downloadList.size() - 1) { getDownloadService().next(); return true; @@ -567,7 +567,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi final DownloadService downloadService = getDownloadService(); - if (downloadService == null || downloadService.getCurrentPlaying() == null) + if (downloadService == null || player.getValue().currentPlaying == null) { playlistFlipper.setDisplayedChild(1); } @@ -632,7 +632,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi } } - final DownloadFile currentDownloading = getDownloadService().getCurrentDownloading(); + final DownloadFile currentDownloading = downloader.getValue().currentDownloading; for (int i = 0; i < count; i++) { if (currentDownloading == playlistView.getItemAtPosition(i)) @@ -782,7 +782,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (downloadService != null) { - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + DownloadFile downloadFile = player.getValue().currentPlaying; if (downloadFile != null) { @@ -1019,7 +1019,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi onDownloadListChanged(); return true; case R.id.menu_item_save_playlist: - if (!getDownloadService().getSongs().isEmpty()) + if (!downloader.getValue().downloadList.isEmpty()) { showDialog(DIALOG_SAVE_PLAYLIST); } @@ -1142,7 +1142,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (downloadService != null) { - List downloadServiceSongs = downloadService.getSongs(); + List downloadServiceSongs = downloader.getValue().downloadList; if (downloadServiceSongs != null) { @@ -1175,12 +1175,12 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi return; } - if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) + if (currentRevision != downloader.getValue().getDownloadListUpdateRevision()) { onDownloadListChanged(); } - if (currentPlaying != getDownloadService().getCurrentPlaying()) + if (currentPlaying != player.getValue().currentPlaying) { onCurrentChanged(); } @@ -1199,7 +1199,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi protected Void doInBackground() throws Throwable { final List entries = new LinkedList(); - for (final DownloadFile downloadFile : getDownloadService().getSongs()) + for (final DownloadFile downloadFile : downloader.getValue().downloadList) { entries.add(downloadFile.getSong()); } @@ -1254,7 +1254,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi { warnIfNetworkOrStorageUnavailable(); - final int current = service.getCurrentPlayingIndex(); + final int current = downloader.getValue().getCurrentPlayingIndex(); if (current == -1) { @@ -1275,7 +1275,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi return; } - final List list = downloadService.getSongs(); + final List list = downloader.getValue().downloadList; emptyTextView.setText(R.string.download_empty); final SongListAdapter adapter = new SongListAdapter(this, list); @@ -1313,7 +1313,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi return; } - DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + DownloadFile currentPlaying = player.getValue().currentPlaying; if (currentPlaying == item) { @@ -1333,7 +1333,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi }); emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); - currentRevision = downloadService.getDownloadListUpdateRevision(); + currentRevision = downloader.getValue().getDownloadListUpdateRevision(); switch (downloadService.getRepeatMode()) { @@ -1360,13 +1360,13 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi return; } - currentPlaying = downloadService.getCurrentPlaying(); + currentPlaying = player.getValue().currentPlaying; scrollToCurrent(); - long totalDuration = downloadService.getDownloadListDuration(); - long totalSongs = downloadService.getSongs().size(); - int currentSongIndex = downloadService.getCurrentPlayingIndex() + 1; + long totalDuration = downloader.getValue().getDownloadListDuration(); + long totalSongs = downloader.getValue().downloadList.size(); + int currentSongIndex = downloader.getValue().getCurrentPlayingIndex() + 1; String duration = Util.formatTotalDuration(totalDuration); @@ -1580,7 +1580,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (e1X - e2X > swipeDistance && absX > swipeVelocity) { warnIfNetworkOrStorageUnavailable(); - if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) + if (downloader.getValue().getCurrentPlayingIndex() < downloader.getValue().downloadList.size() - 1) { downloadService.next(); onCurrentChanged(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java index 6ed7e942..fb0b542b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java @@ -520,7 +520,7 @@ public class SearchActivity extends SubsonicTabActivity if (autoplay) { - downloadService.play(downloadService.size() - 1); + downloadService.play(downloader.getValue().downloadList.size() - 1); } Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java index d975160e..76ed4e51 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java @@ -1024,7 +1024,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity for (MusicDirectory.Entry song : selection) { - DownloadFile downloadFile = getDownloadService().forSong(song); + DownloadFile downloadFile = downloader.getValue().getDownloadFileForSong(song); if (downloadFile.isWorkDone()) { deleteEnabled = true; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index c26f30de..3084b4a6 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -78,6 +78,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); private Lazy lifecycleSupport = inject(DownloadServiceLifecycleSupport.class); + protected Lazy downloader = inject(Downloader.class); + protected Lazy player = inject(Player.class); + public MenuDrawer menuDrawer; private int activePosition = 1; @@ -266,7 +269,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED)) { - DownloadFile file = downloadServiceImpl.getValue().getCurrentPlaying(); + DownloadFile file = player.getValue().currentPlaying; if (file != null) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java index 85ce1782..214436c7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java @@ -5,8 +5,9 @@ import android.content.Context; import android.content.Intent; import org.moire.ultrasonic.domain.MusicDirectory.Entry; -import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.Downloader; +import org.moire.ultrasonic.service.Player; import kotlin.Lazy; @@ -16,16 +17,18 @@ public class A2dpIntentReceiver extends BroadcastReceiver { private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse"; private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + private Lazy downloader = inject(Downloader.class); + protected Lazy player = inject(Player.class); @Override public void onReceive(Context context, Intent intent) { - if (downloadServiceImpl.getValue().getCurrentPlaying() == null) + if (player.getValue().currentPlaying == null) { return; } - Entry song = downloadServiceImpl.getValue().getCurrentPlaying().getSong(); + Entry song = player.getValue().currentPlaying.getSong(); if (song == null) { @@ -35,8 +38,8 @@ public class A2dpIntentReceiver extends BroadcastReceiver Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE); Integer duration = song.getDuration(); - Integer playerPosition = downloadServiceImpl.getValue().getPlayerPosition(); - Integer listSize = downloadServiceImpl.getValue().getDownloads().size(); + int playerPosition = downloadServiceImpl.getValue().getPlayerPosition(); + int listSize = downloader.getValue().getDownloads().size(); if (duration != null) { @@ -52,11 +55,7 @@ public class A2dpIntentReceiver extends BroadcastReceiver avrcpIntent.putExtra("playing", true); break; case STOPPED: - avrcpIntent.putExtra("playing", false); - break; case PAUSED: - avrcpIntent.putExtra("playing", false); - break; case COMPLETED: avrcpIntent.putExtra("playing", false); break; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java new file mode 100644 index 00000000..537ef5f4 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java @@ -0,0 +1,6 @@ +package org.moire.ultrasonic.service; + +public abstract class BiConsumer +{ + public abstract void accept(T t, U u); +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java index 2d4c7646..2d0c7b90 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java @@ -68,6 +68,9 @@ public class DownloadFile private volatile boolean saveWhenDone; private volatile boolean completeWhenDone; + private Lazy downloader = inject(Downloader.class); + + public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) { super(); @@ -443,7 +446,7 @@ public class DownloadFile new CacheCleaner(context).cleanSpace(); - MediaPlayerService.checkDownloads(context); + downloader.getValue().checkDownloads(); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadService.java index 9e69b722..b11eaaf6 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadService.java @@ -61,30 +61,10 @@ public interface DownloadService void clear(); - void clearBackground(); - void clearIncomplete(); - int size(); - - void remove(int which); - void remove(DownloadFile downloadFile); - long getDownloadListDuration(); - - List getSongs(); - - List getDownloads(); - - List getBackgroundDownloads(); - - int getCurrentPlayingIndex(); - - DownloadFile getCurrentPlaying(); - - DownloadFile getCurrentDownloading(); - void play(int index); void seekTo(int position); @@ -111,10 +91,6 @@ public interface DownloadService void unpin(List songs); - DownloadFile forSong(Entry song); - - long getDownloadListUpdateRevision(); - void setSuggestedPlaylistName(String name); String getSuggestedPlaylistName(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java index ba3e1de0..b4262ec7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java @@ -32,19 +32,15 @@ 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.LRUCache; import org.moire.ultrasonic.util.ShufflePlayBuffer; import org.moire.ultrasonic.util.Util; -import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import kotlin.Lazy; import static org.koin.java.standalone.KoinJavaComponent.inject; -import static org.moire.ultrasonic.service.MediaPlayerService.playerState; /** * @author Sindre Mehus, Joshua Bahnsen @@ -54,8 +50,6 @@ public class DownloadServiceImpl implements DownloadService { private static final String TAG = DownloadServiceImpl.class.getSimpleName(); - private final LRUCache downloadFileCache = new LRUCache<>(100); - private String suggestedPlaylistName; private boolean keepScreenOn; @@ -66,13 +60,18 @@ public class DownloadServiceImpl implements DownloadService public Lazy jukeboxService = inject(JukeboxService.class); private Lazy downloadQueueSerializer = inject(DownloadQueueSerializer.class); private Lazy externalStorageMonitor = inject(ExternalStorageMonitor.class); + private final Downloader downloader; + private final ShufflePlayBuffer shufflePlayBuffer; + private final Player player; - public DownloadServiceImpl(Context context) + public DownloadServiceImpl(Context context, Downloader downloader, ShufflePlayBuffer shufflePlayBuffer, + Player player) { this.context = context; + this.downloader = downloader; + this.shufflePlayBuffer = shufflePlayBuffer; + this.player = player; - // TODO: refactor - MediaPlayerService.shufflePlayBuffer = new ShufflePlayBuffer(context); externalStorageMonitor.getValue().onCreate(new Runnable() { @Override public void run() { @@ -120,26 +119,25 @@ public class DownloadServiceImpl implements DownloadService } }); - if (MediaPlayerService.currentPlaying != null) + if (player.currentPlaying != null) { if (autoPlay && jukeboxService.getValue().isEnabled()) { - jukeboxService.getValue().skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000); + jukeboxService.getValue().skip(downloader.getCurrentPlayingIndex(), currentPlayingPosition / 1000); } else { - if (MediaPlayerService.currentPlaying.isCompleteFileAvailable()) + if (player.currentPlaying.isCompleteFileAvailable()) { executeOnStartedMediaPlayerService(new Consumer() { @Override public void accept(MediaPlayerService mediaPlayerService) { - mediaPlayerService.doPlay(MediaPlayerService.currentPlaying, currentPlayingPosition, autoPlay); + player.doPlay(player.currentPlaying, currentPlayingPosition, autoPlay); } }); } } } - autoPlayStart = false; } } @@ -168,7 +166,7 @@ public class DownloadServiceImpl implements DownloadService @Override public synchronized void togglePlayPause() { - if (playerState == PlayerState.IDLE) autoPlayStart = true; + if (player.playerState == PlayerState.IDLE) autoPlayStart = true; executeOnStartedMediaPlayerService(new Consumer() { @Override public void accept(MediaPlayerService mediaPlayerService) { @@ -222,96 +220,42 @@ public class DownloadServiceImpl implements DownloadService } @Override - public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist) + public synchronized void download(List songs, boolean save, boolean autoPlay, boolean playNext, boolean shuffle, boolean newPlaylist) { - MediaPlayerService.shufflePlay = false; - int offset = 1; - - if (songs.isEmpty()) - { - return; - } - - if (newPlaylist) - { - MediaPlayerService.downloadList.clear(); - } - - if (playNext) - { - if (autoplay && getCurrentPlayingIndex() >= 0) - { - offset = 0; - } - - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(context, song, save); - MediaPlayerService.downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); - offset++; - } - } - else - { - int size = size(); - int index = getCurrentPlayingIndex(); - - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(context, song, save); - MediaPlayerService.downloadList.add(downloadFile); - } - - if (!autoplay && (size - 1) == index) - { - MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); - } - - } - MediaPlayerService.revision++; - + downloader.download(songs, save, autoPlay, playNext, newPlaylist); jukeboxService.getValue().updatePlaylist(); if (shuffle) shuffle(); - if (autoplay) + if (!playNext && !autoPlay && (downloader.downloadList.size() - 1) == downloader.getCurrentPlayingIndex()) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); + } + + if (autoPlay) { play(0); } else { - if (MediaPlayerService.currentPlaying == null) - { - MediaPlayerService.currentPlaying = MediaPlayerService.downloadList.get(0); - MediaPlayerService.currentPlaying.setPlaying(true); - } - - MediaPlayerService.checkDownloads(context); + downloader.setFirstPlaying(); } - downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); } @Override public synchronized void downloadBackground(List songs, boolean save) { - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(context, song, save); - MediaPlayerService.backgroundDownloadList.add(downloadFile); - } - - MediaPlayerService.revision++; - - MediaPlayerService.checkDownloads(context); - downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + downloader.downloadBackground(songs, save); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); } public synchronized void setCurrentPlaying(DownloadFile currentPlaying) { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setCurrentPlaying(currentPlaying); + if (mediaPlayerService != null) player.setCurrentPlaying(currentPlaying); } public synchronized void setCurrentPlaying(int index) @@ -323,7 +267,7 @@ public class DownloadServiceImpl implements DownloadService public synchronized void setPlayerState(PlayerState state) { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setPlayerState(state); + if (mediaPlayerService != null) player.setPlayerState(state); } @Override @@ -341,31 +285,26 @@ public class DownloadServiceImpl implements DownloadService @Override public synchronized void setShufflePlayEnabled(boolean enabled) { - MediaPlayerService.shufflePlay = enabled; - if (MediaPlayerService.shufflePlay) + shufflePlayBuffer.isEnabled = enabled; + if (enabled) { clear(); - MediaPlayerService.checkDownloads(context); + downloader.checkDownloads(); } } @Override public boolean isShufflePlayEnabled() { - return MediaPlayerService.shufflePlay; + return shufflePlayBuffer.isEnabled; } @Override public synchronized void shuffle() { - Collections.shuffle(MediaPlayerService.downloadList); - if (MediaPlayerService.currentPlaying != null) - { - MediaPlayerService.downloadList.remove(getCurrentPlayingIndex()); - MediaPlayerService.downloadList.add(0, MediaPlayerService.currentPlaying); - } - MediaPlayerService.revision++; - downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + downloader.shuffle(); + + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); jukeboxService.getValue().updatePlaylist(); MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); @@ -410,33 +349,6 @@ public class DownloadServiceImpl implements DownloadService this.showVisualization = showVisualization; } - @Override - public synchronized DownloadFile forSong(MusicDirectory.Entry song) - { - for (DownloadFile downloadFile : MediaPlayerService.downloadList) - { - if (downloadFile.getSong().equals(song) && ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) - { - return downloadFile; - } - } - for (DownloadFile downloadFile : MediaPlayerService.backgroundDownloadList) - { - if (downloadFile.getSong().equals(song)) - { - return downloadFile; - } - } - - DownloadFile downloadFile = downloadFileCache.get(song); - if (downloadFile == null) - { - downloadFile = new DownloadFile(context, song, false); - downloadFileCache.put(song, downloadFile); - } - return downloadFile; - } - @Override public synchronized void clear() { @@ -445,30 +357,17 @@ public class DownloadServiceImpl implements DownloadService public synchronized void clear(boolean serialize) { - MediaPlayerService.clear(serialize); - jukeboxService.getValue().updatePlaylist(); - if (serialize) - { - downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); - } - } + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.clear(serialize); - @Override - public synchronized void clearBackground() - { - if (MediaPlayerService.currentDownloading != null && MediaPlayerService.backgroundDownloadList.contains(MediaPlayerService.currentDownloading)) - { - MediaPlayerService.currentDownloading.cancelDownload(); - MediaPlayerService.currentDownloading = null; - } - MediaPlayerService.backgroundDownloadList.clear(); + jukeboxService.getValue().updatePlaylist(); } @Override public synchronized void clearIncomplete() { reset(); - Iterator iterator = MediaPlayerService.downloadList.iterator(); + Iterator iterator = downloader.downloadList.iterator(); while (iterator.hasNext()) { @@ -479,41 +378,25 @@ public class DownloadServiceImpl implements DownloadService } } - downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); jukeboxService.getValue().updatePlaylist(); } - @Override - public synchronized int size() - { - return MediaPlayerService.downloadList.size(); - } - - @Override - public synchronized void remove(int which) - { - MediaPlayerService.downloadList.remove(which); - } - @Override public synchronized void remove(DownloadFile downloadFile) { - if (downloadFile == MediaPlayerService.currentDownloading) - { - MediaPlayerService.currentDownloading.cancelDownload(); - MediaPlayerService.currentDownloading = null; - } - if (downloadFile == MediaPlayerService.currentPlaying) + if (downloadFile == player.currentPlaying) { reset(); setCurrentPlaying(null); } - MediaPlayerService.downloadList.remove(downloadFile); - MediaPlayerService.backgroundDownloadList.remove(downloadFile); - MediaPlayerService.revision++; - downloadQueueSerializer.getValue().serializeDownloadQueue(getSongs(), getCurrentPlayingIndex(), getPlayerPosition()); + + downloader.removeDownloadFile(downloadFile); + + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); jukeboxService.getValue().updatePlaylist(); - if (downloadFile == MediaPlayerService.nextPlaying) + + if (downloadFile == player.nextPlaying) { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); @@ -525,7 +408,7 @@ public class DownloadServiceImpl implements DownloadService { for (MusicDirectory.Entry song : songs) { - forSong(song).delete(); + downloader.getDownloadFileForSong(song).delete(); } } @@ -534,76 +417,14 @@ public class DownloadServiceImpl implements DownloadService { for (MusicDirectory.Entry song : songs) { - forSong(song).unpin(); + downloader.getDownloadFileForSong(song).unpin(); } } - @Override - public synchronized int getCurrentPlayingIndex() - { - return MediaPlayerService.downloadList.indexOf(MediaPlayerService.currentPlaying); - } - - @Override - public DownloadFile getCurrentPlaying() - { - return MediaPlayerService.currentPlaying; - } - - @Override - public DownloadFile getCurrentDownloading() - { - return MediaPlayerService.currentDownloading; - } - - @Override - public List getSongs() { return MediaPlayerService.downloadList; } - - @Override - public long getDownloadListDuration() - { - long totalDuration = 0; - - for (DownloadFile downloadFile : MediaPlayerService.downloadList) - { - Entry entry = downloadFile.getSong(); - - if (!entry.isDirectory()) - { - if (entry.getArtist() != null) - { - Integer duration = entry.getDuration(); - - if (duration != null) - { - totalDuration += duration; - } - } - } - } - - return totalDuration; - } - - @Override - public synchronized List getDownloads() - { - List temp = new ArrayList<>(); - temp.addAll(MediaPlayerService.downloadList); - temp.addAll(MediaPlayerService.backgroundDownloadList); - return temp; - } - - @Override - public List getBackgroundDownloads() - { - return MediaPlayerService.backgroundDownloadList; - } - @Override public synchronized void previous() { - int index = getCurrentPlayingIndex(); + int index = downloader.getCurrentPlayingIndex(); if (index == -1) { return; @@ -623,7 +444,7 @@ public class DownloadServiceImpl implements DownloadService @Override public synchronized void next() { - int index = getCurrentPlayingIndex(); + int index = downloader.getCurrentPlayingIndex(); if (index != -1) { play(index + 1); @@ -634,7 +455,7 @@ public class DownloadServiceImpl implements DownloadService public synchronized void reset() { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.reset(); + if (mediaPlayerService != null) player.reset(); } @Override @@ -648,24 +469,22 @@ public class DownloadServiceImpl implements DownloadService @Override public synchronized int getPlayerDuration() { - if (MediaPlayerService.currentPlaying != null) + if (player.currentPlaying != null) { - Integer duration = MediaPlayerService.currentPlaying.getSong().getDuration(); + Integer duration = player.currentPlaying.getSong().getDuration(); if (duration != null) { return duration * 1000; } } + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); if (mediaPlayerService == null) return 0; return mediaPlayerService.getPlayerDuration(); } @Override - public PlayerState getPlayerState() - { - return playerState; - } + public PlayerState getPlayerState() { return player.playerState; } @Override public void setSuggestedPlaylistName(String name) @@ -680,12 +499,12 @@ public class DownloadServiceImpl implements DownloadService } @Override - public boolean getEqualizerAvailable() { return MediaPlayerService.equalizerAvailable; } + public boolean getEqualizerAvailable() { return player.equalizerAvailable; } @Override public boolean getVisualizerAvailable() { - return MediaPlayerService.visualizerAvailable; + return player.visualizerAvailable; } @Override @@ -693,7 +512,7 @@ public class DownloadServiceImpl implements DownloadService { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); if (mediaPlayerService == null) return null; - return mediaPlayerService.getEqualizerController(); + return player.getEqualizerController(); } @Override @@ -701,7 +520,7 @@ public class DownloadServiceImpl implements DownloadService { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); if (mediaPlayerService == null) return null; - return mediaPlayerService.getVisualizerController(); + return player.getVisualizerController(); } @Override @@ -757,9 +576,9 @@ public class DownloadServiceImpl implements DownloadService reset(); // Cancel current download, if necessary. - if (MediaPlayerService.currentDownloading != null) + if (downloader.currentDownloading != null) { - MediaPlayerService.currentDownloading.cancelDownload(); + downloader.currentDownloading.cancelDownload(); } } else @@ -778,13 +597,13 @@ public class DownloadServiceImpl implements DownloadService public void setVolume(float volume) { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.setVolume(volume); + if (mediaPlayerService != null) player.setVolume(volume); } @Override public synchronized void swap(boolean mainList, int from, int to) { - List list = mainList ? MediaPlayerService.downloadList : MediaPlayerService.backgroundDownloadList; + List list = mainList ? downloader.downloadList : downloader.backgroundDownloadList; int max = list.size(); if (to >= max) @@ -796,7 +615,7 @@ public class DownloadServiceImpl implements DownloadService to = 0; } - int currentPlayingIndex = getCurrentPlayingIndex(); + int currentPlayingIndex = downloader.getCurrentPlayingIndex(); DownloadFile movedSong = list.remove(from); list.add(to, movedSong); @@ -804,7 +623,7 @@ public class DownloadServiceImpl implements DownloadService { jukeboxService.getValue().updatePlaylist(); } - else if (mainList && (movedSong == MediaPlayerService.nextPlaying || (currentPlayingIndex + 1) == to)) + else if (mainList && (movedSong == player.nextPlaying || (currentPlayingIndex + 1) == to)) { // Moving next playing or moving a song to be next playing MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); @@ -812,17 +631,11 @@ public class DownloadServiceImpl implements DownloadService } } - @Override - public long getDownloadListUpdateRevision() - { - return MediaPlayerService.revision; - } - @Override public void updateNotification() { MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); - if (mediaPlayerService != null) mediaPlayerService.updateNotification(); + if (mediaPlayerService != null) mediaPlayerService.updateNotification(player.playerState, player.currentPlaying); } public void setSongRating(final int rating) @@ -830,10 +643,10 @@ public class DownloadServiceImpl implements DownloadService if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) return; - if (MediaPlayerService.currentPlaying == null) + if (player.currentPlaying == null) return; - final Entry song = MediaPlayerService.currentPlaying.getSong(); + final Entry song = player.currentPlaying.getSong(); song.setUserRating(rating); new Thread(new Runnable() diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java index a8ce6468..407a2fd2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java @@ -49,14 +49,16 @@ public class DownloadServiceLifecycleSupport private Lazy downloadQueueSerializer = inject(DownloadQueueSerializer.class); private final DownloadServiceImpl downloadService; // From DI + private final Downloader downloader; // From DI private BroadcastReceiver headsetEventReceiver; private Context context; - public DownloadServiceLifecycleSupport(Context context, final DownloadServiceImpl downloadService) + public DownloadServiceLifecycleSupport(Context context, final DownloadServiceImpl downloadService, final Downloader downloader) { this.downloadService = downloadService; this.context = context; + this.downloader = downloader; registerHeadsetReceiver(); @@ -80,8 +82,8 @@ public class DownloadServiceLifecycleSupport downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition, false, false); // Work-around: Serialize again, as the restore() method creates a serialization without current playing info. - downloadQueueSerializer.getValue().serializeDownloadQueue(downloadService.getSongs(), - downloadService.getCurrentPlayingIndex(), downloadService.getPlayerPosition()); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.downloadList, + downloader.getCurrentPlayingIndex(), downloadService.getPlayerPosition()); } }); @@ -165,7 +167,7 @@ public class DownloadServiceLifecycleSupport downloadService.previous(); break; case KeyEvent.KEYCODE_MEDIA_NEXT: - if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) + if (downloader.getCurrentPlayingIndex() < downloader.downloadList.size() - 1) { downloadService.next(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java new file mode 100644 index 00000000..3b694012 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java @@ -0,0 +1,454 @@ +package org.moire.ultrasonic.service; + +import android.content.Context; +import android.util.Log; + +import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.util.LRUCache; +import org.moire.ultrasonic.util.ShufflePlayBuffer; +import org.moire.ultrasonic.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; +import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; +import static org.moire.ultrasonic.domain.PlayerState.STARTED; + +public class Downloader +{ + private static final String TAG = Downloader.class.getSimpleName(); + + private final ShufflePlayBuffer shufflePlayBuffer; + private final ExternalStorageMonitor externalStorageMonitor; + private final Player player; + public Lazy jukeboxService = inject(JukeboxService.class); + + public final List downloadList = new ArrayList<>(); + public final List backgroundDownloadList = new ArrayList<>(); + private final List cleanupCandidates = new ArrayList<>(); + private final LRUCache downloadFileCache = new LRUCache<>(100); + + public DownloadFile currentDownloading; + public static long revision; + + private ScheduledExecutorService executorService; + private Context context; + + public Downloader(Context context, ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor, + Player player) + { + this.context = context; + this.shufflePlayBuffer = shufflePlayBuffer; + this.externalStorageMonitor = externalStorageMonitor; + this.player = player; + } + + public void onCreate() + { + Runnable downloadChecker = new Runnable() + { + @Override + public void run() + { + try + { + checkDownloads(); + } + catch (Throwable x) + { + Log.e(TAG, "checkDownloads() failed.", x); + } + } + }; + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); + Log.i(TAG, "Downloader created"); + + } + + public void onDestroy() + { + executorService.shutdown(); + Log.i(TAG, "Downloader destroyed"); + } + + protected synchronized void checkDownloads() + { + if (!Util.isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable()) + { + return; + } + + if (shufflePlayBuffer.isEnabled) + { + checkShufflePlay(context); + } + + if (jukeboxService.getValue().isEnabled() || !Util.isNetworkConnected(context)) + { + return; + } + + if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) + { + return; + } + + // Need to download current playing? + if (player.currentPlaying != null && player.currentPlaying != currentDownloading && !player.currentPlaying.isWorkDone()) + { + // Cancel current download, if necessary. + if (currentDownloading != null) + { + currentDownloading.cancelDownload(); + } + + currentDownloading = player.currentPlaying; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + + // Delete obsolete .partial and .complete files. + cleanup(); + return; + } + + // Find a suitable target for download. + if (currentDownloading != null && + !currentDownloading.isWorkDone() && + (!currentDownloading.isFailed() || (downloadList.isEmpty() && backgroundDownloadList.isEmpty()))) + { + cleanup(); + return; + } + + // There is a target to download + currentDownloading = null; + int n = downloadList.size(); + + int preloaded = 0; + + if (n != 0) + { + int start = player.currentPlaying == null ? 0 : getCurrentPlayingIndex(); + if (start == -1) start = 0; + + int i = start; + do + { + DownloadFile downloadFile = downloadList.get(i); + if (!downloadFile.isWorkDone()) + { + if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(context)) + { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + if (i == (start + 1)) + { + player.setNextPlayerState(DOWNLOADING); + } + break; + } + } + else if (player.currentPlaying != downloadFile) + { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(context) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) + { + for (int i = 0; i < backgroundDownloadList.size(); i++) + { + DownloadFile downloadFile = backgroundDownloadList.get(i); + if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) + { + if (Util.getShouldScanMedia(context)) + { + Util.scanMedia(context, downloadFile.getCompleteFile()); + } + + // Don't need to keep list like active song list + backgroundDownloadList.remove(i); + revision++; + i--; + } + else + { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + break; + } + } + } + + // Delete obsolete .partial and .complete files. + cleanup(); + } + + public synchronized int getCurrentPlayingIndex() + { + return downloadList.indexOf(player.currentPlaying); + } + + public long getDownloadListDuration() + { + long totalDuration = 0; + + for (DownloadFile downloadFile : downloadList) + { + MusicDirectory.Entry entry = downloadFile.getSong(); + + if (!entry.isDirectory()) + { + if (entry.getArtist() != null) + { + Integer duration = entry.getDuration(); + + if (duration != null) + { + totalDuration += duration; + } + } + } + } + + return totalDuration; + } + + public synchronized List getDownloads() + { + List temp = new ArrayList<>(); + temp.addAll(downloadList); + temp.addAll(backgroundDownloadList); + return temp; + } + + public List getBackgroundDownloads() + { + return backgroundDownloadList; + } + + public long getDownloadListUpdateRevision() + { + return revision; + } + + public synchronized void clear() + { + downloadList.clear(); + revision++; + if (currentDownloading != null) + { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + } + + public synchronized void clearBackground() + { + if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) + { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + backgroundDownloadList.clear(); + } + + public synchronized void removeDownloadFile(DownloadFile downloadFile) + { + if (downloadFile == currentDownloading) + { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + + downloadList.remove(downloadFile); + backgroundDownloadList.remove(downloadFile); + revision++; + } + + public synchronized void download(List songs, boolean save, boolean autoPlay, boolean playNext, boolean newPlaylist) + { + shufflePlayBuffer.isEnabled = false; + int offset = 1; + + if (songs.isEmpty()) + { + return; + } + + if (newPlaylist) + { + downloadList.clear(); + } + + if (playNext) + { + if (autoPlay && getCurrentPlayingIndex() >= 0) + { + offset = 0; + } + + for (MusicDirectory.Entry song : songs) + { + DownloadFile downloadFile = new DownloadFile(context, song, save); + downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); + offset++; + } + } + else + { + for (MusicDirectory.Entry song : songs) + { + DownloadFile downloadFile = new DownloadFile(context, song, save); + downloadList.add(downloadFile); + } + } + revision++; + } + + public synchronized void downloadBackground(List songs, boolean save) + { + for (MusicDirectory.Entry song : songs) + { + DownloadFile downloadFile = new DownloadFile(context, song, save); + backgroundDownloadList.add(downloadFile); + } + + revision++; + + checkDownloads(); + } + + public synchronized void shuffle() + { + Collections.shuffle(downloadList); + if (player.currentPlaying != null) + { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, player.currentPlaying); + } + revision++; + } + + public synchronized void setFirstPlaying() + { + if (player.currentPlaying == null) + { + player.currentPlaying = downloadList.get(0); + player.currentPlaying.setPlaying(true); + } + + checkDownloads(); + } + + public synchronized DownloadFile getDownloadFileForSong(MusicDirectory.Entry song) + { + for (DownloadFile downloadFile : downloadList) + { + if (downloadFile.getSong().equals(song) && ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone())) + { + return downloadFile; + } + } + for (DownloadFile downloadFile : backgroundDownloadList) + { + if (downloadFile.getSong().equals(song)) + { + return downloadFile; + } + } + + DownloadFile downloadFile = downloadFileCache.get(song); + if (downloadFile == null) + { + downloadFile = new DownloadFile(context, song, false); + downloadFileCache.put(song, downloadFile); + } + return downloadFile; + } + + private synchronized void cleanup() + { + Iterator iterator = cleanupCandidates.iterator(); + while (iterator.hasNext()) + { + DownloadFile downloadFile = iterator.next(); + if (downloadFile != player.currentPlaying && downloadFile != currentDownloading) + { + if (downloadFile.cleanup()) + { + iterator.remove(); + } + } + } + } + + private synchronized void checkShufflePlay(Context context) + { + // Get users desired random playlist size + int listSize = Util.getMaxSongs(context); + boolean wasEmpty = downloadList.isEmpty(); + + long revisionBefore = revision; + + // First, ensure that list is at least 20 songs long. + int size = downloadList.size(); + if (size < listSize) + { + for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) + { + DownloadFile downloadFile = new DownloadFile(context, song, false); + downloadList.add(downloadFile); + revision++; + } + } + + int currIndex = player.currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + // Only shift playlist if playing song #5 or later. + if (currIndex > 4) + { + int songsToShift = currIndex - 2; + for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) + { + downloadList.add(new DownloadFile(context, song, false)); + downloadList.get(0).cancelDownload(); + downloadList.remove(0); + revision++; + } + } + + if (revisionBefore != revision) + { + jukeboxService.getValue().updatePlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) + { + if (jukeboxService.getValue().isEnabled()) + { + jukeboxService.getValue().skip(0, 0); + player.setPlayerState(STARTED); + } + else + { + player.play(downloadList.get(0)); + } + } + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java index afbda458..449e6a43 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java @@ -71,7 +71,9 @@ public class JukeboxService private boolean enabled = false; private Context context; + // TODO: These create circular references, try to refactor private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + private final Downloader downloader; // TODO: Report warning if queue fills up. // TODO: Create shutdown method? @@ -79,9 +81,10 @@ public class JukeboxService // TODO: Persist RC state? // TODO: Minimize status updates. - public JukeboxService(Context context) + public JukeboxService(Context context, Downloader downloader) { this.context = context; + this.downloader = downloader; } public void startJukeboxService() @@ -182,7 +185,7 @@ public class JukeboxService // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); - if (index != null && index != -1 && index != downloadServiceImpl.getValue().getCurrentPlayingIndex()) + if (index != null && index != -1 && index != downloader.getCurrentPlayingIndex()) { downloadServiceImpl.getValue().setCurrentPlaying(index); } @@ -232,8 +235,8 @@ public class JukeboxService tasks.remove(Stop.class); tasks.remove(Start.class); - List ids = new ArrayList(); - for (DownloadFile file : downloadServiceImpl.getValue().getDownloads()) + List ids = new ArrayList<>(); + for (DownloadFile file : downloader.getDownloads()) { ids.add(file.getSong().getId()); } @@ -334,7 +337,7 @@ public class JukeboxService private static class TaskQueue { - private final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); void add(JukeboxTask jukeboxTask) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java index 2797f967..a093bd5b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -1,29 +1,19 @@ package org.moire.ultrasonic.service; -import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.audiofx.AudioEffect; import android.os.Build; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; -import android.os.PowerManager; import android.util.Log; import android.view.View; import android.widget.RemoteViews; -import android.widget.SeekBar; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -32,10 +22,7 @@ import androidx.core.app.NotificationManagerCompat; import org.koin.java.standalone.KoinJavaComponent; import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.DownloadActivity; -import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.activity.SubsonicTabActivity; -import org.moire.ultrasonic.audiofx.EqualizerController; -import org.moire.ultrasonic.audiofx.VisualizerController; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.RepeatMode; @@ -45,24 +32,11 @@ import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x1; import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x2; import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x3; import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x4; -import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; -import org.moire.ultrasonic.util.CancellableTask; -import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.ShufflePlayBuffer; import org.moire.ultrasonic.util.SimpleServiceBinder; -import org.moire.ultrasonic.util.StreamProxy; import org.moire.ultrasonic.util.Util; -import java.io.File; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - import kotlin.Lazy; import static org.koin.java.standalone.KoinJavaComponent.inject; @@ -70,7 +44,6 @@ import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; import static org.moire.ultrasonic.domain.PlayerState.IDLE; import static org.moire.ultrasonic.domain.PlayerState.PAUSED; -import static org.moire.ultrasonic.domain.PlayerState.PREPARED; import static org.moire.ultrasonic.domain.PlayerState.PREPARING; import static org.moire.ultrasonic.domain.PlayerState.STARTED; import static org.moire.ultrasonic.domain.PlayerState.STOPPED; @@ -84,79 +57,18 @@ public class MediaPlayerService extends Service private static MediaPlayerService instance = null; - public static boolean equalizerAvailable; - public static boolean visualizerAvailable; - public static final List downloadList = new ArrayList(); - public static final List backgroundDownloadList = new ArrayList(); - private final IBinder binder = new SimpleServiceBinder<>(this); - private PowerManager.WakeLock wakeLock; - - private Looper mediaPlayerLooper; - private MediaPlayer mediaPlayer; - private MediaPlayer nextMediaPlayer; - private Handler mediaPlayerHandler; - private AudioManager audioManager; - - public RemoteControlClient remoteControlClient; - private EqualizerController equalizerController; - private VisualizerController visualizerController; - public static ShufflePlayBuffer shufflePlayBuffer; private final Scrobbler scrobbler = new Scrobbler(); - private CancellableTask bufferTask; - public static DownloadFile currentPlaying; - public static DownloadFile currentDownloading; - public static DownloadFile nextPlaying; public Lazy jukeboxService = inject(JukeboxService.class); private Lazy downloadQueueSerializer = inject(DownloadQueueSerializer.class); - private Lazy externalStorageMonitor = inject(ExternalStorageMonitor.class); + private Lazy shufflePlayBuffer = inject(ShufflePlayBuffer.class); + private Lazy downloader = inject(Downloader.class); + private Lazy player = inject(Player.class); - private ScheduledExecutorService executorService; - - public static int cachedPosition; - private PositionCache positionCache; - private StreamProxy proxy; - - private static boolean nextSetup; - private static CancellableTask nextPlayingTask; - public static PlayerState playerState = IDLE; - public static PlayerState nextPlayerState = IDLE; private boolean isInForeground = false; - private static final List cleanupCandidates = new ArrayList(); - public static boolean shufflePlay; - public static long revision; - private int secondaryProgress = -1; - private NotificationCompat.Builder notificationBuilder; - static - { - try - { - EqualizerController.checkAvailable(); - equalizerAvailable = true; - } - catch (Throwable t) - { - equalizerAvailable = false; - } - } - - static - { - try - { - VisualizerController.checkAvailable(); - visualizerAvailable = true; - } - catch (Throwable t) - { - visualizerAvailable = false; - } - } - - public static synchronized int size() { return downloadList.size(); } public RepeatMode getRepeatMode() { return Util.getRepeatMode(this); } public static MediaPlayerService getInstance(Context context) @@ -197,98 +109,26 @@ public class MediaPlayerService extends Service { super.onCreate(); - new Thread(new Runnable() - { + downloader.getValue().onCreate(); + shufflePlayBuffer.getValue().onCreate(); + + player.getValue().onCreate(); + setupOnCurrentPlayingChangedHandler(); + setupOnPlayerStateChangedHandler(); + setupOnSongCompletedHandler(); + player.getValue().onPrepared = new Runnable() { @Override - public void run() - { - Thread.currentThread().setName("MediaPlayerService"); - - Looper.prepare(); - - if (mediaPlayer != null) - { - mediaPlayer.release(); - } - - mediaPlayer = new MediaPlayer(); - mediaPlayer.setWakeMode(MediaPlayerService.this, PowerManager.PARTIAL_WAKE_LOCK); - - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int more) - { - handleError(new Exception(String.format("MediaPlayer error: %d (%d)", what, more))); - return false; - } - }); - - try - { - Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(i); - } - catch (Throwable e) - { - // Froyo or lower - } - - mediaPlayerLooper = Looper.myLooper(); - mediaPlayerHandler = new Handler(mediaPlayerLooper); - Looper.loop(); - } - }).start(); - - Runnable downloadChecker = new Runnable() - { - @Override - public void run() - { - try - { - MediaPlayerService.checkDownloads(MediaPlayerService.this); - } - catch (Throwable x) - { - Log.e(TAG, "checkDownloads() failed.", x); - } + public void run() { + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.getValue().downloadList, + downloader.getValue().getCurrentPlayingIndex(), getPlayerPosition()); } }; - - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); - - audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - setUpRemoteControlClient(); - - if (equalizerAvailable) - { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) - { - equalizerController = null; + player.getValue().onNextSongRequested = new Runnable() { + @Override + public void run() { + setNextPlaying(); } - else - { - equalizerController.loadSettings(); - } - } - - if (visualizerAvailable) - { - visualizerController = new VisualizerController(mediaPlayer); - if (!visualizerController.isAvailable()) - { - visualizerController = null; - } - } - - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); - wakeLock.setReferenceCounted(false); + }; // Create Notification Channel if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -303,7 +143,7 @@ public class MediaPlayerService extends Service // We should use a single notification builder, otherwise the notification may not be updated notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); // Update notification early. It is better to show an empty one temporarily than waiting too long and letting Android kill the app - updateNotification(); + updateNotification(IDLE, null); instance = this; Log.i(TAG, "MediaPlayerService created"); @@ -323,50 +163,11 @@ public class MediaPlayerService extends Service instance = null; - reset(); - executorService.shutdown(); - try { - mediaPlayer.release(); - - if (nextMediaPlayer != null) - { - nextMediaPlayer.release(); - } - - mediaPlayerLooper.quit(); - shufflePlayBuffer.shutdown(); - - if (equalizerController != null) - { - equalizerController.release(); - } - - if (visualizerController != null) - { - visualizerController.release(); - } - - if (bufferTask != null) - { - bufferTask.cancel(); - } - - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - } - - Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); - i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); - i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - sendBroadcast(i); - - audioManager.unregisterRemoteControlClient(remoteControlClient); - clearRemoteControl(); - - wakeLock.release(); + player.getValue().onDestroy(); + shufflePlayBuffer.getValue().onDestroy(); + downloader.getValue().onDestroy(); } catch (Throwable ignored) { @@ -375,232 +176,39 @@ public class MediaPlayerService extends Service Log.i(TAG, "MediaPlayerService stopped"); } - public EqualizerController getEqualizerController() + public synchronized void seekTo(int position) { - if (equalizerAvailable && equalizerController == null) + if (jukeboxService.getValue().isEnabled()) { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) - { - equalizerController = null; - } - else - { - equalizerController.loadSettings(); - } - } - return equalizerController; - } - - public VisualizerController getVisualizerController() - { - if (visualizerAvailable && visualizerController == null) - { - visualizerController = new VisualizerController(mediaPlayer); - if (!visualizerController.isAvailable()) - { - visualizerController = null; - } - } - return visualizerController; - } - - public void setUpRemoteControlClient() - { - if (!Util.isLockScreenEnabled(this)) return; - - ComponentName componentName = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); - - if (remoteControlClient == null) - { - final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(componentName); - PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); - remoteControlClient = new RemoteControlClient(broadcast); - audioManager.registerRemoteControlClient(remoteControlClient); - - // Flags for the media transport control that this client supports. - int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | - RemoteControlClient.FLAG_KEY_MEDIA_NEXT | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY | - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_STOP; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - { - flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; - - remoteControlClient.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() - { - @Override - public long onGetPlaybackPosition() - { - return mediaPlayer.getCurrentPosition(); - } - }); - - remoteControlClient.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() - { - @Override - public void onPlaybackPositionUpdate(long newPositionMs) - { - seekTo((int) newPositionMs); - } - }); - } - - remoteControlClient.setTransportControlFlags(flags); - } - } - - private void clearRemoteControl() - { - if (remoteControlClient != null) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - audioManager.unregisterRemoteControlClient(remoteControlClient); - remoteControlClient = null; - } - } - - private void updateRemoteControl() - { - if (!Util.isLockScreenEnabled(this)) - { - clearRemoteControl(); - return; - } - - if (remoteControlClient != null) - { - audioManager.unregisterRemoteControlClient(remoteControlClient); - audioManager.registerRemoteControlClient(remoteControlClient); + jukeboxService.getValue().skip(downloader.getValue().getCurrentPlayingIndex(), position / 1000); } else { - setUpRemoteControlClient(); - } - - Log.i(TAG, String.format("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition())); - - switch (playerState) - { - case STARTED: - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - } - else - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, getPlayerPosition(), 1.0f); - } - break; - default: - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - } - else - { - remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, getPlayerPosition(), 1.0f); - } - break; - } - - if (currentPlaying != null) - { - MusicDirectory.Entry currentSong = currentPlaying.getSong(); - - Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(this, currentSong, Util.getMinDisplayMetric(this), true); - - String artist = currentSong.getArtist(); - String album = currentSong.getAlbum(); - String title = currentSong.getTitle(); - Integer currentSongDuration = currentSong.getDuration(); - Long duration = 0L; - - if (currentSongDuration != null) duration = (long) currentSongDuration * 1000; - - remoteControlClient.editMetadata(true).putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist).putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist).putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album).putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title).putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap).apply(); - } - } - - public synchronized void seekTo(int position) - { - try - { - if (jukeboxService.getValue().isEnabled()) - { - jukeboxService.getValue().skip(getCurrentPlayingIndex(), position / 1000); - } - else - { - mediaPlayer.seekTo(position); - cachedPosition = position; - - updateRemoteControl(); - } - } - catch (Exception x) - { - handleError(x); + player.getValue().seekTo(position); } } public synchronized int getPlayerPosition() { - try + if (player.getValue().playerState == IDLE || player.getValue().playerState == DOWNLOADING || player.getValue().playerState == PREPARING) { - if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) - { - return 0; - } - - return jukeboxService.getValue().isEnabled() ? jukeboxService.getValue().getPositionSeconds() * 1000 : cachedPosition; - } - catch (Exception x) - { - handleError(x); return 0; } + + return jukeboxService.getValue().isEnabled() ? jukeboxService.getValue().getPositionSeconds() * 1000 : + player.getValue().getPlayerPosition(); } public synchronized int getPlayerDuration() { - if (MediaPlayerService.currentPlaying != null) - { - Integer duration = MediaPlayerService.currentPlaying.getSong().getDuration(); - if (duration != null) - { - return duration * 1000; - } - } - if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) - { - try - { - return mediaPlayer.getDuration(); - } - catch (Exception x) - { - handleError(x); - } - } - return 0; - } - - public static synchronized int getCurrentPlayingIndex() - { - return downloadList.indexOf(currentPlaying); + return player.getValue().getPlayerDuration(); } public synchronized void setCurrentPlaying(int currentPlayingIndex) { try { - setCurrentPlaying(downloadList.get(currentPlayingIndex)); + player.getValue().setCurrentPlaying(downloader.getValue().downloadList.get(currentPlayingIndex)); } catch (IndexOutOfBoundsException x) { @@ -608,47 +216,53 @@ public class MediaPlayerService extends Service } } - public synchronized void setCurrentPlaying(DownloadFile currentPlaying) + public void setupOnCurrentPlayingChangedHandler() { - MediaPlayerService.currentPlaying = currentPlaying; + player.getValue().onCurrentPlayingChanged = new Consumer() { + @Override + public void accept(DownloadFile currentPlaying) { + if (currentPlaying != null) + { + Util.broadcastNewTrackInfo(MediaPlayerService.this, currentPlaying.getSong()); + Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), currentPlaying, + downloader.getValue().getDownloads().size(), downloader.getValue().getCurrentPlayingIndex() + 1); + } + else + { + Util.broadcastNewTrackInfo(MediaPlayerService.this, null); + Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), null, + downloader.getValue().getDownloads().size(), downloader.getValue().getCurrentPlayingIndex() + 1); + } - if (currentPlaying != null) - { - Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - Util.broadcastA2dpPlayStatusChange(this, playerState, currentPlaying.getSong(), downloadList.size() + backgroundDownloadList.size(), downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); - } - else - { - Util.broadcastNewTrackInfo(this, null); - Util.broadcastA2dpMetaDataChange(this, null); - } + // Update widget + PlayerState playerState = player.getValue().playerState; + MusicDirectory.Entry song = currentPlaying == null? null : currentPlaying.getSong(); + UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true); + UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); - // Update widget - UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, true); - UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); + SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); - updateRemoteControl(); - SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); - - if (currentPlaying != null) - { - if (tabInstance != null) { - updateNotification(); - tabInstance.showNowPlaying(); + if (currentPlaying != null) + { + if (tabInstance != null) { + updateNotification(player.getValue().playerState, currentPlaying); + tabInstance.showNowPlaying(); + } + } + else + { + if (tabInstance != null) + { + tabInstance.hideNowPlaying(); + stopForeground(true); + isInForeground = false; + stopSelf(); + } + } } - } - else - { - if (tabInstance != null) - { - tabInstance.hideNowPlaying(); - stopForeground(true); - isInForeground = false; - stopSelf(); - } - } + }; } public synchronized void setNextPlaying() @@ -657,12 +271,11 @@ public class MediaPlayerService extends Service if (!gaplessPlayback) { - nextPlaying = null; - nextPlayerState = IDLE; + player.getValue().setNextPlaying(null); return; } - int index = getCurrentPlayingIndex(); + int index = downloader.getValue().getCurrentPlayingIndex(); if (index != -1) { @@ -672,7 +285,7 @@ public class MediaPlayerService extends Service index += 1; break; case ALL: - index = (index + 1) % size(); + index = (index + 1) % downloader.getValue().downloadList.size(); break; case SINGLE: break; @@ -681,56 +294,40 @@ public class MediaPlayerService extends Service } } - nextSetup = false; - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } + player.getValue().clearNextPlaying(); - if (index < size() && index != -1) + if (index < downloader.getValue().downloadList.size() && index != -1) { - nextPlaying = downloadList.get(index); - nextPlayingTask = new CheckCompletionTask(nextPlaying); - nextPlayingTask.start(); + player.getValue().setNextPlaying(downloader.getValue().downloadList.get(index)); } else { - nextPlaying = null; - setNextPlayerState(IDLE); + player.getValue().setNextPlaying(null); } } public synchronized void togglePlayPause() { - if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) + if (player.getValue().playerState == PAUSED || player.getValue().playerState == COMPLETED || player.getValue().playerState == STOPPED) { start(); } - else if (playerState == IDLE) + else if (player.getValue().playerState == IDLE) { play(); } - else if (playerState == STARTED) + else if (player.getValue().playerState == STARTED) { pause(); } } - public void setVolume(float volume) - { - if (mediaPlayer != null) - { - mediaPlayer.setVolume(volume, volume); - } - } - /** * Plays either the current song (resume) or the first/next one in queue. */ public synchronized void play() { - int current = getCurrentPlayingIndex(); + int current = downloader.getValue().getCurrentPlayingIndex(); if (current == -1) { play(0); @@ -748,802 +345,233 @@ public class MediaPlayerService extends Service public synchronized void play(int index, boolean start) { - updateRemoteControl(); - - if (index < 0 || index >= size()) + if (index < 0 || index >= downloader.getValue().downloadList.size()) { resetPlayback(); } else { - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - - setCurrentPlaying(index); - if (start) { if (jukeboxService.getValue().isEnabled()) { - jukeboxService.getValue().skip(getCurrentPlayingIndex(), 0); - setPlayerState(STARTED); + jukeboxService.getValue().skip(index, 0); + player.getValue().setPlayerState(STARTED); } else { - bufferAndPlay(); + player.getValue().play(downloader.getValue().downloadList.get(index)); } } - checkDownloads(this); + downloader.getValue().checkDownloads(); setNextPlaying(); } } private synchronized void resetPlayback() { - reset(); - setCurrentPlaying(null); - downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); - } - - public synchronized void reset() - { - if (bufferTask != null) - { - bufferTask.cancel(); - } - try - { - setPlayerState(IDLE); - mediaPlayer.setOnErrorListener(null); - mediaPlayer.setOnCompletionListener(null); - mediaPlayer.reset(); - } - catch (Exception x) - { - handleError(x); - } - } - - private synchronized void playNext() - { - MediaPlayer tmp = mediaPlayer; - mediaPlayer = nextMediaPlayer; - nextMediaPlayer = tmp; - setCurrentPlaying(nextPlaying); - setPlayerState(PlayerState.STARTED); - setupHandlers(currentPlaying, false); - setNextPlaying(); - - // Proxy should not be being used here since the next player was already setup to play - if (proxy != null) - { - proxy.stop(); - proxy = null; - } + player.getValue().reset(); + player.getValue().setCurrentPlaying(null); + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.getValue().downloadList, + downloader.getValue().getCurrentPlayingIndex(), getPlayerPosition()); } public synchronized void pause() { - try + if (player.getValue().playerState == STARTED) { - if (playerState == STARTED) + if (jukeboxService.getValue().isEnabled()) { - if (jukeboxService.getValue().isEnabled()) - { - jukeboxService.getValue().stop(); - } - else - { - mediaPlayer.pause(); - } - setPlayerState(PAUSED); + jukeboxService.getValue().stop(); } - } - catch (Exception x) - { - handleError(x); + else + { + player.getValue().pause(); + } + player.getValue().setPlayerState(PAUSED); } } public synchronized void stop() { - try + if (player.getValue().playerState == STARTED) { - if (playerState == STARTED) + if (jukeboxService.getValue().isEnabled()) { - if (jukeboxService.getValue().isEnabled()) - { - jukeboxService.getValue().stop(); - } - else - { - mediaPlayer.pause(); - } + jukeboxService.getValue().stop(); + } + else + { + player.getValue().pause(); } - setPlayerState(STOPPED); - } - catch (Exception x) - { - handleError(x); } + player.getValue().setPlayerState(STOPPED); } public synchronized void start() { - try + if (jukeboxService.getValue().isEnabled()) { - if (jukeboxService.getValue().isEnabled()) - { - jukeboxService.getValue().start(); - } - else - { - mediaPlayer.start(); - } - setPlayerState(STARTED); - } - catch (Exception x) - { - handleError(x); - } - } - - private synchronized void bufferAndPlay() - { - if (playerState != PREPARED) - { - reset(); - - bufferTask = new BufferTask(currentPlaying, 0); - bufferTask.start(); + jukeboxService.getValue().start(); } else { - doPlay(currentPlaying, 0, true); + player.getValue().start(); } + player.getValue().setPlayerState(STARTED); } - public synchronized void setPlayerState(PlayerState playerState) + public void setupOnPlayerStateChangedHandler() { - Log.i(TAG, String.format("%s -> %s (%s)", playerState.name(), playerState.name(), currentPlaying)); - - MediaPlayerService.playerState = playerState; - - if (playerState == PAUSED) - { - downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); - } - - if (playerState == PlayerState.STARTED) - { - Util.requestAudioFocus(this); - } - - boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(this)); - boolean show = playerState == PlayerState.STARTED || showWhenPaused; - - Util.broadcastPlaybackStatusChange(this, playerState); - Util.broadcastA2dpPlayStatusChange(this, playerState, currentPlaying.getSong(), downloadList.size() + backgroundDownloadList.size(), downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); - - if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) - { - // Set remote control - updateRemoteControl(); - } - - // Update widget - UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, true); - UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, currentPlaying.getSong(), playerState == PlayerState.STARTED, false); - SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); - - if (show) - { - if (tabInstance != null) - { - // Only update notification is player state is one that will change the icon - if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) - { - updateNotification(); - tabInstance.showNowPlaying(); - } - } - } - else - { - if (tabInstance != null) - { - stopForeground(true); - isInForeground = false; - tabInstance.hideNowPlaying(); - stopSelf(); - } - } - - if (playerState == STARTED) - { - scrobbler.scrobble(this, currentPlaying, false); - } - else if (playerState == COMPLETED) - { - scrobbler.scrobble(this, currentPlaying, true); - } - - if (playerState == STARTED && positionCache == null) - { - positionCache = new PositionCache(); - Thread thread = new Thread(positionCache); - thread.start(); - } - else if (playerState != STARTED && positionCache != null) - { - positionCache.stop(); - positionCache = null; - } - } - - private void setPlayerStateCompleted() - { - Log.i(TAG, String.format("%s -> %s (%s)", playerState.name(), PlayerState.COMPLETED, currentPlaying)); - playerState = PlayerState.COMPLETED; - - if (positionCache != null) - { - positionCache.stop(); - positionCache = null; - } - - scrobbler.scrobble(this, currentPlaying, true); - } - - private static synchronized void setNextPlayerState(PlayerState playerState) - { - Log.i(TAG, String.format("Next: %s -> %s (%s)", nextPlayerState.name(), playerState.name(), nextPlaying)); - nextPlayerState = playerState; - } - - public synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) - { - try - { - downloadFile.setPlaying(false); - //downloadFile.setPlaying(true); - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - boolean partial = file.equals(downloadFile.getPartialFile()); - downloadFile.updateModificationDate(); - - mediaPlayer.setOnCompletionListener(null); - secondaryProgress = -1; // Ensure seeking in non StreamProxy playback works - mediaPlayer.reset(); - setPlayerState(IDLE); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - String dataSource = file.getPath(); - - if (partial) - { - if (proxy == null) - { - proxy = new StreamProxy(new Supplier() { - @Override - public DownloadFile get() { return currentPlaying; } - }); - proxy.start(); - } - - dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); - Log.i(TAG, String.format("Data Source: %s", dataSource)); - } - else if (proxy != null) - { - proxy.stop(); - proxy = null; - } - - Log.i(TAG, "Preparing media player"); - mediaPlayer.setDataSource(dataSource); - setPlayerState(PREPARING); - - mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() - { - @Override - public void onBufferingUpdate(MediaPlayer mp, int percent) - { - SeekBar progressBar = DownloadActivity.getProgressBar(); - MusicDirectory.Entry song = downloadFile.getSong(); - - if (percent == 100) - { - if (progressBar != null) - { - progressBar.setSecondaryProgress(100 * progressBar.getMax()); - } - - mp.setOnBufferingUpdateListener(null); - } - else if (progressBar != null && song.getTranscodedContentType() == null && Util.getMaxBitRate(MediaPlayerService.this) == 0) - { - secondaryProgress = (int) (((double) percent / (double) 100) * progressBar.getMax()); - progressBar.setSecondaryProgress(secondaryProgress); - } - } - }); - - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() - { - @Override - public void onPrepared(MediaPlayer mp) - { - Log.i(TAG, "Media player prepared"); - - setPlayerState(PREPARED); - - SeekBar progressBar = DownloadActivity.getProgressBar(); - - if (progressBar != null && downloadFile.isWorkDone()) - { - // Populate seek bar secondary progress if we have a complete file for consistency - DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax()); - } - - synchronized (MediaPlayerService.this) - { - if (position != 0) - { - Log.i(TAG, String.format("Restarting player from position %d", position)); - seekTo(position); - } - cachedPosition = position; - - if (start) - { - mediaPlayer.start(); - setPlayerState(STARTED); - } - else - { - setPlayerState(PAUSED); - } - } - - downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); - } - }); - - setupHandlers(downloadFile, partial); - - mediaPlayer.prepareAsync(); - } - catch (Exception x) - { - handleError(x); - } - } - - private synchronized void setupNext(final DownloadFile downloadFile) - { - try - { - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - - if (nextMediaPlayer != null) - { - nextMediaPlayer.setOnCompletionListener(null); - nextMediaPlayer.release(); - nextMediaPlayer = null; - } - - nextMediaPlayer = new MediaPlayer(); - nextMediaPlayer.setWakeMode(MediaPlayerService.this, PowerManager.PARTIAL_WAKE_LOCK); - - try - { - nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); - } - catch (Throwable e) - { - nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - } - - nextMediaPlayer.setDataSource(file.getPath()); - setNextPlayerState(PREPARING); - - nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() - { - @Override - @SuppressLint("NewApi") - public void onPrepared(MediaPlayer mp) - { - try - { - setNextPlayerState(PREPARED); - - if (Util.getGaplessPlaybackPreference(MediaPlayerService.this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) - { - mediaPlayer.setNextMediaPlayer(nextMediaPlayer); - nextSetup = true; - } - } - catch (Exception x) - { - handleErrorNext(x); - } - } - }); - - nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int extra) - { - Log.w(TAG, String.format("Error on playing next (%d, %d): %s", what, extra, downloadFile)); - return true; - } - }); - - nextMediaPlayer.prepareAsync(); - } - catch (Exception x) - { - handleErrorNext(x); - } - } - - private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) - { - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() - { + player.getValue().onPlayerStateChanged = new BiConsumer() { @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int extra) - { - Log.w(TAG, String.format("Error on playing file (%d, %d): %s", what, extra, downloadFile)); - int pos = cachedPosition; - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - return true; + public void accept(PlayerState playerState, DownloadFile currentPlaying) { + if (playerState == PAUSED) + { + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.getValue().downloadList, downloader.getValue().getCurrentPlayingIndex(), getPlayerPosition()); + } + + boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(MediaPlayerService.this)); + boolean show = playerState == PlayerState.STARTED || showWhenPaused; + + Util.broadcastPlaybackStatusChange(MediaPlayerService.this, playerState); + Util.broadcastA2dpPlayStatusChange(MediaPlayerService.this, playerState, currentPlaying.getSong(), + downloader.getValue().downloadList.size() + downloader.getValue().backgroundDownloadList.size(), + downloader.getValue().downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); + + MusicDirectory.Entry song = currentPlaying.getSong(); + // Update widget + UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true); + UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false); + SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); + + if (show) + { + if (tabInstance != null) + { + // Only update notification is player state is one that will change the icon + if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) + { + updateNotification(playerState, currentPlaying); + tabInstance.showNowPlaying(); + } + } + } + else + { + if (tabInstance != null) + { + stopForeground(true); + isInForeground = false; + tabInstance.hideNowPlaying(); + stopSelf(); + } + } + + if (playerState == STARTED) + { + scrobbler.scrobble(MediaPlayerService.this, currentPlaying, false); + } + else if (playerState == COMPLETED) + { + scrobbler.scrobble(MediaPlayerService.this, currentPlaying, true); + } } - }); + }; + } - final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; - - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() - { + private void setupOnSongCompletedHandler() + { + player.getValue().onSongCompleted = new Consumer() { @Override - public void onCompletion(MediaPlayer mediaPlayer) - { - // Acquire a temporary wakelock, since when we return from - // this callback the MediaPlayer will release its wakelock - // and allow the device to go to sleep. - wakeLock.acquire(60000); + public void accept(DownloadFile currentPlaying) { + int index = downloader.getValue().getCurrentPlayingIndex(); - int pos = cachedPosition; - Log.i(TAG, String.format("Ending position %d of %d", pos, duration)); - - if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 1000))) + if (currentPlaying != null) { - setPlayerStateCompleted(); + final MusicDirectory.Entry song = currentPlaying.getSong(); - if (Util.getGaplessPlaybackPreference(MediaPlayerService.this) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) + if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) { - if (!nextSetup) + MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this); + try { - playNext(); + musicService.deleteBookmark(song.getId(), MediaPlayerService.this, null); } - else + catch (Exception ignored) { - nextSetup = false; - playNext(); + } } - else - { - onSongCompleted(); - } - - return; } - synchronized (this) + if (index != -1) { - if (downloadFile.isWorkDone()) + switch (getRepeatMode()) { - // Complete was called early even though file is fully buffered - Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - } - else - { - Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); - reset(); - bufferTask = new BufferTask(downloadFile, pos); - bufferTask.start(); - } - } - } - }); - } - - private void onSongCompleted() - { - int index = getCurrentPlayingIndex(); - - if (currentPlaying != null) - { - final MusicDirectory.Entry song = currentPlaying.getSong(); - - if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(this)) - { - MusicService musicService = MusicServiceFactory.getMusicService(this); - try - { - musicService.deleteBookmark(song.getId(), this, null); - } - catch (Exception ignored) - { - - } - } - } - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - if (index + 1 < 0 || index + 1 >= size()) - { - if (Util.getShouldClearPlaylist(this)) - { - clear(true); - jukeboxService.getValue().updatePlaylist(); - downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); - } - - resetPlayback(); - break; - } - - play(index + 1); - break; - case ALL: - play((index + 1) % size()); - break; - case SINGLE: - play(index); - break; - default: - break; - } - } - } - - // TODO: Serialization was originally here, removed for static. refactor this and but back - // downloadQueueSerializer.getValue().serializeDownloadQueue(downloadList, getCurrentPlayingIndex(), getPlayerPosition()); - public static synchronized void clear(boolean serialize) - { - MediaPlayerService mediaPlayerService = getRunningInstance(); - - if (mediaPlayerService != null) mediaPlayerService.reset(); - downloadList.clear(); - revision++; - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - if (mediaPlayerService != null) - { - mediaPlayerService.setCurrentPlaying(null); - mediaPlayerService.setNextPlaying(); - } - } - - protected static synchronized void checkDownloads(Context context) - { - // TODO: refactor inject - if (!Util.isExternalStoragePresent() || !inject(ExternalStorageMonitor.class).getValue().isExternalStorageAvailable()) - { - return; - } - - if (shufflePlay) - { - checkShufflePlay(context); - } - - // TODO: This inject is ugly, refactor - if (inject(JukeboxService.class).getValue().isEnabled() || !Util.isNetworkConnected(context)) - { - return; - } - - if (MediaPlayerService.downloadList.isEmpty() && MediaPlayerService.backgroundDownloadList.isEmpty()) - { - return; - } - - // Need to download current playing? - if (MediaPlayerService.currentPlaying != null && MediaPlayerService.currentPlaying != MediaPlayerService.currentDownloading && !MediaPlayerService.currentPlaying.isWorkDone()) - { - // Cancel current download, if necessary. - if (MediaPlayerService.currentDownloading != null) - { - MediaPlayerService.currentDownloading.cancelDownload(); - } - - MediaPlayerService.currentDownloading = MediaPlayerService.currentPlaying; - MediaPlayerService.currentDownloading.download(); - cleanupCandidates.add(MediaPlayerService.currentDownloading); - } - - // Find a suitable target for download. - else - { - if (MediaPlayerService.currentDownloading == null || - MediaPlayerService.currentDownloading.isWorkDone() || - MediaPlayerService.currentDownloading.isFailed() && - (!MediaPlayerService.downloadList.isEmpty() || !MediaPlayerService.backgroundDownloadList.isEmpty())) - { - MediaPlayerService.currentDownloading = null; - int n = size(); - - int preloaded = 0; - - if (n != 0) - { - int start = MediaPlayerService.currentPlaying == null ? 0 : getCurrentPlayingIndex(); - if (start == -1) - { - start = 0; - } - int i = start; - do - { - DownloadFile downloadFile = MediaPlayerService.downloadList.get(i); - if (!downloadFile.isWorkDone()) - { - if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(context)) + case OFF: + if (index + 1 < 0 || index + 1 >= downloader.getValue().downloadList.size()) { - MediaPlayerService.currentDownloading = downloadFile; - MediaPlayerService.currentDownloading.download(); - cleanupCandidates.add(MediaPlayerService.currentDownloading); - if (i == (start + 1)) + if (Util.getShouldClearPlaylist(MediaPlayerService.this)) { - setNextPlayerState(DOWNLOADING); + clear(true); + jukeboxService.getValue().updatePlaylist(); } + + resetPlayback(); break; } - } - else if (MediaPlayerService.currentPlaying != downloadFile) - { - preloaded++; - } - i = (i + 1) % n; - } while (i != start); - } - - if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(context) || MediaPlayerService.downloadList.isEmpty()) && !MediaPlayerService.backgroundDownloadList.isEmpty()) - { - for (int i = 0; i < MediaPlayerService.backgroundDownloadList.size(); i++) - { - DownloadFile downloadFile = MediaPlayerService.backgroundDownloadList.get(i); - if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) - { - if (Util.getShouldScanMedia(context)) - { - Util.scanMedia(context, downloadFile.getCompleteFile()); - } - - // Don't need to keep list like active song list - MediaPlayerService.backgroundDownloadList.remove(i); - revision++; - i--; - } - else - { - MediaPlayerService.currentDownloading = downloadFile; - MediaPlayerService.currentDownloading.download(); - cleanupCandidates.add(MediaPlayerService.currentDownloading); + play(index + 1); + break; + case ALL: + play((index + 1) % downloader.getValue().downloadList.size()); + break; + case SINGLE: + play(index); + break; + default: break; - } } } } - } - - // Delete obsolete .partial and .complete files. - cleanup(context); + }; } - private static synchronized void checkShufflePlay(Context context) + public synchronized void clear(boolean serialize) { - // Get users desired random playlist size - int listSize = Util.getMaxSongs(context); - boolean wasEmpty = MediaPlayerService.downloadList.isEmpty(); + player.getValue().reset(); + downloader.getValue().clear(); + player.getValue().setCurrentPlaying(null); - long revisionBefore = revision; + setNextPlaying(); - // First, ensure that list is at least 20 songs long. - int size = size(); - if (size < listSize) - { - for (MusicDirectory.Entry song : MediaPlayerService.shufflePlayBuffer.get(listSize - size)) - { - DownloadFile downloadFile = new DownloadFile(context, song, false); - MediaPlayerService.downloadList.add(downloadFile); - revision++; - } - } - - int currIndex = MediaPlayerService.currentPlaying == null ? 0 : getCurrentPlayingIndex(); - - // Only shift playlist if playing song #5 or later. - if (currIndex > 4) - { - int songsToShift = currIndex - 2; - for (MusicDirectory.Entry song : MediaPlayerService.shufflePlayBuffer.get(songsToShift)) - { - MediaPlayerService.downloadList.add(new DownloadFile(context, song, false)); - MediaPlayerService.downloadList.get(0).cancelDownload(); - MediaPlayerService.downloadList.remove(0); - revision++; - } - } - - if (revisionBefore != revision) - { - getInstance(context).jukeboxService.getValue().updatePlaylist(); - } - - if (wasEmpty && !MediaPlayerService.downloadList.isEmpty()) - { - getInstance(context).play(0); + if (serialize) { + downloadQueueSerializer.getValue().serializeDownloadQueue(downloader.getValue().downloadList, + downloader.getValue().getCurrentPlayingIndex(), getPlayerPosition()); } } - private static synchronized void cleanup(Context context) - { - Iterator iterator = cleanupCandidates.iterator(); - while (iterator.hasNext()) - { - DownloadFile downloadFile = iterator.next(); - if (downloadFile != MediaPlayerService.currentPlaying && downloadFile != MediaPlayerService.currentDownloading) - { - if (downloadFile.cleanup()) - { - iterator.remove(); - } - } - } - } - - public void updateNotification() + public void updateNotification(PlayerState playerState, DownloadFile currentPlaying) { if (Util.isNotificationEnabled(this)) { if (isInForeground == true) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification()); + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); } else { final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification()); + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); } Log.w(TAG, "--- Updated notification"); } else { - startForeground(NOTIFICATION_ID, buildForegroundNotification()); + startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); isInForeground = true; Log.w(TAG, "--- Created Foreground notification"); } @@ -1551,7 +579,7 @@ public class MediaPlayerService extends Service } @SuppressWarnings("IconColors") - private Notification buildForegroundNotification() { + private Notification buildForegroundNotification(PlayerState playerState, DownloadFile currentPlaying) { notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); notificationBuilder.setAutoCancel(false); @@ -1629,186 +657,4 @@ public class MediaPlayerService extends Service return notification; } - - private class PositionCache implements Runnable - { - boolean isRunning = true; - - public void stop() - { - isRunning = false; - } - - @Override - public void run() - { - Thread.currentThread().setName("PositionCache"); - - // Stop checking position before the song reaches completion - while (isRunning) - { - try - { - if (mediaPlayer != null && playerState == STARTED) - { - cachedPosition = mediaPlayer.getCurrentPosition(); - } - - Util.sleepQuietly(25L); - } - catch (Exception e) - { - Log.w(TAG, "Crashed getting current position", e); - isRunning = false; - positionCache = null; - } - } - } - } - - private void handleError(Exception x) - { - Log.w(TAG, String.format("Media player error: %s", x), x); - - try - { - mediaPlayer.reset(); - } - catch (Exception ex) - { - Log.w(TAG, String.format("Exception encountered when resetting media player: %s", ex), ex); - } - } - - private void handleErrorNext(Exception x) - { - Log.w(TAG, String.format("Next Media player error: %s", x), x); - nextMediaPlayer.reset(); - } - - private class BufferTask extends CancellableTask - { - private final DownloadFile downloadFile; - private final int position; - private final long expectedFileSize; - private final File partialFile; - - public BufferTask(DownloadFile downloadFile, int position) - { - this.downloadFile = downloadFile; - this.position = position; - partialFile = downloadFile.getPartialFile(); - - long bufferLength = Util.getBufferLength(MediaPlayerService.this); - - if (bufferLength == 0) - { - // Set to seconds in a day, basically infinity - bufferLength = 86400L; - } - - // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. - int bitRate = downloadFile.getBitRate(); - long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); - - // Find out how large the file should grow before resuming playback. - Log.i(TAG, String.format("Buffering from position %d and bitrate %d", position, bitRate)); - expectedFileSize = (position * bitRate / 8) + byteCount; - } - - @Override - public void execute() - { - setPlayerState(DOWNLOADING); - - while (!bufferComplete() && !Util.isOffline(MediaPlayerService.this)) - { - Util.sleepQuietly(1000L); - if (isCancelled()) - { - return; - } - } - doPlay(downloadFile, position, true); - } - - private boolean bufferComplete() - { - boolean completeFileAvailable = downloadFile.isWorkDone(); - long size = partialFile.length(); - - Log.i(TAG, String.format("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable)); - return completeFileAvailable || size >= expectedFileSize; - } - - @Override - public String toString() - { - return String.format("BufferTask (%s)", downloadFile); - } - } - - private class CheckCompletionTask extends CancellableTask - { - private final DownloadFile downloadFile; - private final File partialFile; - - public CheckCompletionTask(DownloadFile downloadFile) - { - super(); - setNextPlayerState(PlayerState.IDLE); - - this.downloadFile = downloadFile; - - partialFile = downloadFile != null ? downloadFile.getPartialFile() : null; - } - - @Override - public void execute() - { - Thread.currentThread().setName("CheckCompletionTask"); - - if (downloadFile == null) - { - return; - } - - // Do an initial sleep so this prepare can't compete with main prepare - Util.sleepQuietly(5000L); - - while (!bufferComplete()) - { - Util.sleepQuietly(5000L); - - if (isCancelled()) - { - return; - } - } - - // Start the setup of the next media player - mediaPlayerHandler.post(new Runnable() - { - @Override - public void run() - { - setupNext(downloadFile); - } - }); - } - - private boolean bufferComplete() - { - boolean completeFileAvailable = downloadFile.isWorkDone(); - Log.i(TAG, String.format("Buffering next %s (%d)", partialFile, partialFile.length())); - return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); - } - - @Override - public String toString() - { - return String.format("CheckCompletionTask (%s)", downloadFile); - } - - } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Player.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Player.java new file mode 100644 index 00000000..6922dc91 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Player.java @@ -0,0 +1,1070 @@ +package org.moire.ultrasonic.service; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.audiofx.AudioEffect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.util.Log; +import android.widget.SeekBar; + +import org.moire.ultrasonic.activity.DownloadActivity; +import org.moire.ultrasonic.audiofx.EqualizerController; +import org.moire.ultrasonic.audiofx.VisualizerController; +import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; +import org.moire.ultrasonic.util.CancellableTask; +import org.moire.ultrasonic.util.Constants; +import org.moire.ultrasonic.util.FileUtil; +import org.moire.ultrasonic.util.StreamProxy; +import org.moire.ultrasonic.util.Util; + +import java.io.File; +import java.net.URLEncoder; + +import static org.moire.ultrasonic.domain.PlayerState.COMPLETED; +import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING; +import static org.moire.ultrasonic.domain.PlayerState.IDLE; +import static org.moire.ultrasonic.domain.PlayerState.PAUSED; +import static org.moire.ultrasonic.domain.PlayerState.PREPARED; +import static org.moire.ultrasonic.domain.PlayerState.PREPARING; +import static org.moire.ultrasonic.domain.PlayerState.STARTED; + +public class Player +{ + private static final String TAG = Player.class.getSimpleName(); + private final Context context; + + private PowerManager.WakeLock wakeLock; + public DownloadFile currentPlaying; + public DownloadFile nextPlaying; + private static boolean nextSetup; + private static CancellableTask nextPlayingTask; + private MediaPlayer mediaPlayer; + + private MediaPlayer nextMediaPlayer; + private Looper mediaPlayerLooper; + private Handler mediaPlayerHandler; + public static int cachedPosition; + private StreamProxy proxy; + + public PlayerState playerState = IDLE; + public PlayerState nextPlayerState = IDLE; + + private AudioManager audioManager; + public RemoteControlClient remoteControlClient; + + public static boolean equalizerAvailable; + public static boolean visualizerAvailable; + private EqualizerController equalizerController; + private VisualizerController visualizerController; + private CancellableTask bufferTask; + private PositionCache positionCache; + private int secondaryProgress = -1; + + public Consumer onCurrentPlayingChanged; + public Consumer onSongCompleted; + public BiConsumer onPlayerStateChanged; + public Runnable onPrepared; + public Runnable onNextSongRequested; + + static + { + try + { + EqualizerController.checkAvailable(); + equalizerAvailable = true; + } + catch (Throwable t) + { + equalizerAvailable = false; + } + } + + static + { + try + { + VisualizerController.checkAvailable(); + visualizerAvailable = true; + } + catch (Throwable t) + { + visualizerAvailable = false; + } + } + + public Player(Context context) + { + this.context = context; + } + + public void onCreate() + { + new Thread(new Runnable() + { + @Override + public void run() + { + Thread.currentThread().setName("MediaPlayerService"); + + Looper.prepare(); + + if (mediaPlayer != null) + { + mediaPlayer.release(); + } + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() + { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) + { + handleError(new Exception(String.format("MediaPlayer error: %d (%d)", what, more))); + return false; + } + }); + + try + { + Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); + context.sendBroadcast(i); + } + catch (Throwable e) + { + // Froyo or lower + } + + mediaPlayerLooper = Looper.myLooper(); + mediaPlayerHandler = new Handler(mediaPlayerLooper); + Looper.loop(); + } + }).start(); + + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + setUpRemoteControlClient(); + + if (equalizerAvailable) + { + equalizerController = new EqualizerController(context, mediaPlayer); + if (!equalizerController.isAvailable()) + { + equalizerController = null; + } + else + { + equalizerController.loadSettings(); + } + } + + if (visualizerAvailable) + { + visualizerController = new VisualizerController(mediaPlayer); + if (!visualizerController.isAvailable()) + { + visualizerController = null; + } + } + + Log.i(TAG, "Player created"); + } + + public void onDestroy() + { + reset(); + + try + { + mediaPlayer.release(); + if (nextMediaPlayer != null) + { + nextMediaPlayer.release(); + } + + mediaPlayerLooper.quit(); + + if (equalizerController != null) + { + equalizerController.release(); + } + + if (visualizerController != null) + { + visualizerController.release(); + } + + if (bufferTask != null) + { + bufferTask.cancel(); + } + + if (nextPlayingTask != null) + { + nextPlayingTask.cancel(); + } + + Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); + context.sendBroadcast(i); + + audioManager.unregisterRemoteControlClient(remoteControlClient); + clearRemoteControl(); + wakeLock.release(); + } + catch (Throwable ignored) + { + } + + Log.i(TAG, "Player destroyed"); + } + + public EqualizerController getEqualizerController() + { + if (equalizerAvailable && equalizerController == null) + { + equalizerController = new EqualizerController(context, mediaPlayer); + if (!equalizerController.isAvailable()) + { + equalizerController = null; + } + else + { + equalizerController.loadSettings(); + } + } + return equalizerController; + } + + public VisualizerController getVisualizerController() + { + if (visualizerAvailable && visualizerController == null) + { + visualizerController = new VisualizerController(mediaPlayer); + if (!visualizerController.isAvailable()) + { + visualizerController = null; + } + } + return visualizerController; + } + + public synchronized void setPlayerState(PlayerState playerState) + { + Log.i(TAG, String.format("%s -> %s (%s)", playerState.name(), playerState.name(), currentPlaying)); + + this.playerState = playerState; + + if (playerState == PlayerState.STARTED) + { + Util.requestAudioFocus(context); + } + + if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) + { + updateRemoteControl(); + } + + onPlayerStateChanged.accept(playerState, currentPlaying); + + if (playerState == STARTED && positionCache == null) + { + positionCache = new PositionCache(); + Thread thread = new Thread(positionCache); + thread.start(); + } + else if (playerState != STARTED && positionCache != null) + { + positionCache.stop(); + positionCache = null; + } + } + + public synchronized void setCurrentPlaying(DownloadFile currentPlaying) + { + this.currentPlaying = currentPlaying; + updateRemoteControl(); + onCurrentPlayingChanged.accept(currentPlaying); + } + + public synchronized void setNextPlaying(DownloadFile nextToPlay) + { + if (nextToPlay == null) + { + nextPlaying = null; + setNextPlayerState(IDLE); + return; + } + + nextPlaying = nextToPlay; + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.start(); + } + + public synchronized void clearNextPlaying() + { + nextSetup = false; + nextPlaying = null; + if (nextPlayingTask != null) + { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + } + + public synchronized void setNextPlayerState(PlayerState playerState) + { + Log.i(TAG, String.format("Next: %s -> %s (%s)", nextPlayerState.name(), playerState.name(), nextPlaying)); + nextPlayerState = playerState; + } + + public synchronized void bufferAndPlay() + { + if (playerState != PREPARED) + { + reset(); + + bufferTask = new BufferTask(currentPlaying, 0); + bufferTask.start(); + } + else + { + doPlay(currentPlaying, 0, true); + } + } + + public synchronized void play(DownloadFile fileToPlay) + { + if (nextPlayingTask != null) + { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + + updateRemoteControl(); + setCurrentPlaying(fileToPlay); + bufferAndPlay(); + } + + public synchronized void playNext() + { + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false); + onNextSongRequested.run(); + + // Proxy should not be being used here since the next player was already setup to play + if (proxy != null) + { + proxy.stop(); + proxy = null; + } + } + + public synchronized void pause() + { + try + { + mediaPlayer.pause(); + } + catch (Exception x) + { + handleError(x); + } + } + + public synchronized void start() + { + try + { + mediaPlayer.start(); + } + catch (Exception x) + { + handleError(x); + } + } + + public void updateRemoteControl() + { + if (!Util.isLockScreenEnabled(context)) + { + clearRemoteControl(); + return; + } + + if (remoteControlClient != null) + { + audioManager.unregisterRemoteControlClient(remoteControlClient); + audioManager.registerRemoteControlClient(remoteControlClient); + } + else + { + setUpRemoteControlClient(); + } + + Log.i(TAG, String.format("In updateRemoteControl, playerState: %s [%d]", playerState, getPlayerPosition())); + + switch (playerState) + { + case STARTED: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } + else + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING, getPlayerPosition(), 1.0f); + } + break; + default: + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + } + else + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED, getPlayerPosition(), 1.0f); + } + break; + } + + if (currentPlaying != null) + { + MusicDirectory.Entry currentSong = currentPlaying.getSong(); + + Bitmap lockScreenBitmap = FileUtil.getAlbumArtBitmap(context, currentSong, Util.getMinDisplayMetric(context), true); + + String artist = currentSong.getArtist(); + String album = currentSong.getAlbum(); + String title = currentSong.getTitle(); + Integer currentSongDuration = currentSong.getDuration(); + Long duration = 0L; + + if (currentSongDuration != null) duration = (long) currentSongDuration * 1000; + + remoteControlClient.editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, artist) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, lockScreenBitmap) + .apply(); + } + } + + private void clearRemoteControl() + { + if (remoteControlClient != null) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + audioManager.unregisterRemoteControlClient(remoteControlClient); + remoteControlClient = null; + } + } + + public void setUpRemoteControlClient() + { + if (!Util.isLockScreenEnabled(context)) return; + + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + + if (remoteControlClient == null) + { + final Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(componentName); + PendingIntent broadcast = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); + remoteControlClient = new RemoteControlClient(broadcast); + audioManager.registerRemoteControlClient(remoteControlClient); + + // Flags for the media transport control that this client supports. + int flags = RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_STOP; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) + { + flags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; + + remoteControlClient.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() + { + @Override + public long onGetPlaybackPosition() + { + return mediaPlayer.getCurrentPosition(); + } + }); + + remoteControlClient.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() + { + @Override + public void onPlaybackPositionUpdate(long newPositionMs) + { + seekTo((int) newPositionMs); + } + }); + } + + remoteControlClient.setTransportControlFlags(flags); + } + } + + public synchronized void seekTo(int position) + { + try + { + mediaPlayer.seekTo(position); + cachedPosition = position; + + updateRemoteControl(); + } + catch (Exception x) + { + handleError(x); + } + } + + public synchronized int getPlayerPosition() + { + try + { + if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) + { + return 0; + } + + return cachedPosition; + } + catch (Exception x) + { + handleError(x); + return 0; + } + } + + public synchronized int getPlayerDuration() + { + if (currentPlaying != null) + { + Integer duration = currentPlaying.getSong().getDuration(); + if (duration != null) + { + return duration * 1000; + } + } + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) + { + try + { + return mediaPlayer.getDuration(); + } + catch (Exception x) + { + handleError(x); + } + } + return 0; + } + + public void setVolume(float volume) + { + if (mediaPlayer != null) + { + mediaPlayer.setVolume(volume, volume); + } + } + + public synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) + { + try + { + downloadFile.setPlaying(false); + //downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + boolean partial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + mediaPlayer.setOnCompletionListener(null); + secondaryProgress = -1; // Ensure seeking in non StreamProxy playback works + mediaPlayer.reset(); + setPlayerState(IDLE); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + String dataSource = file.getPath(); + + if (partial) + { + if (proxy == null) + { + proxy = new StreamProxy(new Supplier() { + @Override + public DownloadFile get() { return currentPlaying; } + }); + proxy.start(); + } + + dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); + Log.i(TAG, String.format("Data Source: %s", dataSource)); + } + else if (proxy != null) + { + proxy.stop(); + proxy = null; + } + + Log.i(TAG, "Preparing media player"); + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() + { + @Override + public void onBufferingUpdate(MediaPlayer mp, int percent) + { + SeekBar progressBar = DownloadActivity.getProgressBar(); + MusicDirectory.Entry song = downloadFile.getSong(); + + if (percent == 100) + { + if (progressBar != null) + { + progressBar.setSecondaryProgress(100 * progressBar.getMax()); + } + + mp.setOnBufferingUpdateListener(null); + } + else if (progressBar != null && song.getTranscodedContentType() == null && Util.getMaxBitRate(context) == 0) + { + secondaryProgress = (int) (((double) percent / (double) 100) * progressBar.getMax()); + progressBar.setSecondaryProgress(secondaryProgress); + } + } + }); + + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() + { + @Override + public void onPrepared(MediaPlayer mp) + { + Log.i(TAG, "Media player prepared"); + + setPlayerState(PREPARED); + + SeekBar progressBar = DownloadActivity.getProgressBar(); + + if (progressBar != null && downloadFile.isWorkDone()) + { + // Populate seek bar secondary progress if we have a complete file for consistency + DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax()); + } + + synchronized (Player.this) + { + if (position != 0) + { + Log.i(TAG, String.format("Restarting player from position %d", position)); + seekTo(position); + } + cachedPosition = position; + + if (start) + { + mediaPlayer.start(); + setPlayerState(STARTED); + } + else + { + setPlayerState(PAUSED); + } + } + + onPrepared.run(); + } + }); + + setupHandlers(downloadFile, partial); + + mediaPlayer.prepareAsync(); + } + catch (Exception x) + { + handleError(x); + } + } + + private synchronized void setupNext(final DownloadFile downloadFile) + { + try + { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + + if (nextMediaPlayer != null) + { + nextMediaPlayer.setOnCompletionListener(null); + nextMediaPlayer.release(); + nextMediaPlayer = null; + } + + nextMediaPlayer = new MediaPlayer(); + nextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + + try + { + nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); + } + catch (Throwable e) + { + nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + + nextMediaPlayer.setDataSource(file.getPath()); + setNextPlayerState(PREPARING); + + nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() + { + @Override + @SuppressLint("NewApi") + public void onPrepared(MediaPlayer mp) + { + try + { + setNextPlayerState(PREPARED); + + if (Util.getGaplessPlaybackPreference(context) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) + { + mediaPlayer.setNextMediaPlayer(nextMediaPlayer); + nextSetup = true; + } + } + catch (Exception x) + { + handleErrorNext(x); + } + } + }); + + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() + { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) + { + Log.w(TAG, String.format("Error on playing next (%d, %d): %s", what, extra, downloadFile)); + return true; + } + }); + + nextMediaPlayer.prepareAsync(); + } + catch (Exception x) + { + handleErrorNext(x); + } + } + + private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) + { + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() + { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) + { + Log.w(TAG, String.format("Error on playing file (%d, %d): %s", what, extra, downloadFile)); + int pos = cachedPosition; + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + return true; + } + }); + + final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; + + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() + { + @Override + public void onCompletion(MediaPlayer mediaPlayer) + { + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock.acquire(60000); + + int pos = cachedPosition; + Log.i(TAG, String.format("Ending position %d of %d", pos, duration)); + + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 1000))) + { + setPlayerState(COMPLETED); + + if (Util.getGaplessPlaybackPreference(context) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) + { + if (!nextSetup) + { + playNext(); + } + else + { + nextSetup = false; + playNext(); + } + } + else + { + onSongCompleted.accept(currentPlaying); + } + + return; + } + + synchronized (this) + { + if (downloadFile.isWorkDone()) + { + // Complete was called early even though file is fully buffered + Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } + else + { + Log.i(TAG, String.format("Requesting restart from %d of %d", pos, duration)); + reset(); + bufferTask = new BufferTask(downloadFile, pos); + bufferTask.start(); + } + } + } + }); + } + + public synchronized void reset() + { + if (bufferTask != null) + { + bufferTask.cancel(); + } + try + { + setPlayerState(IDLE); + mediaPlayer.setOnErrorListener(null); + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + } + catch (Exception x) + { + handleError(x); + } + } + + private class BufferTask extends CancellableTask + { + private final DownloadFile downloadFile; + private final int position; + private final long expectedFileSize; + private final File partialFile; + + public BufferTask(DownloadFile downloadFile, int position) + { + this.downloadFile = downloadFile; + this.position = position; + partialFile = downloadFile.getPartialFile(); + + long bufferLength = Util.getBufferLength(context); + + if (bufferLength == 0) + { + // Set to seconds in a day, basically infinity + bufferLength = 86400L; + } + + // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. + int bitRate = downloadFile.getBitRate(); + long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); + + // Find out how large the file should grow before resuming playback. + Log.i(TAG, String.format("Buffering from position %d and bitrate %d", position, bitRate)); + expectedFileSize = (position * bitRate / 8) + byteCount; + } + + @Override + public void execute() + { + setPlayerState(DOWNLOADING); + + while (!bufferComplete() && !Util.isOffline(context)) + { + Util.sleepQuietly(1000L); + if (isCancelled()) + { + return; + } + } + doPlay(downloadFile, position, true); + } + + private boolean bufferComplete() + { + boolean completeFileAvailable = downloadFile.isWorkDone(); + long size = partialFile.length(); + + Log.i(TAG, String.format("Buffering %s (%d/%d, %s)", partialFile, size, expectedFileSize, completeFileAvailable)); + return completeFileAvailable || size >= expectedFileSize; + } + + @Override + public String toString() + { + return String.format("BufferTask (%s)", downloadFile); + } + } + + private class CheckCompletionTask extends CancellableTask + { + private final DownloadFile downloadFile; + private final File partialFile; + + public CheckCompletionTask(DownloadFile downloadFile) + { + super(); + setNextPlayerState(PlayerState.IDLE); + + this.downloadFile = downloadFile; + + partialFile = downloadFile != null ? downloadFile.getPartialFile() : null; + } + + @Override + public void execute() + { + Thread.currentThread().setName("CheckCompletionTask"); + + if (downloadFile == null) + { + return; + } + + // Do an initial sleep so this prepare can't compete with main prepare + Util.sleepQuietly(5000L); + + while (!bufferComplete()) + { + Util.sleepQuietly(5000L); + + if (isCancelled()) + { + return; + } + } + + // Start the setup of the next media player + mediaPlayerHandler.post(new Runnable() + { + @Override + public void run() + { + setupNext(downloadFile); + } + }); + } + + private boolean bufferComplete() + { + boolean completeFileAvailable = downloadFile.isWorkDone(); + Log.i(TAG, String.format("Buffering next %s (%d)", partialFile, partialFile.length())); + return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); + } + + @Override + public String toString() + { + return String.format("CheckCompletionTask (%s)", downloadFile); + } + + } + + private class PositionCache implements Runnable + { + boolean isRunning = true; + + public void stop() + { + isRunning = false; + } + + @Override + public void run() + { + Thread.currentThread().setName("PositionCache"); + + // Stop checking position before the song reaches completion + while (isRunning) + { + try + { + if (mediaPlayer != null && playerState == STARTED) + { + cachedPosition = mediaPlayer.getCurrentPosition(); + } + + Util.sleepQuietly(25L); + } + catch (Exception e) + { + Log.w(TAG, "Crashed getting current position", e); + isRunning = false; + positionCache = null; + } + } + } + } + + private void handleError(Exception x) + { + Log.w(TAG, String.format("Media player error: %s", x), x); + + try + { + mediaPlayer.reset(); + } + catch (Exception ex) + { + Log.w(TAG, String.format("Exception encountered when resetting media player: %s", ex), ex); + } + } + + private void handleErrorNext(Exception x) + { + Log.w(TAG, String.format("Next Media player error: %s", x), x); + nextMediaPlayer.reset(); + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index 83ccad56..cc6a83a1 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -7,8 +7,7 @@ import android.util.Log; import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.Downloader; import java.io.File; import java.util.ArrayList; @@ -35,7 +34,7 @@ public class CacheCleaner private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L; private final Context context; - private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + private Lazy downloader = inject(Downloader.class); public CacheCleaner(Context context) { @@ -223,7 +222,7 @@ public class CacheCleaner { Set filesToNotDelete = new HashSet(5); - for (DownloadFile downloadFile : downloadServiceImpl.getValue().getDownloads()) + for (DownloadFile downloadFile : downloader.getValue().getDownloads()) { filesToNotDelete.add(downloadFile.getPartialFile()); filesToNotDelete.add(downloadFile.getCompleteFile()); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java index 8b476acd..07aef6b7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ShufflePlayBuffer.java @@ -37,19 +37,24 @@ import java.util.concurrent.TimeUnit; */ public class ShufflePlayBuffer { - private static final String TAG = ShufflePlayBuffer.class.getSimpleName(); private static final int CAPACITY = 50; private static final int REFILL_THRESHOLD = 40; - private final ScheduledExecutorService executorService; private final List buffer = new ArrayList(); private final Context context; + private ScheduledExecutorService executorService; private int currentServer; + public boolean isEnabled = false; + public ShufflePlayBuffer(Context context) { this.context = context; + } + + public void onCreate() + { executorService = Executors.newSingleThreadScheduledExecutor(); Runnable runnable = new Runnable() { @@ -62,6 +67,11 @@ public class ShufflePlayBuffer executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS); } + public void onDestroy() + { + executorService.shutdown(); + } + public List get(int size) { clearBufferIfNecessary(); @@ -78,13 +88,9 @@ public class ShufflePlayBuffer return result; } - public void shutdown() - { - executorService.shutdown(); - } - private void refill() { + if (!isEnabled) return; // Check if active server has changed. clearBufferIfNecessary(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java index da8b1d9e..1bc31fe3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -57,6 +57,7 @@ import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.service.DownloadServiceLifecycleSupport; +import org.moire.ultrasonic.service.Downloader; import org.moire.ultrasonic.service.MediaPlayerService; import org.moire.ultrasonic.service.MusicServiceFactory; @@ -960,7 +961,7 @@ public class Util extends DownloadActivity context.sendBroadcast(intent); } - public static void broadcastA2dpMetaDataChange(Context context, DownloadService downloadService) + public static void broadcastA2dpMetaDataChange(Context context, int playerPosition, DownloadFile currentPlaying, int listSize, int id) { if (!Util.getShouldSendBluetoothNotifications(context)) { @@ -970,17 +971,9 @@ public class Util extends DownloadActivity Entry song = null; Intent avrcpIntent = new Intent(CM_AVRCP_METADATA_CHANGED); - if (downloadService != null) - { - DownloadFile entry = downloadService.getCurrentPlaying(); + if (currentPlaying != null) song = currentPlaying.getSong(); - if (entry != null) - { - song = entry.getSong(); - } - } - - if (downloadService == null || song == null) + if (song == null) { avrcpIntent.putExtra("track", ""); avrcpIntent.putExtra("track_name", ""); @@ -1013,9 +1006,6 @@ public class Util extends DownloadActivity String artist = song.getArtist(); String album = song.getAlbum(); Integer duration = song.getDuration(); - Integer listSize = downloadService.getDownloads().size(); - Integer id = downloadService.getCurrentPlayingIndex() + 1; - Integer playerPosition = downloadService.getPlayerPosition(); avrcpIntent.putExtra("track", title); avrcpIntent.putExtra("track_name", title); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java index 5014c71b..5c617dbc 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java @@ -39,8 +39,10 @@ import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.Downloader; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; +import org.moire.ultrasonic.service.Player; import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.util.VideoPlayerType; @@ -82,7 +84,8 @@ public class SongView extends UpdateView implements Checkable private boolean maximized = false; private boolean useFiveStarRating; - private Lazy downloadServiceImpl = inject(DownloadServiceImpl.class); + private Lazy downloader = inject(Downloader.class); + protected Lazy player = inject(Player.class); public SongView(Context context) { @@ -169,7 +172,7 @@ public class SongView extends UpdateView implements Checkable this.song = song; - this.downloadFile = downloadServiceImpl.getValue().forSong(song); + this.downloadFile = downloader.getValue().getDownloadFileForSong(song); StringBuilder artist = new StringBuilder(60); @@ -320,7 +323,7 @@ public class SongView extends UpdateView implements Checkable { updateBackground(); - downloadFile = downloadServiceImpl.getValue().forSong(this.song); + downloadFile = downloader.getValue().getDownloadFileForSong(this.song); File partialFile = downloadFile.getPartialFile(); if (downloadFile.isWorkDone()) @@ -410,7 +413,7 @@ public class SongView extends UpdateView implements Checkable viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable); viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable); - boolean playing = downloadServiceImpl.getValue().getCurrentPlaying() == downloadFile; + boolean playing = player.getValue().currentPlaying == downloadFile; if (playing) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index eb07d759..7072738d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -15,6 +15,7 @@ import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.service.* import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.ShufflePlayBuffer internal const val MUSIC_SERVICE_CONTEXT = "CurrentMusicService" internal const val ONLINE_MUSIC_SERVICE = "OnlineMusicService" @@ -112,9 +113,12 @@ val musicServiceModule = module(MUSIC_SERVICE_CONTEXT) { single { SubsonicImageLoader(getProperty(DiProperties.APP_CONTEXT), get()) } - single { DownloadServiceImpl(androidContext()) } - single { JukeboxService(androidContext()) } - single { DownloadServiceLifecycleSupport(androidContext(), get())} - single { DownloadQueueSerializer(androidContext())} - single { ExternalStorageMonitor(androidContext())} + single { DownloadServiceImpl(androidContext(), get(), get(), get()) } + single { JukeboxService(androidContext(), get()) } + single { DownloadServiceLifecycleSupport(androidContext(), get(), get()) } + single { DownloadQueueSerializer(androidContext()) } + single { ExternalStorageMonitor(androidContext()) } + single { ShufflePlayBuffer(androidContext()) } + single { Downloader(androidContext(), get(), get(), get()) } + single { Player(androidContext()) } }