diff --git a/core/library/src/main/java/com/mobeta/android/dslv/DragSortListView.java b/core/library/src/main/java/com/mobeta/android/dslv/DragSortListView.java index 4f8ec744..b7e09722 100644 --- a/core/library/src/main/java/com/mobeta/android/dslv/DragSortListView.java +++ b/core/library/src/main/java/com/mobeta/android/dslv/DragSortListView.java @@ -2933,7 +2933,9 @@ public class DragSortListView extends ListView { // always do scroll mBlockLayoutRequests = true; - setSelectionFromTop(movePos, top - padTop); + // This cast is a workaround of an API bug, see https://issuetracker.google.com/issues/37045361 + ((ListView)DragSortListView.this).setSelectionFromTop(movePos, top - padTop); + DragSortListView.this.layoutChildren(); invalidate(); diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index fc439fa2..9a3a6166 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -960,7 +960,7 @@ 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..cae4632d 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/BookmarkActivity.java @@ -34,6 +34,7 @@ import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.service.DownloadFile; +import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.Constants; @@ -206,8 +207,8 @@ public class BookmarkActivity extends SubsonicTabActivity if (!getSelectedSongs(albumListView).isEmpty()) { int position = songs.get(0).getBookmarkPosition(); - if (getDownloadService() == null) return; - getDownloadService().restore(songs, 0, position, true, true); + if (getMediaPlayerController() == null) return; + getMediaPlayerController().restore(songs, 0, position, true, true); selectAll(false, false); } } @@ -296,7 +297,8 @@ public class BookmarkActivity extends SubsonicTabActivity private void enableButtons() { - if (getDownloadService() == null) + MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController == null) { return; } @@ -310,7 +312,7 @@ public class BookmarkActivity extends SubsonicTabActivity for (MusicDirectory.Entry song : selection) { - DownloadFile downloadFile = getDownloadService().forSong(song); + DownloadFile downloadFile = mediaPlayerController.getDownloadFileForSong(song); if (downloadFile.isWorkDone()) { deleteEnabled = true; @@ -345,7 +347,7 @@ public class BookmarkActivity extends SubsonicTabActivity private void downloadBackground(final boolean save, final List songs) { - if (getDownloadService() == null) + if (getMediaPlayerController() == null) { return; } @@ -356,7 +358,7 @@ public class BookmarkActivity extends SubsonicTabActivity public void run() { warnIfNetworkOrStorageUnavailable(); - getDownloadService().downloadBackground(songs, save); + getMediaPlayerController().downloadBackground(songs, save); if (save) { @@ -382,19 +384,19 @@ public class BookmarkActivity extends SubsonicTabActivity songs = getSelectedSongs(albumListView); } - if (getDownloadService() != null) + if (getMediaPlayerController() != null) { - getDownloadService().delete(songs); + getMediaPlayerController().delete(songs); } } private void unpin() { - if (getDownloadService() != null) + if (getMediaPlayerController() != null) { List songs = getSelectedSongs(albumListView); Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); - getDownloadService().unpin(songs); + getMediaPlayerController().unpin(songs); } } 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..4b6369a9 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java @@ -60,7 +60,7 @@ import org.moire.ultrasonic.domain.RepeatMode; import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.DownloadService; +import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.Constants; @@ -186,8 +186,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (!useFiveStarRating) ratingLinearLayout.setVisibility(View.GONE); - hollowStar = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_hollow); - fullStar = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_full); + hollowStar = Util.getDrawableFromAttribute(this, R.attr.star_hollow); + fullStar = Util.getDrawableFromAttribute(this, R.attr.star_full); fiveStar1ImageView.setOnClickListener(new View.OnClickListener() { @@ -257,7 +257,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - getDownloadService().previous(); + getMediaPlayerController().previous(); return null; } @@ -293,9 +293,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Boolean doInBackground() throws Throwable { - if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) + if (getMediaPlayerController().getCurrentPlayingNumberOnPlaylist() < getMediaPlayerController().getPlaylistSize() - 1) { - getDownloadService().next(); + getMediaPlayerController().next(); return true; } else @@ -337,7 +337,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - getDownloadService().pause(); + getMediaPlayerController().pause(); return null; } @@ -361,7 +361,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - getDownloadService().reset(); + getMediaPlayerController().reset(); return null; } @@ -406,7 +406,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override public void onClick(final View view) { - getDownloadService().shuffle(); + getMediaPlayerController().shuffle(); Util.toast(DownloadActivity.this, R.string.download_menu_shuffle_notification); } }); @@ -416,9 +416,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override public void onClick(final View view) { - final RepeatMode repeatMode = getDownloadService().getRepeatMode().next(); + final RepeatMode repeatMode = getMediaPlayerController().getRepeatMode().next(); - getDownloadService().setRepeatMode(repeatMode); + getMediaPlayerController().setRepeatMode(repeatMode); onDownloadListChanged(); switch (repeatMode) @@ -448,7 +448,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - getDownloadService().seekTo(getProgressBar().getProgress()); + getMediaPlayerController().seekTo(getProgressBar().getProgress()); return null; } @@ -483,7 +483,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - getDownloadService().play(position); + getMediaPlayerController().play(position); return null; } @@ -499,15 +499,15 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi registerForContextMenu(playlistView); - final DownloadService downloadService = getDownloadService(); - if (downloadService != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) { warnIfNetworkOrStorageUnavailable(); - downloadService.setShufflePlayEnabled(true); + mediaPlayerController.setShufflePlayEnabled(true); } - visualizerAvailable = (downloadService != null) && (downloadService.getVisualizerController() != null); - equalizerAvailable = (downloadService != null) && (downloadService.getEqualizerController() != null); + visualizerAvailable = (mediaPlayerController != null) && (mediaPlayerController.getVisualizerController() != null); + equalizerAvailable = (mediaPlayerController != null) && (mediaPlayerController.getEqualizerController() != null); new Thread(new Runnable() { @@ -516,8 +516,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi { try { - DownloadService downloadService = getDownloadService(); - jukeboxAvailable = (downloadService != null) && (downloadService.isJukeboxAvailable()); + MediaPlayerController mediaPlayerController = getMediaPlayerController(); + jukeboxAvailable = (mediaPlayerController != null) && (mediaPlayerController.isJukeboxAvailable()); } catch (Exception e) { @@ -549,7 +549,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi public boolean onTouch(final View view, final MotionEvent motionEvent) { visualizerView.setActive(!visualizerView.isActive()); - getDownloadService().setShowVisualization(visualizerView.isActive()); + getMediaPlayerController().setShowVisualization(visualizerView.isActive()); return true; } }); @@ -565,9 +565,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi { super.onResume(); - final DownloadService downloadService = getDownloadService(); + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (downloadService == null || downloadService.getCurrentPlaying() == null) + if (mediaPlayerController == null || mediaPlayerController.getCurrentPlaying() == null) { playlistFlipper.setDisplayedChild(1); } @@ -592,7 +592,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi executorService = Executors.newSingleThreadScheduledExecutor(); executorService.scheduleWithFixedDelay(runnable, 0L, 250L, TimeUnit.MILLISECONDS); - if (downloadService != null && downloadService.getKeepScreenOn()) + if (mediaPlayerController != null && mediaPlayerController.getKeepScreenOn()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -603,7 +603,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (visualizerView != null) { - visualizerView.setActive(downloadService != null && downloadService.getShowVisualization()); + visualizerView.setActive(mediaPlayerController != null && mediaPlayerController.getShowVisualization()); } invalidateOptionsMenu(); @@ -612,7 +612,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi // Scroll to current playing/downloading. private void scrollToCurrent() { - if (getDownloadService() == null) + if (getMediaPlayerController() == null) { return; } @@ -632,7 +632,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi } } - final DownloadFile currentDownloading = getDownloadService().getCurrentDownloading(); + final DownloadFile currentDownloading = getMediaPlayerController().getCurrentDownloading(); for (int i = 0; i < count; i++) { if (currentDownloading == playlistView.getItemAtPosition(i)) @@ -706,7 +706,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi { if (id == DIALOG_SAVE_PLAYLIST) { - final String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null; + final String playlistName = (getMediaPlayerController() != null) ? getMediaPlayerController().getSuggestedPlaylistName() : null; if (playlistName != null) { playlistNameView.setText(playlistName); @@ -778,11 +778,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi visualizerMenuItem.setVisible(visualizerAvailable); } - final DownloadService downloadService = getDownloadService(); + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (downloadService != null) + if (mediaPlayerController != null) { - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + DownloadFile downloadFile = mediaPlayerController.getCurrentPlaying(); if (downloadFile != null) { @@ -807,7 +807,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi } - if (downloadService.getKeepScreenOn()) + if (mediaPlayerController.getKeepScreenOn()) { if (screenOption != null) { @@ -827,7 +827,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi jukeboxOption.setEnabled(jukeboxAvailable); jukeboxOption.setVisible(jukeboxAvailable); - if (downloadService.isJukeboxEnabled()) + if (mediaPlayerController.isJukeboxEnabled()) { jukeboxOption.setTitle(R.string.download_menu_jukebox_off); } @@ -967,23 +967,23 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi startActivityForResultWithoutTransition(this, intent); return true; case R.id.menu_remove: - getDownloadService().remove(song); + getMediaPlayerController().remove(song); onDownloadListChanged(); return true; case R.id.menu_item_screen_on_off: - if (getDownloadService().getKeepScreenOn()) + if (getMediaPlayerController().getKeepScreenOn()) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getDownloadService().setKeepScreenOn(false); + getMediaPlayerController().setKeepScreenOn(false); } else { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getDownloadService().setKeepScreenOn(true); + getMediaPlayerController().setKeepScreenOn(true); } return true; case R.id.menu_shuffle: - getDownloadService().shuffle(); + getMediaPlayerController().shuffle(); Util.toast(this, R.string.download_menu_shuffle_notification); return true; case R.id.menu_item_equalizer: @@ -1002,24 +1002,24 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi visualizerViewLayout.setVisibility(View.VISIBLE); } - getDownloadService().setShowVisualization(visualizerView.isActive()); + getMediaPlayerController().setShowVisualization(visualizerView.isActive()); Util.toast(DownloadActivity.this, active ? R.string.download_visualizer_on : R.string.download_visualizer_off); return true; case R.id.menu_item_jukebox: - final boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled(); - getDownloadService().setJukeboxEnabled(jukeboxEnabled); + final boolean jukeboxEnabled = !getMediaPlayerController().isJukeboxEnabled(); + getMediaPlayerController().setJukeboxEnabled(jukeboxEnabled); Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false); return true; case R.id.menu_item_toggle_list: toggleFullScreenAlbumArt(); return true; case R.id.menu_item_clear_playlist: - getDownloadService().setShufflePlayEnabled(false); - getDownloadService().clear(); + getMediaPlayerController().setShufflePlayEnabled(false); + getMediaPlayerController().clear(); onDownloadListChanged(); return true; case R.id.menu_item_save_playlist: - if (!getDownloadService().getSongs().isEmpty()) + if (getMediaPlayerController().getPlaylistSize() > 0) { showDialog(DIALOG_SAVE_PLAYLIST); } @@ -1077,7 +1077,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi } final String songId = currentSong.getId(); - final int playerPosition = getDownloadService().getPlayerPosition(); + final int playerPosition = getMediaPlayerController().getPlayerPosition(); currentSong.setBookmarkPosition(playerPosition); @@ -1137,12 +1137,12 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi return true; case R.id.menu_item_share: - DownloadService downloadService = getDownloadService(); + MediaPlayerController mediaPlayerController = getMediaPlayerController(); List entries = new ArrayList(); - if (downloadService != null) + if (mediaPlayerController != null) { - List downloadServiceSongs = downloadService.getSongs(); + List downloadServiceSongs = mediaPlayerController.getPlayList(); if (downloadServiceSongs != null) { @@ -1170,17 +1170,18 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void update() { - if (getDownloadService() == null) + MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController == null) { return; } - if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) + if (currentRevision != mediaPlayerController.getPlayListUpdateRevision()) { onDownloadListChanged(); } - if (currentPlaying != getDownloadService().getCurrentPlaying()) + if (currentPlaying != mediaPlayerController.getCurrentPlaying()) { onCurrentChanged(); } @@ -1192,14 +1193,14 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void savePlaylistInBackground(final String playlistName) { Util.toast(DownloadActivity.this, getResources().getString(R.string.download_playlist_saving, playlistName)); - getDownloadService().setSuggestedPlaylistName(playlistName); + getMediaPlayerController().setSuggestedPlaylistName(playlistName); new SilentBackgroundTask(this) { @Override protected Void doInBackground() throws Throwable { final List entries = new LinkedList(); - for (final DownloadFile downloadFile : getDownloadService().getSongs()) + for (final DownloadFile downloadFile : getMediaPlayerController().getPlayList()) { entries.add(downloadFile.getSong()); } @@ -1243,7 +1244,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void start() { - final DownloadService service = getDownloadService(); + final MediaPlayerController service = getMediaPlayerController(); final PlayerState state = service.getPlayerState(); if (state == PAUSED || state == COMPLETED || state == STOPPED) @@ -1254,7 +1255,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi { warnIfNetworkOrStorageUnavailable(); - final int current = service.getCurrentPlayingIndex(); + final int current = getMediaPlayerController().getCurrentPlayingNumberOnPlaylist(); if (current == -1) { @@ -1269,13 +1270,13 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void onDownloadListChanged() { - final DownloadService downloadService = getDownloadService(); - if (downloadService == null) + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController == null) { return; } - final List list = downloadService.getSongs(); + final List list = mediaPlayerController.getPlayList(); emptyTextView.setText(R.string.download_empty); final SongListAdapter adapter = new SongListAdapter(this, list); @@ -1306,18 +1307,18 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi public void remove(int which) { DownloadFile item = adapter.getItem(which); - DownloadService downloadService = getDownloadService(); + MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (item == null || downloadService == null) + if (item == null || mediaPlayerController == null) { return; } - DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + DownloadFile currentPlaying = mediaPlayerController.getCurrentPlaying(); if (currentPlaying == item) { - getDownloadService().next(); + getMediaPlayerController().next(); } adapter.remove(item); @@ -1333,9 +1334,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi }); emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); - currentRevision = downloadService.getDownloadListUpdateRevision(); + currentRevision = mediaPlayerController.getPlayListUpdateRevision(); - switch (downloadService.getRepeatMode()) + switch (mediaPlayerController.getRepeatMode()) { case OFF: repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_off)); @@ -1353,20 +1354,20 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void onCurrentChanged() { - DownloadService downloadService = getDownloadService(); + MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (downloadService == null) + if (mediaPlayerController == null) { return; } - currentPlaying = downloadService.getCurrentPlaying(); + currentPlaying = mediaPlayerController.getCurrentPlaying(); scrollToCurrent(); - long totalDuration = downloadService.getDownloadListDuration(); - long totalSongs = downloadService.getSongs().size(); - int currentSongIndex = downloadService.getCurrentPlayingIndex() + 1; + long totalDuration = mediaPlayerController.getPlayListDuration(); + long totalSongs = mediaPlayerController.getPlaylistSize(); + int currentSongIndex = mediaPlayerController.getCurrentPlayingNumberOnPlaylist() + 1; String duration = Util.formatTotalDuration(totalDuration); @@ -1398,16 +1399,16 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void onSliderProgressChanged() { - DownloadService downloadService = getDownloadService(); + MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (downloadService == null || onProgressChangedTask != null) + if (mediaPlayerController == null || onProgressChangedTask != null) { return; } onProgressChangedTask = new SilentBackgroundTask(this) { - DownloadService downloadService; + MediaPlayerController mediaPlayerController; boolean isJukeboxEnabled; int millisPlayed; Integer duration; @@ -1416,11 +1417,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - downloadService = getDownloadService(); - isJukeboxEnabled = downloadService.isJukeboxEnabled(); - millisPlayed = Math.max(0, downloadService.getPlayerPosition()); - duration = downloadService.getPlayerDuration(); - playerState = getDownloadService().getPlayerState(); + this.mediaPlayerController = getMediaPlayerController(); + isJukeboxEnabled = this.mediaPlayerController.isJukeboxEnabled(); + millisPlayed = Math.max(0, this.mediaPlayerController.getPlayerPosition()); + duration = this.mediaPlayerController.getPlayerDuration(); + playerState = getMediaPlayerController().getPlayerState(); return null; } @@ -1457,9 +1458,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi setActionBarSubtitle(R.string.download_playerstate_buffering); break; case STARTED: - final DownloadService downloadService = getDownloadService(); + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (downloadService != null && downloadService.isShufflePlayEnabled()) + if (mediaPlayerController != null && mediaPlayerController.isShufflePlayEnabled()) { setActionBarSubtitle(R.string.download_playerstate_playing_shuffle); } @@ -1503,7 +1504,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi break; } - // TODO: It would be a lot nicer if DownloadService would send an event when this is necessary instead of updating every time + // TODO: It would be a lot nicer if MediaPlayerController would send an event when this is necessary instead of updating every time displaySongRating(); onProgressChangedTask = null; @@ -1514,8 +1515,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private void changeProgress(final int ms) { - final DownloadService downloadService = getDownloadService(); - if (downloadService == null) + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController == null) { return; } @@ -1529,12 +1530,12 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected Void doInBackground() throws Throwable { - msPlayed = Math.max(0, downloadService.getPlayerPosition()); - duration = downloadService.getPlayerDuration(); + msPlayed = Math.max(0, mediaPlayerController.getPlayerPosition()); + duration = mediaPlayerController.getPlayerDuration(); final int msTotal = duration; seekTo = msPlayed + ms > msTotal ? msTotal : msPlayed + ms; - downloadService.seekTo(seekTo); + mediaPlayerController.seekTo(seekTo); return null; } @@ -1562,9 +1563,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX, final float velocityY) { - final DownloadService downloadService = getDownloadService(); + final MediaPlayerController mediaPlayerController = getMediaPlayerController(); - if (downloadService == null || e1 == null || e2 == null) + if (mediaPlayerController == null || e1 == null || e2 == null) { return false; } @@ -1580,9 +1581,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (e1X - e2X > swipeDistance && absX > swipeVelocity) { warnIfNetworkOrStorageUnavailable(); - if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) + if (mediaPlayerController.getCurrentPlayingNumberOnPlaylist() < mediaPlayerController.getPlaylistSize() - 1) { - downloadService.next(); + mediaPlayerController.next(); onCurrentChanged(); onSliderProgressChanged(); } @@ -1593,7 +1594,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (e2X - e1X > swipeDistance && absX > swipeVelocity) { warnIfNetworkOrStorageUnavailable(); - downloadService.previous(); + mediaPlayerController.previous(); onCurrentChanged(); onSliderProgressChanged(); return true; @@ -1603,7 +1604,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { warnIfNetworkOrStorageUnavailable(); - downloadService.seekTo(downloadService.getPlayerPosition() + 30000); + mediaPlayerController.seekTo(mediaPlayerController.getPlayerPosition() + 30000); onSliderProgressChanged(); return true; } @@ -1612,7 +1613,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { warnIfNetworkOrStorageUnavailable(); - downloadService.seekTo(downloadService.getPlayerPosition() - 8000); + mediaPlayerController.seekTo(mediaPlayerController.getPlayerPosition() - 8000); onSliderProgressChanged(); return true; } @@ -1663,6 +1664,6 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi return; displaySongRating(); - getDownloadService().setSongRating(rating); + getMediaPlayerController().setSongRating(rating); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java index 58047244..07b48a96 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/EqualizerActivity.java @@ -32,12 +32,15 @@ import android.widget.TextView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.audiofx.EqualizerController; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerController; import java.util.HashMap; import java.util.Map; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Equalizer controls. * @@ -52,6 +55,8 @@ public class EqualizerActivity extends ResultActivity private EqualizerController equalizerController; private Equalizer equalizer; + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); + @Override public void onCreate(Bundle bundle) { @@ -123,14 +128,7 @@ public class EqualizerActivity extends ResultActivity private void setup() { - DownloadService instance = DownloadServiceImpl.getInstance(); - - if (instance == null) - { - return; - } - - equalizerController = instance.getEqualizerController(); + equalizerController = mediaPlayerControllerLazy.getValue().getEqualizerController(); equalizer = equalizerController.getEqualizer(); initEqualizer(); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java index d9665a79..66297dba 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java @@ -34,8 +34,8 @@ import android.widget.ListView; import android.widget.TextView; import org.moire.ultrasonic.R; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerController; +import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.Constants; @@ -46,7 +46,10 @@ import org.moire.ultrasonic.util.Util; import java.util.Collections; +import kotlin.Lazy; + import static java.util.Arrays.asList; +import static org.koin.java.standalone.KoinJavaComponent.inject; public class MainActivity extends SubsonicTabActivity { @@ -67,6 +70,8 @@ public class MainActivity extends SubsonicTabActivity private static boolean infoDialogDisplayed; private static boolean shouldUseId3; + private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); + /** * Called when the activity is first created. */ @@ -79,9 +84,9 @@ public class MainActivity extends SubsonicTabActivity { setResult(Constants.RESULT_CLOSE_ALL); - if (getDownloadService() != null) + if (getMediaPlayerController() != null) { - getDownloadService().stopJukeboxService(); + getMediaPlayerController().stopJukeboxService(); } if (getImageLoader() != null) @@ -456,7 +461,7 @@ public class MainActivity extends SubsonicTabActivity private void setActiveServer(final int instance) { - final DownloadService service = getDownloadService(); + final MediaPlayerController service = getMediaPlayerController(); if (Util.getActiveServer(this) != instance) { @@ -476,8 +481,8 @@ public class MainActivity extends SubsonicTabActivity private void exit() { - stopService(new Intent(this, DownloadServiceImpl.class)); - Util.unregisterMediaButtonEventReceiver(this); + lifecycleSupport.getValue().onDestroy(); + Util.unregisterMediaButtonEventReceiver(this, false); finish(); } 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..88b7a70a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SearchActivity.java @@ -37,7 +37,7 @@ import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.SearchCriteria; import org.moire.ultrasonic.domain.SearchResult; -import org.moire.ultrasonic.service.DownloadService; +import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.BackgroundTask; @@ -322,7 +322,7 @@ public class SearchActivity extends SubsonicTabActivity { songs.add(entry); Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); - getDownloadService().unpin(songs); + getMediaPlayerController().unpin(songs); } break; case R.id.menu_item_share: @@ -341,7 +341,7 @@ public class SearchActivity extends SubsonicTabActivity private void downloadBackground(final boolean save, final List songs) { - if (getDownloadService() == null) + if (getMediaPlayerController() == null) { return; } @@ -352,7 +352,7 @@ public class SearchActivity extends SubsonicTabActivity public void run() { warnIfNetworkOrStorageUnavailable(); - getDownloadService().downloadBackground(songs, save); + getMediaPlayerController().downloadBackground(songs, save); } }; @@ -508,19 +508,19 @@ public class SearchActivity extends SubsonicTabActivity private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) { - DownloadService downloadService = getDownloadService(); - if (downloadService != null) + MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController != null) { if (!append && !playNext) { - downloadService.clear(); + mediaPlayerController.clear(); } - downloadService.download(Collections.singletonList(song), save, false, playNext, false, false); + mediaPlayerController.download(Collections.singletonList(song), save, false, playNext, false, false); if (autoplay) { - downloadService.play(downloadService.size() - 1); + mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 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..a96159a2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectAlbumActivity.java @@ -40,6 +40,7 @@ import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.service.DownloadFile; +import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.AlbumHeader; @@ -1010,7 +1011,8 @@ public class SelectAlbumActivity extends SubsonicTabActivity private void enableButtons() { - if (getDownloadService() == null) + MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (mediaPlayerController == null) { return; } @@ -1024,7 +1026,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity for (MusicDirectory.Entry song : selection) { - DownloadFile downloadFile = getDownloadService().forSong(song); + DownloadFile downloadFile = mediaPlayerController.getDownloadFileForSong(song); if (downloadFile.isWorkDone()) { deleteEnabled = true; @@ -1061,7 +1063,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity private void downloadBackground(final boolean save, final List songs) { - if (getDownloadService() == null) + if (getMediaPlayerController() == null) { return; } @@ -1072,7 +1074,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity public void run() { warnIfNetworkOrStorageUnavailable(); - getDownloadService().downloadBackground(songs, save); + getMediaPlayerController().downloadBackground(songs, save); if (save) { @@ -1098,19 +1100,19 @@ public class SelectAlbumActivity extends SubsonicTabActivity songs = getSelectedSongs(albumListView); } - if (getDownloadService() != null) + if (getMediaPlayerController() != null) { - getDownloadService().delete(songs); + getMediaPlayerController().delete(songs); } } private void unpin() { - if (getDownloadService() != null) + if (getMediaPlayerController() != null) { List songs = getSelectedSongs(albumListView); Util.toast(SelectAlbumActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); - getDownloadService().unpin(songs); + getMediaPlayerController().unpin(songs); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java index 7af8157d..0ba31f04 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SelectPlaylistActivity.java @@ -126,7 +126,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt List playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this); if (!Util.isOffline(SelectPlaylistActivity.this)) - new CacheCleaner(SelectPlaylistActivity.this, getDownloadService()).cleanPlaylists(playlists); + new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists); return playlists; } 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 a3e7954e..6d790618 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -29,7 +29,6 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import androidx.appcompat.app.ActionBar; import android.util.Log; import android.view.*; @@ -39,6 +38,7 @@ import android.widget.*; import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.Position; import org.koin.java.standalone.KoinJavaComponent; +import static org.koin.java.standalone.KoinJavaComponent.inject; import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory.Entry; @@ -56,6 +56,8 @@ import java.io.PrintWriter; import java.util.*; import java.util.regex.Pattern; +import kotlin.Lazy; + /** * @author Sindre Mehus */ @@ -74,6 +76,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen private static final String STATE_ACTIVE_POSITION = "org.moire.ultrasonic.activePosition"; private static final int DIALOG_ASK_FOR_SHARE_DETAILS = 102; + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); + private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); + public MenuDrawer menuDrawer; private int activePosition = 1; private int menuActiveViewId; @@ -97,8 +102,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen applyTheme(); super.onCreate(bundle); - // This should always succeed as it is called when Ultrasonic is in the foreground - startService(new Intent(this, DownloadServiceImpl.class)); setVolumeControlStream(AudioManager.STREAM_MUSIC); if (bundle != null) @@ -155,7 +158,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen applyTheme(); instance = this; - Util.registerMediaButtonEventReceiver(this); + Util.registerMediaButtonEventReceiver(this, false); + // Lifecycle support's constructor registers some event receivers so it should be created early + lifecycleSupport.getValue().onCreate(); // Make sure to update theme if (theme != null && !theme.equals(Util.getTheme(this))) @@ -190,7 +195,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen @Override protected void onDestroy() { - Util.unregisterMediaButtonEventReceiver(this); + Util.unregisterMediaButtonEventReceiver(this, false); super.onDestroy(); destroyed = true; nowPlayingView = null; @@ -203,11 +208,11 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN; boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP; boolean isVolumeAdjust = isVolumeDown || isVolumeUp; - boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled(); + boolean isJukebox = getMediaPlayerController() != null && getMediaPlayerController().isJukeboxEnabled(); if (isVolumeAdjust && isJukebox) { - getDownloadService().adjustJukeboxVolume(isVolumeUp); + getMediaPlayerController().adjustJukeboxVolume(isVolumeUp); return true; } @@ -257,27 +262,22 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen if (nowPlayingView != null) { - final DownloadService downloadService = DownloadServiceImpl.getInstance(); + PlayerState playerState = mediaPlayerControllerLazy.getValue().getPlayerState(); - if (downloadService != null) + if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED)) { - PlayerState playerState = downloadService.getPlayerState(); + DownloadFile file = mediaPlayerControllerLazy.getValue().getCurrentPlaying(); - if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED)) + if (file != null) { - DownloadFile file = downloadService.getCurrentPlaying(); - - if (file != null) - { - final Entry song = file.getSong(); - showNowPlaying(SubsonicTabActivity.this, downloadService, song, playerState); - } - } - else - { - hideNowPlaying(); + final Entry song = file.getSong(); + showNowPlaying(SubsonicTabActivity.this, mediaPlayerControllerLazy.getValue(), song, playerState); } } + else + { + hideNowPlaying(); + } } return null; @@ -306,9 +306,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen } } - private void showNowPlaying(final Context context, final DownloadService downloadService, final Entry song, final PlayerState playerState) + private void showNowPlaying(final Context context, final MediaPlayerController mediaPlayerController, final Entry song, final PlayerState playerState) { - if (context == null || downloadService == null || song == null || playerState == null) + if (context == null || mediaPlayerController == null || song == null || playerState == null) { return; } @@ -387,7 +387,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen ImageView nowPlayingControlPlay = (ImageView) nowPlayingView.findViewById(R.id.now_playing_control_play); - SwipeDetector swipeDetector = new SwipeDetector(SubsonicTabActivity.this, downloadService); + SwipeDetector swipeDetector = new SwipeDetector(SubsonicTabActivity.this, mediaPlayerController); setOnTouchListenerOnUiThread(nowPlayingView, swipeDetector); setOnClickListenerOnUiThread(nowPlayingView, new OnClickListener() @@ -403,7 +403,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen @Override public void onClick(View view) { - downloadService.togglePlayPause(); + mediaPlayerController.togglePlayPause(); } }); @@ -762,33 +762,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen } } - public DownloadService getDownloadService() + public MediaPlayerController getMediaPlayerController() { - // If service is not available, request it to start and wait for it. - for (int i = 0; i < 5; i++) - { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - - if (downloadService != null) - { - return downloadService; - } - - Log.w(TAG, "DownloadService not running. Attempting to start it."); - - try - { - startService(new Intent(this, DownloadServiceImpl.class)); - } - catch (IllegalStateException exception) - { - Log.w(TAG, "getDownloadService couldn't start DownloadServiceImpl because the application was in the background."); - return null; - } - Util.sleepQuietly(50L); - } - - return DownloadServiceImpl.getInstance(); + return mediaPlayerControllerLazy.getValue(); } protected void warnIfNetworkOrStorageUnavailable() @@ -839,7 +815,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List songs) { - if (getDownloadService() == null) + if (getMediaPlayerController() == null) { return; } @@ -851,16 +827,16 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen { if (!append && !playNext) { - getDownloadService().clear(); + getMediaPlayerController().clear(); } warnIfNetworkOrStorageUnavailable(); - getDownloadService().download(songs, save, autoPlay, playNext, shuffle, false); + getMediaPlayerController().download(songs, save, autoPlay, playNext, shuffle, false); String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME); if (playlistName != null) { - getDownloadService().setSuggestedPlaylistName(playlistName); + getMediaPlayerController().setSuggestedPlaylistName(playlistName); } if (autoPlay) @@ -1015,23 +991,23 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen Collections.sort(songs, new EntryByDiscAndTrackComparator()); } - DownloadService downloadService = getDownloadService(); - if (!songs.isEmpty() && downloadService != null) + MediaPlayerController mediaPlayerController = getMediaPlayerController(); + if (!songs.isEmpty() && mediaPlayerController != null) { if (!append && !playNext && !unpin && !background) { - downloadService.clear(); + mediaPlayerController.clear(); } warnIfNetworkOrStorageUnavailable(); if (!background) { if (unpin) { - downloadService.unpin(songs); + mediaPlayerController.unpin(songs); } else { - downloadService.download(songs, save, autoplay, playNext, shuffle, false); + mediaPlayerController.download(songs, save, autoplay, playNext, shuffle, false); if (!append && Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this)) { startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class); @@ -1042,11 +1018,11 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen { if (unpin) { - downloadService.unpin(songs); + mediaPlayerController.unpin(songs); } else { - downloadService.downloadBackground(songs, save); + mediaPlayerController.downloadBackground(songs, save); } } } @@ -1374,15 +1350,15 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen protected class SwipeDetector implements OnTouchListener { - public SwipeDetector(SubsonicTabActivity activity, final DownloadService downloadService) + public SwipeDetector(SubsonicTabActivity activity, final MediaPlayerController mediaPlayerController) { - this.downloadService = downloadService; + this.mediaPlayerController = mediaPlayerController; this.activity = activity; } private static final int MIN_DISTANCE = 30; private float downX, downY, upX, upY; - private DownloadService downloadService; + private MediaPlayerController mediaPlayerController; private SubsonicTabActivity activity; @Override @@ -1409,12 +1385,12 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen // left or right if (deltaX < 0) { - downloadService.previous(); + mediaPlayerController.previous(); return false; } if (deltaX > 0) { - downloadService.next(); + mediaPlayerController.next(); return false; } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java index 864d8a14..d15d6a0e 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SettingsFragment.java @@ -16,12 +16,15 @@ import org.moire.ultrasonic.activity.SubsonicTabActivity; import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.provider.SearchSuggestionProvider; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.util.*; import java.io.File; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Shows main app settings. */ @@ -62,6 +65,8 @@ public class SettingsFragment extends PreferenceFragment private SharedPreferences settings; private int activeServers; + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -370,10 +375,10 @@ public class SettingsFragment extends PreferenceFragment private void setMediaButtonsEnabled(boolean enabled) { if (enabled) { lockScreenEnabled.setEnabled(true); - Util.registerMediaButtonEventReceiver(getActivity()); + Util.registerMediaButtonEventReceiver(getActivity(), false); } else { lockScreenEnabled.setEnabled(false); - Util.unregisterMediaButtonEventReceiver(getActivity()); + Util.unregisterMediaButtonEventReceiver(getActivity(), false); } } @@ -401,7 +406,6 @@ public class SettingsFragment extends PreferenceFragment } // Clear download queue. - DownloadService downloadService = DownloadServiceImpl.getInstance(); - downloadService.clear(); + mediaPlayerControllerLazy.getValue().clear(); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java index d4e986c6..849ded07 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/UltraSonicAppWidgetProvider.java @@ -17,8 +17,9 @@ import org.moire.ultrasonic.R; import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; +import org.moire.ultrasonic.service.MediaPlayerController; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; public class UltraSonicAppWidgetProvider extends AppWidgetProvider @@ -67,13 +68,13 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider } /** - * Handle a change notification coming over from {@link DownloadService} + * Handle a change notification coming over from {@link MediaPlayerController} */ - public void notifyChange(Context context, DownloadService service, boolean playing, boolean setAlbum) + public void notifyChange(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum) { if (hasInstances(context)) { - performUpdate(context, service, null, playing, setAlbum); + performUpdate(context, currentSong, null, playing, setAlbum); } } @@ -96,15 +97,14 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider /** * Update all active widget instances by pushing changes */ - private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing, boolean setAlbum) + private void performUpdate(Context context, MusicDirectory.Entry currentSong, int[] appWidgetIds, boolean playing, boolean setAlbum) { final Resources res = context.getResources(); final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId); - MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong(); - String title = currentPlaying == null ? null : currentPlaying.getTitle(); - String artist = currentPlaying == null ? null : currentPlaying.getArtist(); - String album = currentPlaying == null ? null : currentPlaying.getAlbum(); + String title = currentSong == null ? null : currentSong.getTitle(); + String artist = currentSong == null ? null : currentSong.getArtist(); + String album = currentSong == null ? null : currentSong.getAlbum(); CharSequence errorState = null; // Show error message? @@ -117,7 +117,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider { errorState = res.getText(R.string.widget_sdcard_missing); } - else if (currentPlaying == null) + else if (currentSong == null) { errorState = res.getText(R.string.widget_initial_text); } @@ -157,7 +157,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider // Set the cover art try { - Bitmap bitmap = currentPlaying == null ? null : FileUtil.getAlbumArtBitmap(context, currentPlaying, 240, true); + Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmap(context, currentSong, 240, true); if (bitmap == null) { @@ -176,7 +176,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider } // Link actions buttons to intents - linkButtons(context, views, currentPlaying != null); + linkButtons(context, views, currentSong != null); pushUpdate(context, appWidgetIds, views); } @@ -194,27 +194,30 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class); intent.setAction("android.intent.action.MAIN"); intent.addCategory("android.intent.category.LAUNCHER"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); // Emulate media button clicks. - intent = new Intent("1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + //intent.setPackage(context.getPackageName()); + intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0); views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - intent = new Intent("2"); // Use a unique action name to ensure a different PendingIntent to be created. - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + //intent.setPackage(context.getPackageName()); + intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0); views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - intent = new Intent("3"); // Use a unique action name to ensure a different PendingIntent to be created. - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + //intent.setPackage(context.getPackageName()); + intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class)); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); } } 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 721f0eb1..9bc2ad66 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/A2dpIntentReceiver.java @@ -5,31 +5,26 @@ 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.MediaPlayerController; + +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; public class A2dpIntentReceiver extends BroadcastReceiver { - private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse"; + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); @Override public void onReceive(Context context, Intent intent) { - - DownloadService downloadService = DownloadServiceImpl.getInstance(); - - if (downloadService == null) + if (mediaPlayerControllerLazy.getValue().getCurrentPlaying() == null) { return; } - if (downloadService.getCurrentPlaying() == null) - { - return; - } - - Entry song = downloadService.getCurrentPlaying().getSong(); + Entry song = mediaPlayerControllerLazy.getValue().getCurrentPlaying().getSong(); if (song == null) { @@ -39,8 +34,8 @@ public class A2dpIntentReceiver extends BroadcastReceiver Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE); Integer duration = song.getDuration(); - Integer playerPosition = downloadService.getPlayerPosition(); - Integer listSize = downloadService.getDownloads().size(); + int playerPosition = mediaPlayerControllerLazy.getValue().getPlayerPosition(); + int listSize = mediaPlayerControllerLazy.getValue().getPlaylistSize(); if (duration != null) { @@ -50,17 +45,13 @@ public class A2dpIntentReceiver extends BroadcastReceiver avrcpIntent.putExtra("position", (long) playerPosition); avrcpIntent.putExtra("ListSize", (long) listSize); - switch (downloadService.getPlayerState()) + switch (mediaPlayerControllerLazy.getValue().getPlayerState()) { case STARTED: 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/receiver/BluetoothIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java index 4a731322..fa1c61c4 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java @@ -24,7 +24,7 @@ import android.content.Context; import android.content.Intent; import android.util.Log; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; /** @@ -65,13 +65,13 @@ public class BluetoothIntentReceiver extends BroadcastReceiver if (connected) { Log.i(TAG, "Connected to Bluetooth device, requesting media button focus."); - Util.registerMediaButtonEventReceiver(context); + Util.registerMediaButtonEventReceiver(context, false); } if (disconnected) { Log.i(TAG, "Disconnected from Bluetooth device, requesting pause."); - context.sendBroadcast(new Intent(DownloadServiceImpl.CMD_PAUSE)); + context.sendBroadcast(new Intent(Constants.CMD_PAUSE)); } } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java index 171fe2ff..51b9db57 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/MediaButtonIntentReceiver.java @@ -21,82 +21,63 @@ package org.moire.ultrasonic.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; -import android.view.KeyEvent; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; +import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Util; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * @author Sindre Mehus */ public class MediaButtonIntentReceiver extends BroadcastReceiver { - private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); + private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); @Override public void onReceive(Context context, Intent intent) { - if (Util.getMediaButtonsPreference(context)) + String intentAction = intent.getAction(); + + // If media button are turned off and we received a media button, exit + if (!Util.getMediaButtonsPreference(context) && + Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) return; + + // Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets + if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction) && + !Constants.CMD_PROCESS_KEYCODE.equals(intentAction)) return; + + Bundle extras = intent.getExtras(); + + if (extras == null) { - String intentAction = intent.getAction(); + return; + } - if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) return; + Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT); + Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); - Bundle extras = intent.getExtras(); - - if (extras == null) - { - return; - } - - Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT); - Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); - - Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); + try + { + Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE); serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + lifecycleSupport.getValue().receiveIntent(serviceIntent); - try + if (isOrderedBroadcast()) { - context.startService(serviceIntent); - } - catch (IllegalStateException exception) - { - Log.i(TAG, "MediaButtonIntentReceiver couldn't start DownloadServiceImpl because the application was in the background."); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - { - KeyEvent keyEvent = (KeyEvent) event; - if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getRepeatCount() == 0) - { - int keyCode = keyEvent.getKeyCode(); - if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || - keyCode == KeyEvent.KEYCODE_HEADSETHOOK || - keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) - { - // TODO: The only time it is OK to start DownloadServiceImpl as a foreground service is when we now it will display its notification. - // When DownloadServiceImpl is refactored to a proper foreground service, this can be removed. - context.startForegroundService(serviceIntent); - Log.i(TAG, "MediaButtonIntentReceiver started DownloadServiceImpl as foreground service"); - } - } - } - } - - try - { - if (isOrderedBroadcast()) - { - abortBroadcast(); - } - } - catch (Exception x) - { - // Ignored. + abortBroadcast(); } } + catch (Exception x) + { + // Ignored. + } } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java new file mode 100644 index 00000000..282b1061 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/AudioFocusHandler.java @@ -0,0 +1,90 @@ +package org.moire.ultrasonic.service; + +import android.content.Context; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.util.Log; + +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.util.Constants; +import org.moire.ultrasonic.util.Util; + +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + +public class AudioFocusHandler +{ + private static final String TAG = AudioFocusHandler.class.getSimpleName(); + + private static boolean hasFocus; + private static boolean pauseFocus; + private static boolean lowerFocus; + + // TODO: This is a circular reference, try to remove it + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); + private Context context; + + public AudioFocusHandler(Context context) + { + this.context = context; + } + + public void requestAudioFocus() + { + if (!hasFocus) + { + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + hasFocus = true; + audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() + { + @Override + public void onAudioFocusChange(int focusChange) + { + MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue(); + if ((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !mediaPlayerController.isJukeboxEnabled()) + { + Log.v(TAG, "Lost Audio Focus"); + if (mediaPlayerController.getPlayerState() == PlayerState.STARTED) + { + SharedPreferences preferences = Util.getPreferences(context); + int lossPref = Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")); + if (lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) + { + lowerFocus = true; + mediaPlayerController.setVolume(0.1f); + } + else if (lossPref == 0 || (lossPref == 1)) + { + pauseFocus = true; + mediaPlayerController.pause(); + } + } + } + else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) + { + Log.v(TAG, "Regained Audio Focus"); + if (pauseFocus) + { + pauseFocus = false; + mediaPlayerController.start(); + } + else if (lowerFocus) + { + lowerFocus = false; + mediaPlayerController.setVolume(1.0f); + } + } + else if (focusChange == AudioManager.AUDIOFOCUS_LOSS && !mediaPlayerController.isJukeboxEnabled()) + { + hasFocus = false; + mediaPlayerController.pause(); + audioManager.abandonAudioFocus(this); + Log.v(TAG, "Abandoned Audio Focus"); + } + } + }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + Log.v(TAG, "Got Audio Focus"); + } + } +} 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..8909762e --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/BiConsumer.java @@ -0,0 +1,11 @@ +package org.moire.ultrasonic.service; + +/** + * Abstract class for consumers with two parameters + * @param The type of the first object to consume + * @param The type of the second object to consume + */ +public abstract class BiConsumer +{ + public abstract void accept(T t, U u); +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java new file mode 100644 index 00000000..d2b09de7 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Consumer.java @@ -0,0 +1,10 @@ +package org.moire.ultrasonic.service; + +/** + * Abstract class for consumers with one parameter + * @param The type of the object to consume + */ +public abstract class Consumer +{ + public abstract void accept(T t); +} 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 3c7d9ef4..4bfa4277 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java @@ -24,6 +24,7 @@ import android.os.PowerManager; import android.text.TextUtils; import android.util.Log; +import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.util.CacheCleaner; import org.moire.ultrasonic.util.CancellableTask; @@ -37,11 +38,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; +import kotlin.Lazy; import kotlin.Pair; import static android.content.Context.POWER_SERVICE; import static android.os.PowerManager.ON_AFTER_RELEASE; import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK; +import static org.koin.java.standalone.KoinJavaComponent.inject; /** * @author Sindre Mehus @@ -49,7 +52,6 @@ import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK; */ public class DownloadFile { - private static final String TAG = DownloadFile.class.getSimpleName(); private final Context context; private final MusicDirectory.Entry song; @@ -66,6 +68,8 @@ 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(); @@ -282,6 +286,7 @@ public class DownloadFile this.isPlaying = isPlaying; } + @NotNull @Override public String toString() { @@ -304,7 +309,7 @@ public class DownloadFile { PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE); wakeLock = pm.newWakeLock(SCREEN_DIM_WAKE_LOCK | ON_AFTER_RELEASE, toString()); - wakeLock.acquire(); + wakeLock.acquire(10*60*1000L /*10 minutes*/); Log.i(TAG, String.format("Acquired wake lock %s", wakeLock)); } @@ -439,15 +444,13 @@ public class DownloadFile wifiLock.release(); } - new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace(); + new CacheCleaner(context).cleanSpace(); - if (DownloadServiceImpl.getInstance() != null) - { - ((DownloadServiceImpl) DownloadServiceImpl.getInstance()).checkDownloads(); - } + downloader.getValue().checkDownloads(); } } + @NotNull @Override public String toString() { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java new file mode 100644 index 00000000..3fe70765 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadQueueSerializer.java @@ -0,0 +1,110 @@ +package org.moire.ultrasonic.service; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +import org.moire.ultrasonic.util.Constants; +import org.moire.ultrasonic.util.FileUtil; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * This class is responsible for the serialization / deserialization + * of the DownloadQueue (playlist) to the filesystem. + * It also serializes the player state e.g. current playing number and play position. + */ +public class DownloadQueueSerializer +{ + private static final String TAG = DownloadQueueSerializer.class.getSimpleName(); + + public final Lock lock = new ReentrantLock(); + public final AtomicBoolean setup = new AtomicBoolean(false); + private Context context; + + public DownloadQueueSerializer(Context context) + { + this.context = context; + } + + public void serializeDownloadQueue(Iterable songs, int currentPlayingIndex, int currentPlayingPosition) + { + if (!setup.get()) + { + return; + } + + new SerializeTask().execute(songs, currentPlayingIndex, currentPlayingPosition); + } + + public void serializeDownloadQueueNow(Iterable songs, int currentPlayingIndex, int currentPlayingPosition) + { + State state = new State(); + for (DownloadFile downloadFile : songs) + { + state.songs.add(downloadFile.getSong()); + } + state.currentPlayingIndex = currentPlayingIndex; + state.currentPlayingPosition = currentPlayingPosition; + + Log.i(TAG, String.format("Serialized currentPlayingIndex: %d, currentPlayingPosition: %d", state.currentPlayingIndex, state.currentPlayingPosition)); + FileUtil.serialize(context, state, Constants.FILENAME_DOWNLOADS_SER); + } + + public void deserializeDownloadQueue(Consumer afterDeserialized) + { + new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, afterDeserialized); + } + + public void deserializeDownloadQueueNow(Consumer afterDeserialized) + { + State state = FileUtil.deserialize(context, Constants.FILENAME_DOWNLOADS_SER); + if (state == null) return; + Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + afterDeserialized.accept(state); + } + + private class SerializeTask extends AsyncTask + { + @Override + protected Void doInBackground(Object... params) + { + if (lock.tryLock()) + { + try + { + Thread.currentThread().setName("SerializeTask"); + serializeDownloadQueueNow((Iterable)params[0], (int)params[1], (int)params[2]); + } + finally + { + lock.unlock(); + } + } + return null; + } + } + + private class DeserializeTask extends AsyncTask + { + @Override + protected Void doInBackground(Object... params) + { + try + { + Thread.currentThread().setName("DeserializeTask"); + lock.lock(); + deserializeDownloadQueueNow((Consumer)params[0]); + setup.set(true); + } + finally + { + lock.unlock(); + } + + return null; + } + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java deleted file mode 100644 index 309eb8e4..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceImpl.java +++ /dev/null @@ -1,2398 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -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 androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; -import android.widget.SeekBar; - -import org.koin.java.standalone.KoinJavaComponent; -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.activity.DownloadActivity; -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.MusicDirectory.Entry; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.domain.RepeatMode; -import org.moire.ultrasonic.domain.UserInfo; -import org.moire.ultrasonic.featureflags.Feature; -import org.moire.ultrasonic.featureflags.FeatureStorage; -import org.moire.ultrasonic.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.LRUCache; -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.Collections; -import java.util.Iterator; -import java.util.List; - -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; - -/** - * @author Sindre Mehus, Joshua Bahnsen - * @version $Id$ - */ -public class DownloadServiceImpl extends Service implements DownloadService -{ - private static final String TAG = DownloadServiceImpl.class.getSimpleName(); - - public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY"; - public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE"; - public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE"; - public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP"; - public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"; - public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"; - - private static final String NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"; - private static final String NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"; - private static final int NOTIFICATION_ID = 3033; - - private final IBinder binder = new SimpleServiceBinder(this); - private Looper mediaPlayerLooper; - private MediaPlayer mediaPlayer; - private MediaPlayer nextMediaPlayer; - private boolean nextSetup; - private final List downloadList = new ArrayList(); - private final List backgroundDownloadList = new ArrayList(); - private final Handler handler = new Handler(); - private Handler mediaPlayerHandler; - private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); - private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); - - private final LRUCache downloadFileCache = new LRUCache(100); - private final List cleanupCandidates = new ArrayList(); - private final Scrobbler scrobbler = new Scrobbler(); - private final JukeboxService jukeboxService = new JukeboxService(this); - - private DownloadFile currentPlaying; - private DownloadFile nextPlaying; - private DownloadFile currentDownloading; - private CancellableTask bufferTask; - private CancellableTask nextPlayingTask; - private PlayerState playerState = IDLE; - private PlayerState nextPlayerState = IDLE; - private boolean shufflePlay; - private long revision; - private static DownloadService instance; - private String suggestedPlaylistName; - private PowerManager.WakeLock wakeLock; - private boolean keepScreenOn; - private int cachedPosition; - - private static boolean equalizerAvailable; - private static boolean visualizerAvailable; - private EqualizerController equalizerController; - private VisualizerController visualizerController; - private boolean showVisualization; - private boolean jukeboxEnabled; - private PositionCache positionCache; - private StreamProxy proxy; - public RemoteControlClient remoteControlClient; - private AudioManager audioManager; - private int secondaryProgress = -1; - private boolean autoPlayStart; - private final static int lockScreenBitmapSize = 500; - - private boolean isInForeground = false; - 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; - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() - { - super.onCreate(); - - new Thread(new Runnable() - { - @Override - public void run() - { - Thread.currentThread().setName("DownloadServiceImpl"); - - Looper.prepare(); - - if (mediaPlayer != null) - { - mediaPlayer.release(); - } - - mediaPlayer = new MediaPlayer(); - mediaPlayer.setWakeMode(DownloadServiceImpl.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(); - - audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - setUpRemoteControlClient(); - - if (equalizerAvailable) - { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) - { - equalizerController = null; - } - 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); - - instance = this; - lifecycleSupport.onCreate(); - - // Create Notification Channel - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - //The suggested importance of a startForeground service notification is IMPORTANCE_LOW - NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); - channel.setLightColor(android.R.color.holo_blue_dark); - channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(channel); - } - - // We should use a single notification builder, otherwise the notification may not be updated - notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID); - - Log.i(TAG, "DownloadServiceImpl created"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) - { - super.onStartCommand(intent, flags, startId); - - lifecycleSupport.onStart(intent); - Log.i(TAG, "DownloadServiceImpl started with intent"); - return START_NOT_STICKY; - } - - @Override - public void onDestroy() - { - super.onDestroy(); - - try - { - instance = null; - lifecycleSupport.onDestroy(); - 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(); - } - catch (Throwable ignored) - { - } - - Log.i(TAG, "DownloadServiceImpl stopped"); - } - - public static DownloadService getInstance() - { - return instance; - } - - @Override - public IBinder onBind(Intent intent) - { - return binder; - } - - @Override - public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist) - { - shufflePlay = 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(this, song, save); - downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); - offset++; - } - - revision++; - } - else - { - int size = size(); - int index = getCurrentPlayingIndex(); - - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(this, song, save); - downloadList.add(downloadFile); - } - - if (!autoplay && (size - 1) == index) - { - setNextPlaying(); - } - - revision++; - } - - updateJukeboxPlaylist(); - - if (shuffle) shuffle(); - - if (autoplay) - { - play(0); - } - else - { - if (currentPlaying == null) - { - currentPlaying = downloadList.get(0); - currentPlaying.setPlaying(true); - } - - checkDownloads(); - } - - lifecycleSupport.serializeDownloadQueue(); - } - - @Override - public synchronized void downloadBackground(List songs, boolean save) - { - for (MusicDirectory.Entry song : songs) - { - DownloadFile downloadFile = new DownloadFile(this, song, save); - backgroundDownloadList.add(downloadFile); - } - - revision++; - - checkDownloads(); - lifecycleSupport.serializeDownloadQueue(); - } - - private void updateJukeboxPlaylist() - { - if (jukeboxEnabled) - { - jukeboxService.updatePlaylist(); - } - } - - @Override - public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist) - { - download(songs, false, false, false, false, newPlaylist); - - if (currentPlayingIndex != -1) - { - while (mediaPlayer == null) - { - Util.sleepQuietly(50L); - } - - play(currentPlayingIndex, autoPlayStart); - - if (currentPlaying != null) - { - if (autoPlay && jukeboxEnabled) - { - jukeboxService.skip(getCurrentPlayingIndex(), currentPlayingPosition / 1000); - } - else - { - if (currentPlaying.isCompleteFileAvailable()) - { - doPlay(currentPlaying, currentPlayingPosition, autoPlay); - } - } - } - - autoPlayStart = false; - } - } - - @Override - public void stopJukeboxService() - { - jukeboxService.stopJukeboxService(); - } - - @Override - public void startJukeboxService() - { - jukeboxService.startJukeboxService(); - } - - @Override - public synchronized void setShufflePlayEnabled(boolean enabled) - { - shufflePlay = enabled; - if (shufflePlay) - { - clear(); - checkDownloads(); - } - } - - @Override - public boolean isShufflePlayEnabled() - { - return shufflePlay; - } - - @Override - public synchronized void shuffle() - { - Collections.shuffle(downloadList); - if (currentPlaying != null) - { - downloadList.remove(getCurrentPlayingIndex()); - downloadList.add(0, currentPlaying); - } - revision++; - lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - setNextPlaying(); - } - - @Override - public RepeatMode getRepeatMode() - { - return Util.getRepeatMode(this); - } - - @Override - public void setRepeatMode(RepeatMode repeatMode) - { - Util.setRepeatMode(this, repeatMode); - setNextPlaying(); - } - - @Override - public boolean getKeepScreenOn() - { - return keepScreenOn; - } - - @Override - public void setKeepScreenOn(boolean keepScreenOn) - { - this.keepScreenOn = keepScreenOn; - } - - @Override - public boolean getShowVisualization() - { - return showVisualization; - } - - @Override - public void setShowVisualization(boolean showVisualization) - { - this.showVisualization = showVisualization; - } - - @Override - public synchronized DownloadFile forSong(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(this, song, false); - downloadFileCache.put(song, downloadFile); - } - return downloadFile; - } - - @Override - public synchronized void clear() - { - clear(true); - } - - @Override - public synchronized void clearBackground() - { - if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) - { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - backgroundDownloadList.clear(); - } - - @Override - public synchronized void clearIncomplete() - { - reset(); - Iterator iterator = downloadList.iterator(); - - while (iterator.hasNext()) - { - DownloadFile downloadFile = iterator.next(); - if (!downloadFile.isCompleteFileAvailable()) - { - iterator.remove(); - } - } - - lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - } - - @Override - public synchronized int size() - { - return downloadList.size(); - } - - public synchronized void clear(boolean serialize) - { - reset(); - downloadList.clear(); - revision++; - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - setCurrentPlaying(null); - - if (serialize) - { - lifecycleSupport.serializeDownloadQueue(); - } - updateJukeboxPlaylist(); - setNextPlaying(); - } - - @Override - public synchronized void remove(int which) - { - downloadList.remove(which); - } - - @Override - public synchronized void remove(DownloadFile downloadFile) - { - if (downloadFile == currentDownloading) - { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - if (downloadFile == currentPlaying) - { - reset(); - setCurrentPlaying(null); - } - downloadList.remove(downloadFile); - backgroundDownloadList.remove(downloadFile); - revision++; - lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - if (downloadFile == nextPlaying) - { - setNextPlaying(); - } - } - - @Override - public synchronized void delete(List songs) - { - for (MusicDirectory.Entry song : songs) - { - forSong(song).delete(); - } - } - - @Override - public synchronized void unpin(List songs) - { - for (MusicDirectory.Entry song : songs) - { - forSong(song).unpin(); - } - } - - synchronized void setCurrentPlaying(int currentPlayingIndex) - { - try - { - setCurrentPlaying(downloadList.get(currentPlayingIndex)); - } - catch (IndexOutOfBoundsException x) - { - // Ignored - } - } - - synchronized void setCurrentPlaying(DownloadFile currentPlaying) - { - this.currentPlaying = currentPlaying; - - if (currentPlaying != null) - { - Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - Util.broadcastA2dpMetaDataChange(this, instance); - } - else - { - Util.broadcastNewTrackInfo(this, null); - Util.broadcastA2dpMetaDataChange(this, null); - } - - updateRemoteControl(); - - // Update widget - UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, true); - UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, this, playerState == PlayerState.STARTED, false); - SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); - - if (currentPlaying != null) - { - if (tabInstance != null) { - updateNotification(); - tabInstance.showNowPlaying(); - } - } - else - { - if (tabInstance != null) - { - stopForeground(true); - clearRemoteControl(); - isInForeground = false; - tabInstance.hideNowPlaying(); - } - } - } - - synchronized void setNextPlaying() - { - boolean gaplessPlayback = Util.getGaplessPlaybackPreference(DownloadServiceImpl.this); - - if (!gaplessPlayback) - { - nextPlaying = null; - nextPlayerState = IDLE; - return; - } - - int index = getCurrentPlayingIndex(); - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - index += 1; - break; - case ALL: - index = (index + 1) % size(); - break; - case SINGLE: - break; - default: - break; - } - } - - nextSetup = false; - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - - if (index < size() && index != -1) - { - nextPlaying = downloadList.get(index); - nextPlayingTask = new CheckCompletionTask(nextPlaying); - nextPlayingTask.start(); - } - else - { - nextPlaying = null; - setNextPlayerState(IDLE); - } - } - - @Override - public synchronized int getCurrentPlayingIndex() - { - return downloadList.indexOf(currentPlaying); - } - - @Override - public DownloadFile getCurrentPlaying() - { - return currentPlaying; - } - - @Override - public DownloadFile getCurrentDownloading() - { - return currentDownloading; - } - - @Override - public List getSongs() - { - return downloadList; - } - - @Override - public long getDownloadListDuration() - { - long totalDuration = 0; - - for (DownloadFile downloadFile : 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(downloadList); - temp.addAll(backgroundDownloadList); - return temp; - } - - @Override - public List getBackgroundDownloads() - { - return backgroundDownloadList; - } - - /** - * Plays either the current song (resume) or the first/next one in queue. - */ - public synchronized void play() - { - int current = getCurrentPlayingIndex(); - if (current == -1) - { - play(0); - } - else - { - play(current); - } - } - - @Override - public synchronized void play(int index) - { - play(index, true); - } - - private synchronized void play(int index, boolean start) - { - updateRemoteControl(); - - if (index < 0 || index >= size()) - { - resetPlayback(); - } - else - { - if (nextPlayingTask != null) - { - nextPlayingTask.cancel(); - nextPlayingTask = null; - } - - setCurrentPlaying(index); - - if (start) - { - if (jukeboxEnabled) - { - jukeboxService.skip(getCurrentPlayingIndex(), 0); - setPlayerState(STARTED); - } - else - { - bufferAndPlay(); - } - } - - checkDownloads(); - setNextPlaying(); - } - } - - private synchronized void resetPlayback() - { - reset(); - setCurrentPlaying(null); - lifecycleSupport.serializeDownloadQueue(); - } - - 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; - } - } - - /** - * Plays or resumes the playback, depending on the current player state. - */ - @Override - public synchronized void togglePlayPause() - { - if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) - { - start(); - } - else if (playerState == IDLE) - { - autoPlayStart = true; - play(); - } - else if (playerState == STARTED) - { - pause(); - } - } - - @Override - public synchronized void seekTo(int position) - { - try - { - if (jukeboxEnabled) - { - jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); - } - else - { - mediaPlayer.seekTo(position); - cachedPosition = position; - - updateRemoteControl(); - } - } - catch (Exception x) - { - handleError(x); - } - } - - @Override - public synchronized void previous() - { - int index = getCurrentPlayingIndex(); - if (index == -1) - { - return; - } - - // Restart song if played more than five seconds. - if (getPlayerPosition() > 5000 || index == 0) - { - play(index); - } - else - { - play(index - 1); - } - } - - @Override - public synchronized void next() - { - int index = getCurrentPlayingIndex(); - if (index != -1) - { - play(index + 1); - } - } - - private void onSongCompleted() - { - int index = getCurrentPlayingIndex(); - - if (currentPlaying != null) - { - final Entry song = currentPlaying.getSong(); - - if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(this)) - { - MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); - try - { - musicService.deleteBookmark(song.getId(), DownloadServiceImpl.this, null); - } - catch (Exception ignored) - { - - } - } - } - - if (index != -1) - { - switch (getRepeatMode()) - { - case OFF: - if (index + 1 < 0 || index + 1 >= size()) - { - if (Util.getShouldClearPlaylist(this)) - { - clear(); - } - - resetPlayback(); - break; - } - - play(index + 1); - break; - case ALL: - play((index + 1) % size()); - break; - case SINGLE: - play(index); - break; - default: - break; - } - } - } - - @Override - public synchronized void pause() - { - try - { - if (playerState == STARTED) - { - if (jukeboxEnabled) - { - jukeboxService.stop(); - } - else - { - mediaPlayer.pause(); - } - setPlayerState(PAUSED); - } - } - catch (Exception x) - { - handleError(x); - } - } - - @Override - public synchronized void stop() - { - try - { - if (playerState == STARTED) - { - if (jukeboxEnabled) - { - jukeboxService.stop(); - } - else - { - mediaPlayer.pause(); - } - setPlayerState(STOPPED); - } - else - { - setPlayerState(STOPPED); - } - } - catch (Exception x) - { - handleError(x); - } - } - - @Override - public synchronized void start() - { - try - { - if (jukeboxEnabled) - { - jukeboxService.start(); - } - else - { - mediaPlayer.start(); - } - setPlayerState(STARTED); - } - catch (Exception x) - { - handleError(x); - } - } - - @Override - 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); - } - } - - @Override - public synchronized int getPlayerPosition() - { - try - { - if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) - { - return 0; - } - - return jukeboxEnabled ? jukeboxService.getPositionSeconds() * 1000 : cachedPosition; - } - catch (Exception x) - { - handleError(x); - return 0; - } - } - - @Override - 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; - } - - @Override - public PlayerState getPlayerState() - { - return playerState; - } - - synchronized void setPlayerState(PlayerState playerState) - { - Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), playerState.name(), currentPlaying)); - - this.playerState = playerState; - - if (this.playerState == PAUSED) - { - lifecycleSupport.serializeDownloadQueue(); - } - - if (this.playerState == PlayerState.STARTED) - { - Util.requestAudioFocus(this); - } - - boolean showWhenPaused = (this.playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(this)); - boolean show = this.playerState == PlayerState.STARTED || showWhenPaused; - - Util.broadcastPlaybackStatusChange(this, this.playerState); - Util.broadcastA2dpPlayStatusChange(this, this.playerState, instance); - - if (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED) - { - // Set remote control - updateRemoteControl(); - } - - // Update widget - UltraSonicAppWidgetProvider4x1.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x2.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, true); - UltraSonicAppWidgetProvider4x3.getInstance().notifyChange(this, this, this.playerState == PlayerState.STARTED, false); - UltraSonicAppWidgetProvider4x4.getInstance().notifyChange(this, this, this.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 (this.playerState == PlayerState.STARTED || this.playerState == PlayerState.PAUSED) - { - updateNotification(); - tabInstance.showNowPlaying(); - } - } - } - else - { - if (tabInstance != null) - { - stopForeground(true); - clearRemoteControl(); - isInForeground = false; - tabInstance.hideNowPlaying(); - } - } - - if (this.playerState == STARTED) - { - scrobbler.scrobble(this, currentPlaying, false); - } - else if (this.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)", this.playerState.name(), PlayerState.COMPLETED, currentPlaying)); - this.playerState = PlayerState.COMPLETED; - - if (positionCache != null) - { - positionCache.stop(); - positionCache = null; - } - - scrobbler.scrobble(this, currentPlaying, true); - } - - private synchronized void setNextPlayerState(PlayerState playerState) - { - Log.i(TAG, String.format("Next: %s -> %s (%s)", this.nextPlayerState.name(), playerState.name(), nextPlaying)); - this.nextPlayerState = playerState; - } - - @Override - public void setSuggestedPlaylistName(String name) - { - this.suggestedPlaylistName = name; - } - - @Override - public String getSuggestedPlaylistName() - { - return suggestedPlaylistName; - } - - @Override - public boolean getEqualizerAvailable() - { - return equalizerAvailable; - } - - @Override - public boolean getVisualizerAvailable() - { - return visualizerAvailable; - } - - @Override - public EqualizerController getEqualizerController() - { - if (equalizerAvailable && equalizerController == null) - { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) - { - equalizerController = null; - } - else - { - equalizerController.loadSettings(); - } - } - return equalizerController; - } - - @Override - public VisualizerController getVisualizerController() - { - if (visualizerAvailable && visualizerController == null) - { - visualizerController = new VisualizerController(mediaPlayer); - if (!visualizerController.isAvailable()) - { - visualizerController = null; - } - } - return visualizerController; - } - - @Override - public boolean isJukeboxEnabled() - { - return jukeboxEnabled; - } - - @Override - public boolean isJukeboxAvailable() - { - MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); - - try - { - String username = Util.getUserName(DownloadServiceImpl.this, Util.getActiveServer(DownloadServiceImpl.this)); - UserInfo user = musicService.getUser(username, DownloadServiceImpl.this, null); - return user.getJukeboxRole(); - } - catch (Exception e) - { - Log.w(TAG, "Error getting user information", e); - } - - return false; - } - - @Override - public boolean isSharingAvailable() - { - MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); - - try - { - String username = Util.getUserName(DownloadServiceImpl.this, Util.getActiveServer(DownloadServiceImpl.this)); - UserInfo user = musicService.getUser(username, DownloadServiceImpl.this, null); - return user.getShareRole(); - } - catch (Exception e) - { - Log.w(TAG, "Error getting user information", e); - } - - return false; - } - - @Override - public void setJukeboxEnabled(boolean jukeboxEnabled) - { - this.jukeboxEnabled = jukeboxEnabled; - jukeboxService.setEnabled(jukeboxEnabled); - - if (jukeboxEnabled) - { - jukeboxService.startJukeboxService(); - - reset(); - - // Cancel current download, if necessary. - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - } - } - else - { - jukeboxService.stopJukeboxService(); - } - } - - @Override - public void adjustJukeboxVolume(boolean up) - { - jukeboxService.adjustVolume(up); - } - - @SuppressLint("NewApi") - 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); - } - 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(); - } - } - - private synchronized void bufferAndPlay() - { - if (playerState != PREPARED) - { - reset(); - - bufferTask = new BufferTask(currentPlaying, 0); - bufferTask.start(); - } - else - { - doPlay(currentPlaying, 0, true); - } - } - - private 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(this); - 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(DownloadServiceImpl.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 (DownloadServiceImpl.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); - } - } - - lifecycleSupport.serializeDownloadQueue(); - } - }); - - 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(DownloadServiceImpl.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(DownloadServiceImpl.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() - { - @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))) - { - setPlayerStateCompleted(); - - if (Util.getGaplessPlaybackPreference(DownloadServiceImpl.this) && nextPlaying != null && nextPlayerState == PlayerState.PREPARED) - { - if (!nextSetup) - { - playNext(); - } - else - { - nextSetup = false; - playNext(); - } - } - else - { - onSongCompleted(); - } - - return; - } - - synchronized (DownloadServiceImpl.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(); - } - } - } - }); - } - - @Override - public void setVolume(float volume) - { - if (mediaPlayer != null) - { - mediaPlayer.setVolume(volume, volume); - } - } - - @Override - public synchronized void swap(boolean mainList, int from, int to) - { - List list = mainList ? downloadList : backgroundDownloadList; - int max = list.size(); - - if (to >= max) - { - to = max - 1; - } - else if (to < 0) - { - to = 0; - } - - int currentPlayingIndex = getCurrentPlayingIndex(); - DownloadFile movedSong = list.remove(from); - list.add(to, movedSong); - - if (jukeboxEnabled && mainList) - { - updateJukeboxPlaylist(); - } - else if (mainList && (movedSong == nextPlaying || (currentPlayingIndex + 1) == to)) - { - // Moving next playing or moving a song to be next playing - setNextPlaying(); - } - } - - 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); - } - - setPlayerState(IDLE); - } - - private void handleErrorNext(Exception x) - { - Log.w(TAG, String.format("Next Media player error: %s", x), x); - nextMediaPlayer.reset(); - setNextPlayerState(IDLE); - } - - protected synchronized void checkDownloads() - { - if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) - { - return; - } - - if (shufflePlay) - { - checkShufflePlay(); - } - - if (jukeboxEnabled || !Util.isNetworkConnected(this)) - { - return; - } - - if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) - { - return; - } - - // Need to download current playing? - if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) - { - // Cancel current download, if necessary. - if (currentDownloading != null) - { - currentDownloading.cancelDownload(); - } - - currentDownloading = currentPlaying; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - } - - // Find a suitable target for download. - else - { - if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) - { - currentDownloading = null; - int n = size(); - - int preloaded = 0; - - if (n != 0) - { - int start = 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(this)) - { - currentDownloading = downloadFile; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - if (i == (start + 1)) - { - setNextPlayerState(DOWNLOADING); - } - break; - } - } - else if (currentPlaying != downloadFile) - { - preloaded++; - } - - i = (i + 1) % n; - } while (i != start); - } - - if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || 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(this)) - { - Util.scanMedia(this, 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(); - } - - private synchronized void checkShufflePlay() - { - // Get users desired random playlist size - int listSize = Util.getMaxSongs(this); - boolean wasEmpty = downloadList.isEmpty(); - - long revisionBefore = revision; - - // First, ensure that list is at least 20 songs long. - int size = size(); - if (size < listSize) - { - for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) - { - DownloadFile downloadFile = new DownloadFile(this, song, false); - downloadList.add(downloadFile); - revision++; - } - } - - int currIndex = 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(this, song, false)); - downloadList.get(0).cancelDownload(); - downloadList.remove(0); - revision++; - } - } - - if (revisionBefore != revision) - { - updateJukeboxPlaylist(); - } - - if (wasEmpty && !downloadList.isEmpty()) - { - play(0); - } - } - - @Override - public long getDownloadListUpdateRevision() - { - return revision; - } - - private synchronized void cleanup() - { - Iterator iterator = cleanupCandidates.iterator(); - while (iterator.hasNext()) - { - DownloadFile downloadFile = iterator.next(); - if (downloadFile != currentPlaying && downloadFile != currentDownloading) - { - if (downloadFile.cleanup()) - { - iterator.remove(); - } - } - } - } - - @Override - public void updateNotification() - { - 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()); - } - else { - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(this); - notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification()); - } - Log.w(TAG, "--- Updated notification"); - } - else { - startForeground(NOTIFICATION_ID, buildForegroundNotification()); - isInForeground = true; - Log.w(TAG, "--- Created Foreground notification"); - } - } - } - - @SuppressWarnings("IconColors") - private Notification buildForegroundNotification() { - notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); - - notificationBuilder.setAutoCancel(false); - notificationBuilder.setOngoing(true); - notificationBuilder.setOnlyAlertOnce(true); - notificationBuilder.setWhen(System.currentTimeMillis()); - notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); - - RemoteViews contentView = new RemoteViews(this.getPackageName(), R.layout.notification); - Util.linkButtons(this, contentView, false); - RemoteViews bigView = new RemoteViews(this.getPackageName(), R.layout.notification_large); - Util.linkButtons(this, bigView, false); - - notificationBuilder.setContent(contentView); - - Intent notificationIntent = new Intent(this, DownloadActivity.class); - notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0)); - - if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) { - contentView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); - bigView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); - } else if (playerState == PlayerState.STARTED) { - contentView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); - bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); - } - - if (currentPlaying != null) { - final Entry song = currentPlaying.getSong(); - final String title = song.getTitle(); - final String text = song.getArtist(); - final String album = song.getAlbum(); - final int rating = song.getUserRating() == null ? 0 : song.getUserRating(); - final int imageSize = Util.getNotificationImageSize(this); - - try { - final Bitmap nowPlayingImage = FileUtil.getAlbumArtBitmap(this, currentPlaying.getSong(), imageSize, true); - if (nowPlayingImage == null) { - contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } else { - contentView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); - bigView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); - } - } catch (Exception x) { - Log.w(TAG, "Failed to get notification cover art", x); - contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } - - - contentView.setTextViewText(R.id.trackname, title); - bigView.setTextViewText(R.id.trackname, title); - contentView.setTextViewText(R.id.artist, text); - bigView.setTextViewText(R.id.artist, text); - contentView.setTextViewText(R.id.album, album); - bigView.setTextViewText(R.id.album, album); - - boolean useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING); - if (!useFiveStarRating) - bigView.setViewVisibility(R.id.notification_rating, View.INVISIBLE); - else { - bigView.setImageViewResource(R.id.notification_five_star_1, rating > 0 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_2, rating > 1 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_3, rating > 2 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_4, rating > 3 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - bigView.setImageViewResource(R.id.notification_five_star_5, rating > 4 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); - } - } - - Notification notification = notificationBuilder.build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - notification.bigContentView = bigView; - } - - return notification; - } - - public void setSongRating(final int rating) - { - if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) - return; - - if (currentPlaying == null) - return; - - final Entry song = currentPlaying.getSong(); - song.setUserRating(rating); - - new Thread(new Runnable() - { - @Override - public void run() - { - final MusicService musicService = MusicServiceFactory.getMusicService(DownloadServiceImpl.this); - - try - { - musicService.setRating(song.getId(), rating, DownloadServiceImpl.this, null); - } - catch (Exception e) - { - Log.e(TAG, e.getMessage(), e); - } - } - }).start(); - - updateNotification(); - } - - 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(DownloadServiceImpl.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(DownloadServiceImpl.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 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 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); - } - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java deleted file mode 100644 index 265bce86..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadServiceLifecycleSupport.java +++ /dev/null @@ -1,441 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.media.AudioManager; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.util.Log; -import android.view.KeyEvent; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.util.CacheCleaner; -import org.moire.ultrasonic.util.FileUtil; -import org.moire.ultrasonic.util.Util; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * @author Sindre Mehus - */ -public class DownloadServiceLifecycleSupport -{ - - private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); - private static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser"; - - private final DownloadServiceImpl downloadService; - private ScheduledExecutorService executorService; - private BroadcastReceiver headsetEventReceiver; - private BroadcastReceiver ejectEventReceiver; - private PhoneStateListener phoneStateListener; - private boolean externalStorageAvailable = true; - private Lock lock = new ReentrantLock(); - private final AtomicBoolean setup = new AtomicBoolean(false); - - /** - * This receiver manages the intent that could come from other applications. - */ - private BroadcastReceiver intentReceiver = new BroadcastReceiver() - { - @Override - public void onReceive(Context context, Intent intent) - { - String action = intent.getAction(); - Log.i(TAG, "intentReceiver.onReceive: " + action); - if (DownloadServiceImpl.CMD_PLAY.equals(action)) - { - downloadService.play(); - } - else if (DownloadServiceImpl.CMD_NEXT.equals(action)) - { - downloadService.next(); - } - else if (DownloadServiceImpl.CMD_PREVIOUS.equals(action)) - { - downloadService.previous(); - } - else if (DownloadServiceImpl.CMD_TOGGLEPAUSE.equals(action)) - { - downloadService.togglePlayPause(); - } - else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) - { - downloadService.pause(); - } - else if (DownloadServiceImpl.CMD_STOP.equals(action)) - { - downloadService.pause(); - downloadService.seekTo(0); - } - } - }; - - - public DownloadServiceLifecycleSupport(DownloadServiceImpl downloadService) - { - this.downloadService = downloadService; - } - - public void onCreate() - { - Runnable downloadChecker = new Runnable() - { - @Override - public void run() - { - try - { - downloadService.checkDownloads(); - } - catch (Throwable x) - { - Log.e(TAG, "checkDownloads() failed.", x); - } - } - }; - - executorService = Executors.newSingleThreadScheduledExecutor(); - executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); - - registerHeadsetReceiver(); - - // Stop when SD card is ejected. - ejectEventReceiver = new BroadcastReceiver() - { - @Override - public void onReceive(Context context, Intent intent) - { - externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); - if (!externalStorageAvailable) - { - Log.i(TAG, "External media is ejecting. Stopping playback."); - downloadService.reset(); - } - else - { - Log.i(TAG, "External media is available."); - } - } - }; - IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); - ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); - ejectFilter.addDataScheme("file"); - downloadService.registerReceiver(ejectEventReceiver, ejectFilter); - - // React to media buttons. - Util.registerMediaButtonEventReceiver(downloadService); - - // Pause temporarily on incoming phone calls. - //phoneStateListener = new MyPhoneStateListener(); - //TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); - //telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - - // Register the handler for outside intents. - IntentFilter commandFilter = new IntentFilter(); - commandFilter.addAction(DownloadServiceImpl.CMD_PLAY); - commandFilter.addAction(DownloadServiceImpl.CMD_TOGGLEPAUSE); - commandFilter.addAction(DownloadServiceImpl.CMD_PAUSE); - commandFilter.addAction(DownloadServiceImpl.CMD_STOP); - commandFilter.addAction(DownloadServiceImpl.CMD_PREVIOUS); - commandFilter.addAction(DownloadServiceImpl.CMD_NEXT); - downloadService.registerReceiver(intentReceiver, commandFilter); - - int instance = Util.getActiveServer(downloadService); - downloadService.setJukeboxEnabled(Util.getJukeboxEnabled(downloadService, instance)); - - deserializeDownloadQueue(); - - new CacheCleaner(downloadService, downloadService).clean(); - } - - private void registerHeadsetReceiver() { - // Pause when headset is unplugged. - final SharedPreferences sp = Util.getPreferences(downloadService); - final String spKey = downloadService - .getString(R.string.settings_playback_resume_play_on_headphones_plug); - - headsetEventReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final Bundle extras = intent.getExtras(); - - if (extras == null) { - return; - } - - Log.i(TAG, String.format("Headset event for: %s", extras.get("name"))); - final int state = extras.getInt("state"); - if (state == 0) { - if (!downloadService.isJukeboxEnabled()) { - downloadService.pause(); - } - } else if (state == 1) { - if (!downloadService.isJukeboxEnabled() && - sp.getBoolean(spKey, false) && - downloadService.getPlayerState() == PlayerState.PAUSED) { - downloadService.start(); - } - } - } - }; - @SuppressLint("InlinedApi") - IntentFilter headsetIntentFilter = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ? - new IntentFilter(AudioManager.ACTION_HEADSET_PLUG) : - new IntentFilter(Intent.ACTION_HEADSET_PLUG); - downloadService.registerReceiver(headsetEventReceiver, headsetIntentFilter); - } - - public void onStart(Intent intent) - { - if (intent != null && intent.getExtras() != null) - { - KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); - if (event != null) - { - handleKeyEvent(event); - } - } - } - - public void onDestroy() - { - executorService.shutdown(); - serializeDownloadQueueNow(); - downloadService.clear(false); - downloadService.unregisterReceiver(ejectEventReceiver); - downloadService.unregisterReceiver(headsetEventReceiver); - downloadService.unregisterReceiver(intentReceiver); - - //TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); - //telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); - } - - public boolean isExternalStorageAvailable() - { - return externalStorageAvailable; - } - - public void serializeDownloadQueue() - { - if (!setup.get()) - { - return; - } - - new SerializeTask().execute(); - } - - public void serializeDownloadQueueNow() - { - Iterable songs = new ArrayList(downloadService.getSongs()); - State state = new State(); - for (DownloadFile downloadFile : songs) - { - state.songs.add(downloadFile.getSong()); - } - state.currentPlayingIndex = downloadService.getCurrentPlayingIndex(); - state.currentPlayingPosition = downloadService.getPlayerPosition(); - - Log.i(TAG, String.format("Serialized currentPlayingIndex: %d, currentPlayingPosition: %d", state.currentPlayingIndex, state.currentPlayingPosition)); - FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); - } - - private void deserializeDownloadQueue() - { - new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void deserializeDownloadQueueNow() - { - State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER); - if (state == null) - { - return; - } - Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); - // TODO: here the autoPlay = false creates problems when Ultrasonic is started by a Play MediaButton as the player won't start this way. - 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. - serializeDownloadQueue(); - } - - private void handleKeyEvent(KeyEvent event) - { - if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) - { - return; - } - - switch (event.getKeyCode()) - { - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - case KeyEvent.KEYCODE_HEADSETHOOK: - downloadService.togglePlayPause(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - downloadService.previous(); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) - { - downloadService.next(); - } - break; - case KeyEvent.KEYCODE_MEDIA_STOP: - downloadService.stop(); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (downloadService.getPlayerState() == PlayerState.IDLE) - { - downloadService.play(); - } - else if (downloadService.getPlayerState() != PlayerState.STARTED) - { - downloadService.start(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - downloadService.pause(); - break; - case KeyEvent.KEYCODE_1: - downloadService.setSongRating(1); - break; - case KeyEvent.KEYCODE_2: - downloadService.setSongRating(2); - break; - case KeyEvent.KEYCODE_3: - downloadService.setSongRating(3); - break; - case KeyEvent.KEYCODE_4: - downloadService.setSongRating(4); - break; - case KeyEvent.KEYCODE_5: - downloadService.setSongRating(5); - break; - default: - break; - } - } - - /** - * Logic taken from packages/apps/Music. Will pause when an incoming - * call rings or if a call (incoming or outgoing) is connected. - */ - private class MyPhoneStateListener extends PhoneStateListener - { - private boolean resumeAfterCall; - - @Override - public void onCallStateChanged(int state, String incomingNumber) - { - switch (state) - { - case TelephonyManager.CALL_STATE_RINGING: - case TelephonyManager.CALL_STATE_OFFHOOK: - if (downloadService.getPlayerState() == PlayerState.STARTED && !downloadService.isJukeboxEnabled()) - { - resumeAfterCall = true; - downloadService.pause(); - } - break; - case TelephonyManager.CALL_STATE_IDLE: - if (resumeAfterCall) - { - resumeAfterCall = false; - downloadService.start(); - } - break; - default: - break; - } - } - } - - private static class State implements Serializable - { - private static final long serialVersionUID = -6346438781062572270L; - - private List songs = new ArrayList(); - private int currentPlayingIndex; - private int currentPlayingPosition; - } - - private class SerializeTask extends AsyncTask - { - @Override - protected Void doInBackground(Void... params) - { - if (lock.tryLock()) - { - try - { - Thread.currentThread().setName("SerializeTask"); - serializeDownloadQueueNow(); - } - finally - { - lock.unlock(); - } - } - return null; - } - } - - private class DeserializeTask extends AsyncTask - { - @Override - protected Void doInBackground(Void... params) - { - try - { - Thread.currentThread().setName("DeserializeTask"); - lock.lock(); - deserializeDownloadQueueNow(); - setup.set(true); - } - finally - { - lock.unlock(); - } - - return null; - } - } -} \ No newline at end of file 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..285870c3 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Downloader.java @@ -0,0 +1,453 @@ +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; + +/** + * This class is responsible for maintaining the playlist and downloading + * its items from the network to the filesystem. + */ +public class Downloader +{ + private static final String TAG = Downloader.class.getSimpleName(); + + public final List downloadList = new ArrayList<>(); + public final List backgroundDownloadList = new ArrayList<>(); + public DownloadFile currentDownloading; + + private final ShufflePlayBuffer shufflePlayBuffer; + private final ExternalStorageMonitor externalStorageMonitor; + private final LocalMediaPlayer localMediaPlayer; + private final Context context; + + // TODO: This is a circular reference, try to remove + private Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); + + private final List cleanupCandidates = new ArrayList<>(); + private final LRUCache downloadFileCache = new LRUCache<>(100); + private ScheduledExecutorService executorService; + private long revision; + + public Downloader(Context context, ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor, + LocalMediaPlayer localMediaPlayer) + { + this.context = context; + this.shufflePlayBuffer = shufflePlayBuffer; + this.externalStorageMonitor = externalStorageMonitor; + this.localMediaPlayer = localMediaPlayer; + } + + 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() + { + stop(); + clear(); + clearBackground(); + Log.i(TAG, "Downloader destroyed"); + } + + public void stop() + { + executorService.shutdown(); + Log.i(TAG, "Downloader stopped"); + } + + public synchronized void checkDownloads() + { + if (!Util.isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable()) + { + return; + } + + if (shufflePlayBuffer.isEnabled) + { + checkShufflePlay(context); + } + + if (jukeboxMediaPlayer.getValue().isEnabled() || !Util.isNetworkConnected(context)) + { + return; + } + + if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) + { + return; + } + + // Need to download current playing? + if (localMediaPlayer.currentPlaying != null && localMediaPlayer.currentPlaying != currentDownloading && !localMediaPlayer.currentPlaying.isWorkDone()) + { + // Cancel current download, if necessary. + if (currentDownloading != null) + { + currentDownloading.cancelDownload(); + } + + currentDownloading = localMediaPlayer.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 = localMediaPlayer.currentPlaying == null ? 0 : getCurrentPlayingIndex(); + if (start == -1) start = 0; + + int i = start; + // Check all DownloadFiles on the playlist + 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)) + { + // The next file on the playlist is currently downloading + localMediaPlayer.setNextPlayerState(DOWNLOADING); + } + break; + } + } + else if (localMediaPlayer.currentPlaying != downloadFile) + { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + // If the downloadList contains no work, check the backgroundDownloadList + 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(localMediaPlayer.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 long getDownloadListUpdateRevision() + { + return revision; + } + + public synchronized void clear() + { + downloadList.clear(); + revision++; + if (currentDownloading != null) + { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + } + + private 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 (localMediaPlayer.currentPlaying != null) + { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, localMediaPlayer.currentPlaying); + } + revision++; + } + + 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 != localMediaPlayer.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 = localMediaPlayer.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) + { + jukeboxMediaPlayer.getValue().updatePlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) + { + if (jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().skip(0, 0); + localMediaPlayer.setPlayerState(STARTED); + } + else + { + localMediaPlayer.play(downloadList.get(0)); + } + } + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java new file mode 100644 index 00000000..390eb56b --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/ExternalStorageMonitor.java @@ -0,0 +1,58 @@ +package org.moire.ultrasonic.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +/** + * Monitors the state of the mobile's external storage + */ +public class ExternalStorageMonitor +{ + private static final String TAG = ExternalStorageMonitor.class.getSimpleName(); + + private Context context; + private BroadcastReceiver ejectEventReceiver; + private boolean externalStorageAvailable = true; + + public ExternalStorageMonitor(Context context) + { + this.context = context; + } + + public void onCreate(final Runnable ejectedCallback) + { + // Stop when SD card is ejected. + ejectEventReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); + if (!externalStorageAvailable) + { + Log.i(TAG, "External media is ejecting. Stopping playback."); + ejectedCallback.run(); + } + else + { + Log.i(TAG, "External media is available."); + } + } + }; + + IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); + ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + ejectFilter.addDataScheme("file"); + context.registerReceiver(ejectEventReceiver, ejectFilter); + } + + public void onDestroy() + { + context.unregisterReceiver(ejectEventReceiver); + } + + public boolean isExternalStorageAvailable() { return externalStorageAvailable; } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java similarity index 83% rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java rename to ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java index 4d85b0b6..b79a572f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java @@ -27,6 +27,7 @@ import android.view.View; import android.widget.ProgressBar; import android.widget.Toast; +import org.jetbrains.annotations.NotNull; import org.moire.ultrasonic.R; import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; import org.moire.ultrasonic.domain.JukeboxStatus; @@ -44,21 +45,22 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Provides an asynchronous interface to the remote jukebox on the Subsonic server. * * @author Sindre Mehus * @version $Id$ */ -public class JukeboxService +public class JukeboxMediaPlayer { - - private static final String TAG = JukeboxService.class.getSimpleName(); + private static final String TAG = JukeboxMediaPlayer.class.getSimpleName(); private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L; - private final Handler handler = new Handler(); private final TaskQueue tasks = new TaskQueue(); - private final DownloadServiceImpl downloadService; private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture statusUpdateFuture; private final AtomicLong timeOfLastUpdate = new AtomicLong(); @@ -67,6 +69,12 @@ public class JukeboxService private VolumeToast volumeToast; private AtomicBoolean running = new AtomicBoolean(); private Thread serviceThread; + private boolean enabled = false; + private Context context; + + // TODO: These create circular references, try to refactor + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerControllerImpl.class); + private final Downloader downloader; // TODO: Report warning if queue fills up. // TODO: Create shutdown method? @@ -74,9 +82,10 @@ public class JukeboxService // TODO: Persist RC state? // TODO: Minimize status updates. - public JukeboxService(DownloadServiceImpl downloadService) + public JukeboxMediaPlayer(Context context, Downloader downloader) { - this.downloadService = downloadService; + this.context = context; + this.downloader = downloader; } public void startJukeboxService() @@ -149,7 +158,7 @@ public class JukeboxService try { - if (!Util.isOffline(downloadService)) + if (!Util.isOffline(context)) { task = tasks.take(); JukeboxStatus status = task.execute(); @@ -177,9 +186,9 @@ public class JukeboxService // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); - if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) + if (index != null && index != -1 && index != downloader.getCurrentPlayingIndex()) { - downloadService.setCurrentPlaying(index); + mediaPlayerControllerLazy.getValue().setCurrentPlaying(index); } } @@ -207,26 +216,28 @@ public class JukeboxService { Log.w(TAG, x.toString()); - handler.post(new Runnable() + new Handler().post(new Runnable() { @Override public void run() { - Util.toast(downloadService, resourceId, false); + Util.toast(context, resourceId, false); } }); - downloadService.setJukeboxEnabled(false); + mediaPlayerControllerLazy.getValue().setJukeboxEnabled(false); } public void updatePlaylist() { + if (!enabled) return; + tasks.remove(Skip.class); tasks.remove(Stop.class); tasks.remove(Start.class); - List ids = new ArrayList(); - for (DownloadFile file : downloadService.getDownloads()) + List ids = new ArrayList<>(); + for (DownloadFile file : downloader.getDownloads()) { ids.add(file.getSong().getId()); } @@ -248,7 +259,7 @@ public class JukeboxService } tasks.add(new Skip(index, offsetSeconds)); - downloadService.setPlayerState(PlayerState.STARTED); + mediaPlayerControllerLazy.getValue().setPlayerState(PlayerState.STARTED); } public void stop() @@ -280,16 +291,14 @@ public class JukeboxService tasks.remove(SetGain.class); tasks.add(new SetGain(gain)); - if (volumeToast == null) - { - volumeToast = new VolumeToast(downloadService); - } + if (volumeToast == null) volumeToast = new VolumeToast(context); + volumeToast.setVolume(gain); } private MusicService getMusicService() { - return MusicServiceFactory.getMusicService(downloadService); + return MusicServiceFactory.getMusicService(context); } public int getPositionSeconds() @@ -318,13 +327,16 @@ public class JukeboxService } stop(); + } - downloadService.setPlayerState(PlayerState.IDLE); + public boolean isEnabled() + { + return enabled; } private static class TaskQueue { - private final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); void add(JukeboxTask jukeboxTask) { @@ -364,10 +376,11 @@ public class JukeboxService } } - private abstract class JukeboxTask + private abstract static class JukeboxTask { abstract JukeboxStatus execute() throws Exception; + @NotNull @Override public String toString() { @@ -380,7 +393,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().getJukeboxStatus(downloadService, null); + return getMusicService().getJukeboxStatus(context, null); } } @@ -396,7 +409,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().updateJukeboxPlaylist(ids, downloadService, null); + return getMusicService().updateJukeboxPlaylist(ids, context, null); } } @@ -414,7 +427,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().skipJukebox(index, offsetSeconds, downloadService, null); + return getMusicService().skipJukebox(index, offsetSeconds, context, null); } } @@ -423,7 +436,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().stopJukebox(downloadService, null); + return getMusicService().stopJukebox(context, null); } } @@ -432,7 +445,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().startJukebox(downloadService, null); + return getMusicService().startJukebox(context, null); } } @@ -449,7 +462,7 @@ public class JukeboxService @Override JukeboxStatus execute() throws Exception { - return getMusicService().setJukeboxGain(gain, downloadService, null); + return getMusicService().setJukeboxGain(gain, context, null); } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java new file mode 100644 index 00000000..d10cf796 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/LocalMediaPlayer.java @@ -0,0 +1,1119 @@ +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.jetbrains.annotations.NotNull; +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 java.util.Locale; + +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; + +/** + * Represents a Media Player which uses the mobile's resources for playback + */ +public class LocalMediaPlayer +{ + private static final String TAG = LocalMediaPlayer.class.getSimpleName(); + + public Consumer onCurrentPlayingChanged; + public Consumer onSongCompleted; + public BiConsumer onPlayerStateChanged; + public Runnable onPrepared; + public Runnable onNextSongRequested; + + public static boolean equalizerAvailable; + public static boolean visualizerAvailable; + + public PlayerState playerState = IDLE; + public DownloadFile currentPlaying; + public DownloadFile nextPlaying; + + private PlayerState nextPlayerState = IDLE; + private boolean nextSetup; + private CancellableTask nextPlayingTask; + private PowerManager.WakeLock wakeLock; + + private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private Looper mediaPlayerLooper; + private Handler mediaPlayerHandler; + private int cachedPosition; + private StreamProxy proxy; + + private AudioManager audioManager; + private RemoteControlClient remoteControlClient; + + private EqualizerController equalizerController; + private VisualizerController visualizerController; + private CancellableTask bufferTask; + private PositionCache positionCache; + private int secondaryProgress = -1; + + private final AudioFocusHandler audioFocusHandler; + private final Context context; + + static + { + try + { + EqualizerController.checkAvailable(); + equalizerAvailable = true; + } + catch (Throwable t) + { + equalizerAvailable = false; + } + } + + static + { + try + { + VisualizerController.checkAvailable(); + visualizerAvailable = true; + } + catch (Throwable t) + { + visualizerAvailable = false; + } + } + + public LocalMediaPlayer(AudioFocusHandler audioFocusHandler, Context context) + { + this.audioFocusHandler = audioFocusHandler; + this.context = context; + } + + public void onCreate() + { + if (mediaPlayer != null) + { + mediaPlayer.release(); + } + + mediaPlayer = new MediaPlayer(); + + new Thread(new Runnable() + { + @Override + public void run() + { + Thread.currentThread().setName("MediaPlayerThread"); + Looper.prepare(); + 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(Locale.getDefault(), "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); + Util.registerMediaButtonEventReceiver(context, true); + 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, "LocalMediaPlayer created"); + } + + public void onDestroy() + { + reset(); + + try + { + 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); + + 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(); + } + + audioManager.unregisterRemoteControlClient(remoteControlClient); + clearRemoteControl(); + Util.unregisterMediaButtonEventReceiver(context, true); + wakeLock.release(); + } + catch (Throwable exception) + { + Log.w(TAG, "LocalMediaPlayer onDestroy exception: ", exception); + } + + Log.i(TAG, "LocalMediaPlayer 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(final PlayerState playerState) + { + Log.i(TAG, String.format("%s -> %s (%s)", this.playerState.name(), playerState.name(), currentPlaying)); + + this.playerState = playerState; + + if (playerState == PlayerState.STARTED) + { + audioFocusHandler.requestAudioFocus(); + } + + if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) + { + updateRemoteControl(); + } + + if (onPlayerStateChanged != null) + { + Handler mainHandler = new Handler(context.getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + onPlayerStateChanged.accept(playerState, currentPlaying); + } + }; + mainHandler.post(myRunnable); + } + + 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(final DownloadFile currentPlaying) + { + Log.v(TAG, String.format("setCurrentPlaying %s", currentPlaying)); + this.currentPlaying = currentPlaying; + updateRemoteControl(); + + if (onCurrentPlayingChanged != null) + { + Handler mainHandler = new Handler(context.getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + onCurrentPlayingChanged.accept(currentPlaying); + } + }; + mainHandler.post(myRunnable); + } + } + + 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; + } + + setCurrentPlaying(fileToPlay); + bufferAndPlay(); + } + + public synchronized void playNext() + { + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false); + + if (onNextSongRequested != null) { + Handler mainHandler = new Handler(context.getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + onNextSongRequested.run(); + } + }; + mainHandler.post(myRunnable); + } + + // 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); + } + } + + private 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())); + + if (playerState == 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); + } + } else { + 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); + } + } + + 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(); + } + } + + public void clearRemoteControl() + { + if (remoteControlClient != null) + { + remoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + audioManager.unregisterRemoteControlClient(remoteControlClient); + remoteControlClient = null; + } + } + + private 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(Locale.getDefault(), "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 (LocalMediaPlayer.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); + } + } + + if (onPrepared != null) { + Handler mainHandler = new Handler(context.getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + onPrepared.run(); + } + }; + mainHandler.post(myRunnable); + } + } + }); + + 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) + { + nextSetup = false; + } + playNext(); + } + else + { + if (onSongCompleted != null) + { + Handler mainHandler = new Handler(context.getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + onSongCompleted.accept(currentPlaying); + } + }; + mainHandler.post(myRunnable); + } + } + + 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; + } + + @NotNull + @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); + } + + @NotNull + @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(50L); + } + 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/service/DownloadService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerController.java similarity index 83% rename from ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadService.java rename to ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerController.java index 9e69b722..ad6eb2e0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerController.java @@ -27,12 +27,14 @@ import org.moire.ultrasonic.domain.RepeatMode; import java.util.List; /** + * This interface contains all functions which are necessary for the Application UI + * to control the Media Player implementation. + * * @author Sindre Mehus * @version $Id$ */ -public interface DownloadService +public interface MediaPlayerController { - void download(List songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist); void downloadBackground(List songs, boolean save); @@ -53,38 +55,14 @@ public interface DownloadService boolean getShowVisualization(); - boolean getEqualizerAvailable(); - - boolean getVisualizerAvailable(); - void setShowVisualization(boolean showVisualization); 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 +89,6 @@ public interface DownloadService void unpin(List songs); - DownloadFile forSong(Entry song); - - long getDownloadListUpdateRevision(); - void setSuggestedPlaylistName(String name); String getSuggestedPlaylistName(); @@ -127,8 +101,6 @@ public interface DownloadService boolean isJukeboxAvailable(); - boolean isSharingAvailable(); - void setJukeboxEnabled(boolean b); void adjustJukeboxVolume(boolean up); @@ -137,15 +109,27 @@ public interface DownloadService void setVolume(float volume); - void swap(boolean mainList, int from, int to); - void restore(List songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist); void stopJukeboxService(); - void startJukeboxService(); - void updateNotification(); void setSongRating(final int rating); + + DownloadFile getCurrentPlaying(); + + int getPlaylistSize(); + + int getCurrentPlayingNumberOnPlaylist(); + + DownloadFile getCurrentDownloading(); + + List getPlayList(); + + long getPlayListUpdateRevision(); + + long getPlayListDuration(); + + DownloadFile getDownloadFileForSong(Entry song); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java new file mode 100644 index 00000000..b7e9f034 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerControllerImpl.java @@ -0,0 +1,647 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package org.moire.ultrasonic.service; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import org.koin.java.standalone.KoinJavaComponent; +import org.moire.ultrasonic.audiofx.EqualizerController; +import org.moire.ultrasonic.audiofx.VisualizerController; +import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.domain.MusicDirectory.Entry; +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.domain.RepeatMode; +import org.moire.ultrasonic.domain.UserInfo; +import org.moire.ultrasonic.featureflags.Feature; +import org.moire.ultrasonic.featureflags.FeatureStorage; +import org.moire.ultrasonic.util.ShufflePlayBuffer; +import org.moire.ultrasonic.util.Util; + +import java.util.Iterator; +import java.util.List; + +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + +/** + * The implementation of the Media Player Controller. + * This class contains everything that is necessary for the Application UI + * to control the Media Player implementation. + * + * @author Sindre Mehus, Joshua Bahnsen + * @version $Id$ + */ +public class MediaPlayerControllerImpl implements MediaPlayerController +{ + private static final String TAG = MediaPlayerControllerImpl.class.getSimpleName(); + + private boolean created = false; + private String suggestedPlaylistName; + private boolean keepScreenOn; + + private boolean showVisualization; + private boolean autoPlayStart; + + private Context context; + private Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); + private final DownloadQueueSerializer downloadQueueSerializer; + private final ExternalStorageMonitor externalStorageMonitor; + private final Downloader downloader; + private final ShufflePlayBuffer shufflePlayBuffer; + private final LocalMediaPlayer localMediaPlayer; + + public MediaPlayerControllerImpl(Context context, DownloadQueueSerializer downloadQueueSerializer, + ExternalStorageMonitor externalStorageMonitor, Downloader downloader, + ShufflePlayBuffer shufflePlayBuffer, LocalMediaPlayer localMediaPlayer) + { + this.context = context; + this.downloadQueueSerializer = downloadQueueSerializer; + this.externalStorageMonitor = externalStorageMonitor; + this.downloader = downloader; + this.shufflePlayBuffer = shufflePlayBuffer; + this.localMediaPlayer = localMediaPlayer; + + Log.i(TAG, "MediaPlayerControllerImpl constructed"); + } + + public void onCreate() + { + if (created) return; + this.externalStorageMonitor.onCreate(new Runnable() { + @Override + public void run() { + reset(); + } + }); + + int instance = Util.getActiveServer(context); + setJukeboxEnabled(Util.getJukeboxEnabled(context, instance)); + created = true; + + Log.i(TAG, "MediaPlayerControllerImpl created"); + } + + public void onDestroy() + { + if (!created) return; + externalStorageMonitor.onDestroy(); + context.stopService(new Intent(context, MediaPlayerService.class)); + downloader.onDestroy(); + created = false; + + Log.i(TAG, "MediaPlayerControllerImpl destroyed"); + } + + @Override + public synchronized void restore(List songs, final int currentPlayingIndex, final int currentPlayingPosition, final boolean autoPlay, boolean newPlaylist) + { + download(songs, false, false, false, false, newPlaylist); + + if (currentPlayingIndex != -1) + { + MediaPlayerService.executeOnStartedMediaPlayerService(context, new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.play(currentPlayingIndex, autoPlayStart); + + if (localMediaPlayer.currentPlaying != null) + { + if (autoPlay && jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().skip(downloader.getCurrentPlayingIndex(), currentPlayingPosition / 1000); + } + else + { + if (localMediaPlayer.currentPlaying.isCompleteFileAvailable()) + { + localMediaPlayer.doPlay(localMediaPlayer.currentPlaying, currentPlayingPosition, autoPlay); + } + } + } + autoPlayStart = false; + } + }); + } + } + + public synchronized void preload() + { + MediaPlayerService.getInstance(context); + } + + @Override + public synchronized void play(final int index) + { + MediaPlayerService.executeOnStartedMediaPlayerService(context,new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.play(index, true); + } + }); + } + + public synchronized void play() + { + MediaPlayerService.executeOnStartedMediaPlayerService(context, new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.play(); + } + }); + } + + @Override + public synchronized void togglePlayPause() + { + if (localMediaPlayer.playerState == PlayerState.IDLE) autoPlayStart = true; + MediaPlayerService.executeOnStartedMediaPlayerService(context,new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.togglePlayPause(); + } + }); + } + + @Override + public synchronized void start() + { + MediaPlayerService.executeOnStartedMediaPlayerService(context, new Consumer() { + @Override + public void accept(MediaPlayerService mediaPlayerService) { + mediaPlayerService.start(); + } + }); + } + + @Override + public synchronized void seekTo(final int position) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.seekTo(position); + } + + @Override + public synchronized void pause() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.pause(); + } + + @Override + public synchronized void stop() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.stop(); + } + + @Override + public synchronized void download(List songs, boolean save, boolean autoPlay, boolean playNext, boolean shuffle, boolean newPlaylist) + { + downloader.download(songs, save, autoPlay, playNext, newPlaylist); + jukeboxMediaPlayer.getValue().updatePlaylist(); + + if (shuffle) shuffle(); + + if (!playNext && !autoPlay && (downloader.downloadList.size() - 1) == downloader.getCurrentPlayingIndex()) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); + } + + if (autoPlay) + { + play(0); + } + else + { + if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size() > 0) + { + localMediaPlayer.currentPlaying = downloader.downloadList.get(0); + localMediaPlayer.currentPlaying.setPlaying(true); + } + + downloader.checkDownloads(); + } + + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + } + + @Override + public synchronized void downloadBackground(List songs, boolean save) + { + downloader.downloadBackground(songs, save); + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + } + + public synchronized void setCurrentPlaying(DownloadFile currentPlaying) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) localMediaPlayer.setCurrentPlaying(currentPlaying); + } + + public synchronized void setCurrentPlaying(int index) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setCurrentPlaying(index); + } + + public synchronized void setPlayerState(PlayerState state) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) localMediaPlayer.setPlayerState(state); + } + + @Override + public void stopJukeboxService() + { + jukeboxMediaPlayer.getValue().stopJukeboxService(); + } + + @Override + public synchronized void setShufflePlayEnabled(boolean enabled) + { + shufflePlayBuffer.isEnabled = enabled; + if (enabled) + { + clear(); + downloader.checkDownloads(); + } + } + + @Override + public boolean isShufflePlayEnabled() + { + return shufflePlayBuffer.isEnabled; + } + + @Override + public synchronized void shuffle() + { + downloader.shuffle(); + + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + jukeboxMediaPlayer.getValue().updatePlaylist(); + + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); + } + + @Override + public RepeatMode getRepeatMode() + { + return Util.getRepeatMode(context); + } + + @Override + public synchronized void setRepeatMode(RepeatMode repeatMode) + { + Util.setRepeatMode(context, repeatMode); + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); + } + + @Override + public boolean getKeepScreenOn() + { + return keepScreenOn; + } + + @Override + public void setKeepScreenOn(boolean keepScreenOn) + { + this.keepScreenOn = keepScreenOn; + } + + @Override + public boolean getShowVisualization() + { + return showVisualization; + } + + @Override + public void setShowVisualization(boolean showVisualization) + { + this.showVisualization = showVisualization; + } + + @Override + public synchronized void clear() + { + clear(true); + } + + public synchronized void clear(boolean serialize) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.clear(serialize); + + jukeboxMediaPlayer.getValue().updatePlaylist(); + } + + @Override + public synchronized void clearIncomplete() + { + reset(); + Iterator iterator = downloader.downloadList.iterator(); + + while (iterator.hasNext()) + { + DownloadFile downloadFile = iterator.next(); + if (!downloadFile.isCompleteFileAvailable()) + { + iterator.remove(); + } + } + + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + jukeboxMediaPlayer.getValue().updatePlaylist(); + } + + @Override + public synchronized void remove(DownloadFile downloadFile) + { + if (downloadFile == localMediaPlayer.currentPlaying) + { + reset(); + setCurrentPlaying(null); + } + + downloader.removeDownloadFile(downloadFile); + + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + jukeboxMediaPlayer.getValue().updatePlaylist(); + + if (downloadFile == localMediaPlayer.nextPlaying) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.setNextPlaying(); + } + } + + @Override + public synchronized void delete(List songs) + { + for (MusicDirectory.Entry song : songs) + { + downloader.getDownloadFileForSong(song).delete(); + } + } + + @Override + public synchronized void unpin(List songs) + { + for (MusicDirectory.Entry song : songs) + { + downloader.getDownloadFileForSong(song).unpin(); + } + } + + @Override + public synchronized void previous() + { + int index = downloader.getCurrentPlayingIndex(); + if (index == -1) + { + return; + } + + // Restart song if played more than five seconds. + if (getPlayerPosition() > 5000 || index == 0) + { + play(index); + } + else + { + play(index - 1); + } + } + + @Override + public synchronized void next() + { + int index = downloader.getCurrentPlayingIndex(); + if (index != -1) + { + play(index + 1); + } + } + + @Override + public synchronized void reset() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) localMediaPlayer.reset(); + } + + @Override + public synchronized int getPlayerPosition() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return 0; + return mediaPlayerService.getPlayerPosition(); + } + + @Override + public synchronized int getPlayerDuration() + { + if (localMediaPlayer.currentPlaying != null) + { + Integer duration = localMediaPlayer.currentPlaying.getSong().getDuration(); + if (duration != null) + { + return duration * 1000; + } + } + + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return 0; + return mediaPlayerService.getPlayerDuration(); + } + + @Override + public PlayerState getPlayerState() { return localMediaPlayer.playerState; } + + @Override + public void setSuggestedPlaylistName(String name) + { + this.suggestedPlaylistName = name; + } + + @Override + public String getSuggestedPlaylistName() + { + return suggestedPlaylistName; + } + + @Override + public EqualizerController getEqualizerController() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return null; + return localMediaPlayer.getEqualizerController(); + } + + @Override + public VisualizerController getVisualizerController() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService == null) return null; + return localMediaPlayer.getVisualizerController(); + } + + @Override + public boolean isJukeboxEnabled() + { + return jukeboxMediaPlayer.getValue().isEnabled(); + } + + @Override + public boolean isJukeboxAvailable() + { + try + { + String username = Util.getUserName(context, Util.getActiveServer(context)); + UserInfo user = MusicServiceFactory.getMusicService(context).getUser(username, context, null); + return user.getJukeboxRole(); + } + catch (Exception e) + { + Log.w(TAG, "Error getting user information", e); + } + + return false; + } + + @Override + public void setJukeboxEnabled(boolean jukeboxEnabled) + { + jukeboxMediaPlayer.getValue().setEnabled(jukeboxEnabled); + setPlayerState(PlayerState.IDLE); + + if (jukeboxEnabled) + { + jukeboxMediaPlayer.getValue().startJukeboxService(); + + reset(); + + // Cancel current download, if necessary. + if (downloader.currentDownloading != null) + { + downloader.currentDownloading.cancelDownload(); + } + } + else + { + jukeboxMediaPlayer.getValue().stopJukeboxService(); + } + } + + @Override + public void adjustJukeboxVolume(boolean up) + { + jukeboxMediaPlayer.getValue().adjustVolume(up); + } + + @Override + public void setVolume(float volume) + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) localMediaPlayer.setVolume(volume); + } + + @Override + public void updateNotification() + { + MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance(); + if (mediaPlayerService != null) mediaPlayerService.updateNotification(localMediaPlayer.playerState, localMediaPlayer.currentPlaying); + } + + public void setSongRating(final int rating) + { + if (!KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING)) + return; + + if (localMediaPlayer.currentPlaying == null) + return; + + final Entry song = localMediaPlayer.currentPlaying.getSong(); + song.setUserRating(rating); + + new Thread(new Runnable() + { + @Override + public void run() + { + try + { + MusicServiceFactory.getMusicService(context).setRating(song.getId(), rating, context, null); + } + catch (Exception e) + { + Log.e(TAG, e.getMessage(), e); + } + } + }).start(); + + updateNotification(); + } + + @Override + public DownloadFile getCurrentPlaying() { + return localMediaPlayer.currentPlaying; + } + + @Override + public int getPlaylistSize() { + return downloader.downloadList.size(); + } + + @Override + public int getCurrentPlayingNumberOnPlaylist() { + return downloader.getCurrentPlayingIndex(); + } + + @Override + public DownloadFile getCurrentDownloading() { + return downloader.currentDownloading; + } + + @Override + public List getPlayList() { + return downloader.downloadList; + } + + @Override + public long getPlayListUpdateRevision() { + return downloader.getDownloadListUpdateRevision(); + } + + @Override + public long getPlayListDuration() { + return downloader.getDownloadListDuration(); + } + + @Override + public DownloadFile getDownloadFileForSong(Entry song) { + return downloader.getDownloadFileForSong(song); + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java new file mode 100644 index 00000000..7a53cbe0 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.java @@ -0,0 +1,297 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package org.moire.ultrasonic.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; + +import org.moire.ultrasonic.R; +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.util.CacheCleaner; +import org.moire.ultrasonic.util.Constants; +import org.moire.ultrasonic.util.Util; + +/** + * This class is responsible for handling received events for the Media Player implementation + * + * @author Sindre Mehus + */ +public class MediaPlayerLifecycleSupport +{ + private static final String TAG = MediaPlayerLifecycleSupport.class.getSimpleName(); + + private boolean created = false; + private DownloadQueueSerializer downloadQueueSerializer; // From DI + private final MediaPlayerControllerImpl mediaPlayerController; // From DI + private final Downloader downloader; // From DI + private Context context; + + private BroadcastReceiver headsetEventReceiver; + + public MediaPlayerLifecycleSupport(Context context, DownloadQueueSerializer downloadQueueSerializer, + final MediaPlayerControllerImpl mediaPlayerController, final Downloader downloader) + { + this.downloadQueueSerializer = downloadQueueSerializer; + this.mediaPlayerController = mediaPlayerController; + this.context = context; + this.downloader = downloader; + + Log.i(TAG, "LifecycleSupport constructed"); + } + + public void onCreate() + { + onCreate(false, null); + } + + private void onCreate(final boolean autoPlay, final Runnable afterCreated) + { + if (created) + { + if (afterCreated != null) afterCreated.run(); + return; + } + + registerHeadsetReceiver(); + + // React to media buttons. + Util.registerMediaButtonEventReceiver(context, true); + + // Register the handler for outside intents. + IntentFilter commandFilter = new IntentFilter(); + commandFilter.addAction(Constants.CMD_PLAY); + commandFilter.addAction(Constants.CMD_TOGGLEPAUSE); + commandFilter.addAction(Constants.CMD_PAUSE); + commandFilter.addAction(Constants.CMD_STOP); + commandFilter.addAction(Constants.CMD_PREVIOUS); + commandFilter.addAction(Constants.CMD_NEXT); + commandFilter.addAction(Constants.CMD_PROCESS_KEYCODE); + context.registerReceiver(intentReceiver, commandFilter); + + mediaPlayerController.onCreate(); + if (autoPlay) mediaPlayerController.preload(); + + this.downloadQueueSerializer.deserializeDownloadQueue(new Consumer() { + @Override + public void accept(State state) { + mediaPlayerController.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition, autoPlay, false); + + // Work-around: Serialize again, as the restore() method creates a serialization without current playing info. + MediaPlayerLifecycleSupport.this.downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, + downloader.getCurrentPlayingIndex(), mediaPlayerController.getPlayerPosition()); + + if (afterCreated != null) afterCreated.run(); + } + }); + + new CacheCleaner(context).clean(); + created = true; + Log.i(TAG, "LifecycleSupport created"); + } + + public void onDestroy() + { + if (!created) return; + downloadQueueSerializer.serializeDownloadQueueNow(downloader.downloadList, + downloader.getCurrentPlayingIndex(), mediaPlayerController.getPlayerPosition()); + mediaPlayerController.clear(false); + context.unregisterReceiver(headsetEventReceiver); + context.unregisterReceiver(intentReceiver); + mediaPlayerController.onDestroy(); + created = false; + Log.i(TAG, "LifecycleSupport destroyed"); + } + + public void receiveIntent(Intent intent) + { + Log.i(TAG, "Received intent"); + if (intent != null && intent.getExtras() != null) + { + KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + if (event != null) + { + handleKeyEvent(event); + } + } + } + + private void registerHeadsetReceiver() { + // Pause when headset is unplugged. + final SharedPreferences sp = Util.getPreferences(context); + final String spKey = context + .getString(R.string.settings_playback_resume_play_on_headphones_plug); + + headsetEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final Bundle extras = intent.getExtras(); + + if (extras == null) { + return; + } + + Log.i(TAG, String.format("Headset event for: %s", extras.get("name"))); + final int state = extras.getInt("state"); + if (state == 0) { + if (!mediaPlayerController.isJukeboxEnabled()) { + mediaPlayerController.pause(); + } + } else if (state == 1) { + if (!mediaPlayerController.isJukeboxEnabled() && + sp.getBoolean(spKey, false) && + mediaPlayerController.getPlayerState() == PlayerState.PAUSED) { + mediaPlayerController.start(); + } + } + } + }; + + + IntentFilter headsetIntentFilter; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + headsetIntentFilter = new IntentFilter(AudioManager.ACTION_HEADSET_PLUG); + } + else + { + headsetIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + } + context.registerReceiver(headsetEventReceiver, headsetIntentFilter); + } + + private void handleKeyEvent(KeyEvent event) + { + if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) + { + return; + } + + final int keyCode = event.getKeyCode(); + boolean autoStart = (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || + keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || + keyCode == KeyEvent.KEYCODE_HEADSETHOOK || + keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || + keyCode == KeyEvent.KEYCODE_MEDIA_NEXT); + + // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start + onCreate(autoStart, new Runnable() { + @Override + public void run() { + switch (keyCode) + { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + mediaPlayerController.togglePlayPause(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + mediaPlayerController.previous(); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + if (downloader.getCurrentPlayingIndex() < downloader.downloadList.size() - 1) + { + mediaPlayerController.next(); + } + break; + case KeyEvent.KEYCODE_MEDIA_STOP: + mediaPlayerController.stop(); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if (mediaPlayerController.getPlayerState() == PlayerState.IDLE) + { + mediaPlayerController.play(); + } + else if (mediaPlayerController.getPlayerState() != PlayerState.STARTED) + { + mediaPlayerController.start(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + mediaPlayerController.pause(); + break; + case KeyEvent.KEYCODE_1: + mediaPlayerController.setSongRating(1); + break; + case KeyEvent.KEYCODE_2: + mediaPlayerController.setSongRating(2); + break; + case KeyEvent.KEYCODE_3: + mediaPlayerController.setSongRating(3); + break; + case KeyEvent.KEYCODE_4: + mediaPlayerController.setSongRating(4); + break; + case KeyEvent.KEYCODE_5: + mediaPlayerController.setSongRating(5); + break; + default: + break; + } + } + }); + } + + /** + * This receiver manages the intent that could come from other applications. + */ + private BroadcastReceiver intentReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + String action = intent.getAction(); + if (action == null) return; + Log.i(TAG, "intentReceiver.onReceive: " + action); + + switch(action) + { + case Constants.CMD_PLAY: + mediaPlayerController.play(); + break; + case Constants.CMD_NEXT: + mediaPlayerController.next(); + break; + case Constants.CMD_PREVIOUS: + mediaPlayerController.previous(); + break; + case Constants.CMD_TOGGLEPAUSE: + mediaPlayerController.togglePlayPause(); + break; + case Constants.CMD_STOP: + // TODO: There is a stop() function, shouldn't we use that? + mediaPlayerController.pause(); + mediaPlayerController.seekTo(0); + break; + case Constants.CMD_PAUSE: + mediaPlayerController.pause(); + break; + case Constants.CMD_PROCESS_KEYCODE: + receiveIntent(intent); + break; + } + } + }; +} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java new file mode 100644 index 00000000..7c50d0e6 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MediaPlayerService.java @@ -0,0 +1,702 @@ +package org.moire.ultrasonic.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +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.SubsonicTabActivity; +import org.moire.ultrasonic.domain.MusicDirectory; +import org.moire.ultrasonic.domain.PlayerState; +import org.moire.ultrasonic.domain.RepeatMode; +import org.moire.ultrasonic.featureflags.Feature; +import org.moire.ultrasonic.featureflags.FeatureStorage; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x1; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x2; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x3; +import org.moire.ultrasonic.provider.UltraSonicAppWidgetProvider4x4; +import org.moire.ultrasonic.util.FileUtil; +import org.moire.ultrasonic.util.ShufflePlayBuffer; +import org.moire.ultrasonic.util.SimpleServiceBinder; +import org.moire.ultrasonic.util.Util; + +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; +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.PREPARING; +import static org.moire.ultrasonic.domain.PlayerState.STARTED; +import static org.moire.ultrasonic.domain.PlayerState.STOPPED; + +/** + * Android Foreground Service for playing music + * while the rest of the Ultrasonic App is in the background. + */ +public class MediaPlayerService extends Service +{ + private static final String TAG = MediaPlayerService.class.getSimpleName(); + private static final String NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"; + private static final String NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"; + private static final int NOTIFICATION_ID = 3033; + + private static MediaPlayerService instance = null; + private static final Object instanceLock = new Object(); + + private final IBinder binder = new SimpleServiceBinder<>(this); + private final Scrobbler scrobbler = new Scrobbler(); + + public Lazy jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class); + private Lazy downloadQueueSerializerLazy = inject(DownloadQueueSerializer.class); + private Lazy shufflePlayBufferLazy = inject(ShufflePlayBuffer.class); + private Lazy downloaderLazy = inject(Downloader.class); + private Lazy localMediaPlayerLazy = inject(LocalMediaPlayer.class); + private LocalMediaPlayer localMediaPlayer; + private Downloader downloader; + private ShufflePlayBuffer shufflePlayBuffer; + private DownloadQueueSerializer downloadQueueSerializer; + + private boolean isInForeground = false; + private NotificationCompat.Builder notificationBuilder; + + public RepeatMode getRepeatMode() { return Util.getRepeatMode(this); } + + public static MediaPlayerService getInstance(Context context) + { + synchronized (instanceLock) { + for (int i = 0; i < 20; i++) { + if (instance != null) return instance; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(new Intent(context, MediaPlayerService.class)); + } else { + context.startService(new Intent(context, MediaPlayerService.class)); + } + + Util.sleepQuietly(50L); + } + + return instance; + } + } + + public static MediaPlayerService getRunningInstance() + { + synchronized (instanceLock) + { + return instance; + } + } + + public static void executeOnStartedMediaPlayerService(final Context context, final Consumer taskToExecute) + { + Thread t = new Thread() + { + public void run() + { + MediaPlayerService instance = getInstance(context); + if (instance == null) + { + Log.e(TAG, "ExecuteOnStartedMediaPlayerService failed to get a MediaPlayerService instance!"); + return; + } + + taskToExecute.accept(instance); + } + }; + t.start(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) + { + return binder; + } + + @Override + public void onCreate() + { + super.onCreate(); + + downloader = downloaderLazy.getValue(); + localMediaPlayer = localMediaPlayerLazy.getValue(); + shufflePlayBuffer = shufflePlayBufferLazy.getValue(); + downloadQueueSerializer = downloadQueueSerializerLazy.getValue(); + + downloader.onCreate(); + shufflePlayBuffer.onCreate(); + + localMediaPlayer.onCreate(); + setupOnCurrentPlayingChangedHandler(); + setupOnPlayerStateChangedHandler(); + setupOnSongCompletedHandler(); + localMediaPlayer.onPrepared = new Runnable() { + @Override + public void run() { + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, + downloader.getCurrentPlayingIndex(), getPlayerPosition()); + } + }; + localMediaPlayer.onNextSongRequested = new Runnable() { + @Override + public void run() { + setNextPlaying(); + } + }; + + // Create Notification Channel + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //The suggested importance of a startForeground service notification is IMPORTANCE_LOW + NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); + channel.setLightColor(android.R.color.holo_blue_dark); + channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } + + // 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(IDLE, null); + instance = this; + + Log.i(TAG, "MediaPlayerService created"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + super.onStartCommand(intent, flags, startId); + return START_NOT_STICKY; + } + + @Override + public void onDestroy() + { + super.onDestroy(); + + instance = null; + + try { + localMediaPlayer.onDestroy(); + downloader.stop(); + shufflePlayBuffer.onDestroy(); + } catch (Throwable ignored) { + } + + Log.i(TAG, "MediaPlayerService stopped"); + } + + private void stopIfIdle() + { + synchronized (instanceLock) + { + // currentPlaying could be changed from another thread in the meantime, so check again before stopping for good + if (localMediaPlayer.currentPlaying == null || localMediaPlayer.playerState == STOPPED) stopSelf(); + } + } + + public synchronized void seekTo(int position) + { + if (jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().skip(downloader.getCurrentPlayingIndex(), position / 1000); + } + else + { + localMediaPlayer.seekTo(position); + } + } + + public synchronized int getPlayerPosition() + { + if (localMediaPlayer.playerState == IDLE || localMediaPlayer.playerState == DOWNLOADING || localMediaPlayer.playerState == PREPARING) + { + return 0; + } + + return jukeboxMediaPlayer.getValue().isEnabled() ? jukeboxMediaPlayer.getValue().getPositionSeconds() * 1000 : + localMediaPlayer.getPlayerPosition(); + } + + public synchronized int getPlayerDuration() + { + return localMediaPlayer.getPlayerDuration(); + } + + public synchronized void setCurrentPlaying(int currentPlayingIndex) + { + try + { + localMediaPlayer.setCurrentPlaying(downloader.downloadList.get(currentPlayingIndex)); + } + catch (IndexOutOfBoundsException x) + { + // Ignored + } + } + + public void setupOnCurrentPlayingChangedHandler() + { + localMediaPlayer.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.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); + } + else + { + Util.broadcastNewTrackInfo(MediaPlayerService.this, null); + Util.broadcastA2dpMetaDataChange(MediaPlayerService.this, getPlayerPosition(), null, + downloader.getDownloads().size(), downloader.getCurrentPlayingIndex() + 1); + } + + // Update widget + PlayerState playerState = localMediaPlayer.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); + + SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance(); + + if (currentPlaying != null) + { + updateNotification(localMediaPlayer.playerState, currentPlaying); + if (tabInstance != null) { + tabInstance.showNowPlaying(); + } + } + else + { + if (tabInstance != null) + { + tabInstance.hideNowPlaying(); + } + stopForeground(true); + localMediaPlayer.clearRemoteControl(); + isInForeground = false; + stopIfIdle(); + } + } + }; + } + + public synchronized void setNextPlaying() + { + boolean gaplessPlayback = Util.getGaplessPlaybackPreference(this); + + if (!gaplessPlayback) + { + localMediaPlayer.setNextPlaying(null); + return; + } + + int index = downloader.getCurrentPlayingIndex(); + + if (index != -1) + { + switch (getRepeatMode()) + { + case OFF: + index += 1; + break; + case ALL: + index = (index + 1) % downloader.downloadList.size(); + break; + case SINGLE: + default: + break; + } + } + + localMediaPlayer.clearNextPlaying(); + + if (index < downloader.downloadList.size() && index != -1) + { + localMediaPlayer.setNextPlaying(downloader.downloadList.get(index)); + } + else + { + localMediaPlayer.setNextPlaying(null); + } + } + + public synchronized void togglePlayPause() + { + if (localMediaPlayer.playerState == PAUSED || localMediaPlayer.playerState == COMPLETED || localMediaPlayer.playerState == STOPPED) + { + start(); + } + else if (localMediaPlayer.playerState == IDLE) + { + play(); + } + else if (localMediaPlayer.playerState == STARTED) + { + pause(); + } + } + + /** + * Plays either the current song (resume) or the first/next one in queue. + */ + public synchronized void play() + { + int current = downloader.getCurrentPlayingIndex(); + if (current == -1) + { + play(0); + } + else + { + play(current); + } + } + + public synchronized void play(int index) + { + play(index, true); + } + + public synchronized void play(int index, boolean start) + { + Log.v(TAG, String.format("play requested for %d", index)); + if (index < 0 || index >= downloader.downloadList.size()) + { + resetPlayback(); + } + else + { + setCurrentPlaying(index); + + if (start) + { + if (jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().skip(index, 0); + localMediaPlayer.setPlayerState(STARTED); + } + else + { + localMediaPlayer.play(downloader.downloadList.get(index)); + } + } + + downloader.checkDownloads(); + setNextPlaying(); + } + } + + private synchronized void resetPlayback() + { + localMediaPlayer.reset(); + localMediaPlayer.setCurrentPlaying(null); + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, + downloader.getCurrentPlayingIndex(), getPlayerPosition()); + } + + public synchronized void pause() + { + if (localMediaPlayer.playerState == STARTED) + { + if (jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().stop(); + } + else + { + localMediaPlayer.pause(); + } + localMediaPlayer.setPlayerState(PAUSED); + } + } + + public synchronized void stop() + { + if (localMediaPlayer.playerState == STARTED) + { + if (jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().stop(); + } + else + { + localMediaPlayer.pause(); + } + } + localMediaPlayer.setPlayerState(STOPPED); + } + + public synchronized void start() + { + if (jukeboxMediaPlayer.getValue().isEnabled()) + { + jukeboxMediaPlayer.getValue().start(); + } + else + { + localMediaPlayer.start(); + } + localMediaPlayer.setPlayerState(STARTED); + } + + public void setupOnPlayerStateChangedHandler() + { + localMediaPlayer.onPlayerStateChanged = new BiConsumer() { + @Override + public void accept(PlayerState playerState, DownloadFile currentPlaying) { + if (playerState == PAUSED) + { + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, downloader.getCurrentPlayingIndex(), getPlayerPosition()); + } + + boolean showWhenPaused = (playerState != PlayerState.STOPPED && Util.isNotificationAlwaysEnabled(MediaPlayerService.this)); + boolean show = playerState == PlayerState.STARTED || showWhenPaused; + MusicDirectory.Entry song = currentPlaying == null? null : currentPlaying.getSong(); + + Util.broadcastPlaybackStatusChange(MediaPlayerService.this, playerState); + Util.broadcastA2dpPlayStatusChange(MediaPlayerService.this, playerState, song, + downloader.downloadList.size() + downloader.backgroundDownloadList.size(), + downloader.downloadList.indexOf(currentPlaying) + 1, getPlayerPosition()); + + // 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) + { + // Only update notification if player state is one that will change the icon + if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED) + { + updateNotification(playerState, currentPlaying); + if (tabInstance != null) + { + tabInstance.showNowPlaying(); + } + } + } + else + { + if (tabInstance != null) + { + tabInstance.hideNowPlaying(); + } + stopForeground(true); + localMediaPlayer.clearRemoteControl(); + isInForeground = false; + stopIfIdle(); + } + + if (playerState == STARTED) + { + scrobbler.scrobble(MediaPlayerService.this, currentPlaying, false); + } + else if (playerState == COMPLETED) + { + scrobbler.scrobble(MediaPlayerService.this, currentPlaying, true); + } + } + }; + } + + private void setupOnSongCompletedHandler() + { + localMediaPlayer.onSongCompleted = new Consumer() { + @Override + public void accept(DownloadFile currentPlaying) { + int index = downloader.getCurrentPlayingIndex(); + + if (currentPlaying != null) + { + final MusicDirectory.Entry song = currentPlaying.getSong(); + + if (song != null && song.getBookmarkPosition() > 0 && Util.getShouldClearBookmark(MediaPlayerService.this)) + { + MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this); + try + { + musicService.deleteBookmark(song.getId(), MediaPlayerService.this, null); + } + catch (Exception ignored) + { + + } + } + } + + if (index != -1) + { + switch (getRepeatMode()) + { + case OFF: + if (index + 1 < 0 || index + 1 >= downloader.downloadList.size()) + { + if (Util.getShouldClearPlaylist(MediaPlayerService.this)) + { + clear(true); + jukeboxMediaPlayer.getValue().updatePlaylist(); + } + + resetPlayback(); + break; + } + + play(index + 1); + break; + case ALL: + play((index + 1) % downloader.downloadList.size()); + break; + case SINGLE: + play(index); + break; + default: + break; + } + } + } + }; + } + + public synchronized void clear(boolean serialize) + { + localMediaPlayer.reset(); + downloader.clear(); + localMediaPlayer.setCurrentPlaying(null); + + setNextPlaying(); + + if (serialize) { + downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList, + downloader.getCurrentPlayingIndex(), getPlayerPosition()); + } + } + + public void updateNotification(PlayerState playerState, DownloadFile currentPlaying) + { + if (Util.isNotificationEnabled(this)) { + if (isInForeground) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); + } + else { + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(this); + notificationManager.notify(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); + } + Log.w(TAG, "--- Updated notification"); + } + else { + startForeground(NOTIFICATION_ID, buildForegroundNotification(playerState, currentPlaying)); + isInForeground = true; + Log.w(TAG, "--- Created Foreground notification"); + } + } + } + + @SuppressWarnings("IconColors") + private Notification buildForegroundNotification(PlayerState playerState, DownloadFile currentPlaying) { + notificationBuilder.setSmallIcon(R.drawable.ic_stat_ultrasonic); + + notificationBuilder.setAutoCancel(false); + notificationBuilder.setOngoing(true); + notificationBuilder.setOnlyAlertOnce(true); + notificationBuilder.setWhen(System.currentTimeMillis()); + notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + notificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); + + RemoteViews contentView = new RemoteViews(this.getPackageName(), R.layout.notification); + Util.linkButtons(this, contentView, false); + RemoteViews bigView = new RemoteViews(this.getPackageName(), R.layout.notification_large); + Util.linkButtons(this, bigView, false); + + notificationBuilder.setContent(contentView); + + Intent notificationIntent = new Intent(this, DownloadActivity.class); + notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0)); + + if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) { + contentView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); + bigView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark); + } else if (playerState == PlayerState.STARTED) { + contentView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); + bigView.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark); + } + + if (currentPlaying != null) { + final MusicDirectory.Entry song = currentPlaying.getSong(); + final String title = song.getTitle(); + final String text = song.getArtist(); + final String album = song.getAlbum(); + final int rating = song.getUserRating() == null ? 0 : song.getUserRating(); + final int imageSize = Util.getNotificationImageSize(this); + + try { + final Bitmap nowPlayingImage = FileUtil.getAlbumArtBitmap(this, currentPlaying.getSong(), imageSize, true); + if (nowPlayingImage == null) { + contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } else { + contentView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); + bigView.setImageViewBitmap(R.id.notification_image, nowPlayingImage); + } + } catch (Exception x) { + Log.w(TAG, "Failed to get notification cover art", x); + contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + bigView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } + + contentView.setTextViewText(R.id.trackname, title); + bigView.setTextViewText(R.id.trackname, title); + contentView.setTextViewText(R.id.artist, text); + bigView.setTextViewText(R.id.artist, text); + contentView.setTextViewText(R.id.album, album); + bigView.setTextViewText(R.id.album, album); + + boolean useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING); + if (!useFiveStarRating) + bigView.setViewVisibility(R.id.notification_rating, View.INVISIBLE); + else { + bigView.setImageViewResource(R.id.notification_five_star_1, rating > 0 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_2, rating > 1 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_3, rating > 2 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_4, rating > 3 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + bigView.setImageViewResource(R.id.notification_five_star_5, rating > 4 ? R.drawable.ic_star_full_dark : R.drawable.ic_star_hollow_dark); + } + } + + Notification notification = notificationBuilder.build(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + notification.bigContentView = bigView; + } + + return notification; + } +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java index b1ab7bf8..97e3d125 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java @@ -584,12 +584,6 @@ public class OfflineMusicService extends RESTMusicService @Override public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - if (downloadService == null) - { - return new MusicDirectory(); - } - Reader reader = null; BufferedReader buffer = null; try diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java new file mode 100644 index 00000000..60c282c9 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/State.java @@ -0,0 +1,19 @@ +package org.moire.ultrasonic.service; + +import org.moire.ultrasonic.domain.MusicDirectory; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the state of the Media Player implementation + */ +public class State implements Serializable +{ + public static final long serialVersionUID = -6346438781062572270L; + + public List songs = new ArrayList(); + public int currentPlayingIndex; + public int currentPlayingPosition; +} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java new file mode 100644 index 00000000..67ebea09 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/Supplier.java @@ -0,0 +1,10 @@ +package org.moire.ultrasonic.service; + +/** + * Abstract class for supplying items to a consumer + * @param The type of the item supplied + */ +public abstract class Supplier +{ + public abstract T get(); +} 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 9d1edf4f..830eb882 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -7,7 +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.Downloader; import java.io.File; import java.util.ArrayList; @@ -19,10 +19,18 @@ import java.util.List; import java.util.Set; import java.util.SortedSet; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * @author Sindre Mehus * @version $Id$ */ + +/** + * Responsible for cleaning up files from the offline download cache on the filesystem + */ public class CacheCleaner { @@ -30,12 +38,11 @@ public class CacheCleaner private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L; private final Context context; - private final DownloadService downloadService; + private Lazy downloader = inject(Downloader.class); - public CacheCleaner(Context context, DownloadService downloadService) + public CacheCleaner(Context context) { this.context = context; - this.downloadService = downloadService; } public void clean() @@ -219,7 +226,7 @@ public class CacheCleaner { Set filesToNotDelete = new HashSet(5); - for (DownloadFile downloadFile : downloadService.getDownloads()) + for (DownloadFile downloadFile : downloader.getValue().getDownloads()) { filesToNotDelete.add(downloadFile.getPartialFile()); filesToNotDelete.add(downloadFile.getCompleteFile()); @@ -234,12 +241,6 @@ public class CacheCleaner @Override protected Void doInBackground(Void... params) { - if (downloadService == null) - { - Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); - return null; - } - try { Thread.currentThread().setName("BackgroundCleanup"); @@ -268,12 +269,6 @@ public class CacheCleaner @Override protected Void doInBackground(Void... params) { - if (downloadService == null) - { - Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); - return null; - } - try { Thread.currentThread().setName("BackgroundSpaceCleanup"); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index 827891fb..303bc680 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -60,6 +60,15 @@ public final class Constants public static final String INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum"; public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos"; + // Names for Intent Actions + public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE"; + public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE"; + public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP"; + public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"; + public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"; + // Notification IDs. public static final int NOTIFICATION_ID_PLAYING = 100; @@ -140,6 +149,8 @@ public final class Constants // URL for project donations. public static final String DONATION_URL = "http://www.subsonic.org/pages/premium.jsp"; + public static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser"; + public static final String ALBUM_ART_FILE = "folder.jpeg"; public static final String STARRED = "starred"; public static final String ALPHABETICAL_BY_NAME = "alphabeticalByName"; 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..9c84d6a0 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() { @@ -60,6 +65,13 @@ public class ShufflePlayBuffer } }; executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS); + Log.i(TAG, "ShufflePlayBuffer created"); + } + + public void onDestroy() + { + executorService.shutdown(); + Log.i(TAG, "ShufflePlayBuffer destroyed"); } public List get(int size) @@ -78,13 +90,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/StreamProxy.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java index 5e8c5d4a..943b4993 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java @@ -4,7 +4,7 @@ import android.util.Log; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.DownloadService; +import org.moire.ultrasonic.service.Supplier; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -32,9 +32,9 @@ public class StreamProxy implements Runnable private boolean isRunning; private ServerSocket socket; private int port; - private DownloadService downloadService; + private Supplier currentPlaying; - public StreamProxy(DownloadService downloadService) + public StreamProxy(Supplier currentPlaying) { // Create listening socket @@ -43,7 +43,7 @@ public class StreamProxy implements Runnable socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1})); socket.setSoTimeout(5000); port = socket.getLocalPort(); - this.downloadService = downloadService; + this.currentPlaying = currentPlaying; } catch (UnknownHostException e) { // impossible @@ -170,7 +170,7 @@ public class StreamProxy implements Runnable public void run() { Log.i(TAG, "Streaming song in background"); - DownloadFile downloadFile = downloadService.getCurrentPlaying(); + DownloadFile downloadFile = currentPlaying == null? null : currentPlaying.get(); MusicDirectory.Entry song = downloadFile.getSong(); long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8; Log.i(TAG, String.format("Streaming fileSize: %d", fileSize)); 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 4c455236..8b4108f2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java @@ -54,8 +54,7 @@ import org.moire.ultrasonic.domain.*; import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.service.DownloadFile; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerController; import org.moire.ultrasonic.service.MusicServiceFactory; import java.io.*; @@ -93,9 +92,8 @@ public class Util extends DownloadActivity public static final String CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged"; public static final String CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged"; - private static boolean hasFocus; - private static boolean pauseFocus; - private static boolean lowerFocus; + private static boolean mediaButtonsRegisteredForUI; + private static boolean mediaButtonsRegisteredForService; private static final Map SERVER_REST_VERSIONS = new ConcurrentHashMap(); @@ -891,19 +889,29 @@ public class Util extends DownloadActivity return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true); } - public static void registerMediaButtonEventReceiver(Context context) + public static void registerMediaButtonEventReceiver(Context context, boolean isService) { if (getMediaButtonsPreference(context)) { + if (isService) mediaButtonsRegisteredForService = true; + else mediaButtonsRegisteredForUI = true; + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager.registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName())); } } - public static void unregisterMediaButtonEventReceiver(Context context) + public static void unregisterMediaButtonEventReceiver(Context context, boolean isService) { + if (isService) mediaButtonsRegisteredForService = false; + else mediaButtonsRegisteredForUI = false; + + // Do not unregister while there is an active part of the app which needs the control + if (mediaButtonsRegisteredForService || mediaButtonsRegisteredForUI) return; + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager.unregisterMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName())); + Log.i(TAG, "MediaButtonEventReceiver unregistered."); } public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult) @@ -958,7 +966,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)) { @@ -968,17 +976,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", ""); @@ -1011,9 +1011,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); @@ -1045,38 +1042,31 @@ public class Util extends DownloadActivity context.sendBroadcast(avrcpIntent); } - public static void broadcastA2dpPlayStatusChange(Context context, PlayerState state, DownloadService downloadService) + public static void broadcastA2dpPlayStatusChange(Context context, PlayerState state, Entry currentSong, Integer listSize, Integer id, Integer playerPosition) { - if (!Util.getShouldSendBluetoothNotifications(context) || downloadService == null) + if (!Util.getShouldSendBluetoothNotifications(context)) { return; } - DownloadFile currentPlaying = downloadService.getCurrentPlaying(); - - if (currentPlaying != null) + if (currentSong != null) { Intent avrcpIntent = new Intent(CM_AVRCP_PLAYSTATE_CHANGED); - Entry song = currentPlaying.getSong(); - - if (song == null) + if (currentSong == null) { return; } - if (song != currentSong) + if (currentSong != currentSong) { - currentSong = song; + Util.currentSong = currentSong; } - String title = song.getTitle(); - 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(); + String title = currentSong.getTitle(); + String artist = currentSong.getArtist(); + String album = currentSong.getAlbum(); + Integer duration = currentSong.getDuration(); avrcpIntent.putExtra("track", title); avrcpIntent.putExtra("track_name", title); @@ -1089,7 +1079,7 @@ public class Util extends DownloadActivity if (Util.getShouldSendBluetoothAlbumArt(context)) { - File albumArtFile = FileUtil.getAlbumArtFile(context, song); + File albumArtFile = FileUtil.getAlbumArtFile(context, currentSong); avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath()); avrcpIntent.putExtra("cover", albumArtFile.getAbsolutePath()); } @@ -1187,60 +1177,6 @@ public class Util extends DownloadActivity return size; } - public static void requestAudioFocus(final Context context) - { - if (!hasFocus) - { - final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - hasFocus = true; - audioManager.requestAudioFocus(new OnAudioFocusChangeListener() - { - @Override - public void onAudioFocusChange(int focusChange) - { - DownloadService downloadService = (DownloadService) context; - if ((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !downloadService.isJukeboxEnabled()) - { - if (downloadService.getPlayerState() == PlayerState.STARTED) - { - SharedPreferences preferences = getPreferences(context); - int lossPref = Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")); - if (lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) - { - lowerFocus = true; - downloadService.setVolume(0.1f); - } - else if (lossPref == 0 || (lossPref == 1)) - { - pauseFocus = true; - downloadService.pause(); - } - } - } - else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) - { - if (pauseFocus) - { - pauseFocus = false; - downloadService.start(); - } - else if (lowerFocus) - { - lowerFocus = false; - downloadService.setVolume(1.0f); - } - } - else if (focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isJukeboxEnabled()) - { - hasFocus = false; - downloadService.pause(); - audioManager.abandonAudioFocus(this); - } - } - }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); - } - } - public static int getMinDisplayMetric(Context context) { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); @@ -1289,58 +1225,58 @@ public class Util extends DownloadActivity views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); // Emulate media button clicks. - intent = new Intent("1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 1, intent, 0); views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - intent = new Intent("2"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 2, intent, 0); views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - intent = new Intent("3"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 3, intent, 0); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); - intent = new Intent("4"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 4, intent, 0); views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); - intent = new Intent("RATE_1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 5, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent); - intent = new Intent("RATE_2"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 6, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent); - intent = new Intent("RATE_3"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 7, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent); - intent = new Intent("RATE_4"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 8, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent); - intent = new Intent("RATE_5"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent = new Intent(Constants.CMD_PROCESS_KEYCODE); + intent.setPackage(context.getPackageName()); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); + pendingIntent = PendingIntent.getBroadcast(context, 9, intent, 0); views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent); } 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 24460f67..b12b438a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/SongView.java @@ -37,8 +37,7 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.featureflags.Feature; 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.MediaPlayerController; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.Util; @@ -46,6 +45,10 @@ import org.moire.ultrasonic.util.VideoPlayerType; import java.io.File; +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; + /** * Used to display songs in a {@code ListView}. * @@ -72,13 +75,14 @@ public class SongView extends UpdateView implements Checkable private ImageType leftImageType; private ImageType rightImageType; private Drawable rightImage; - private DownloadService downloadService; private DownloadFile downloadFile; private boolean playing; private EntryAdapter.SongViewHolder viewHolder; private boolean maximized = false; private boolean useFiveStarRating; + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); + public SongView(Context context) { super(context); @@ -164,10 +168,7 @@ public class SongView extends UpdateView implements Checkable this.song = song; - if (downloadService != null) - { - this.downloadFile = downloadService.forSong(song); - } + this.downloadFile = mediaPlayerControllerLazy.getValue().getDownloadFileForSong(song); StringBuilder artist = new StringBuilder(60); @@ -311,10 +312,6 @@ public class SongView extends UpdateView implements Checkable @Override protected void updateBackground() { - if (downloadService == null) - { - downloadService = DownloadServiceImpl.getInstance(); - } } @Override @@ -322,12 +319,7 @@ public class SongView extends UpdateView implements Checkable { updateBackground(); - if (downloadService == null) - { - return; - } - - downloadFile = downloadService.forSong(this.song); + downloadFile = mediaPlayerControllerLazy.getValue().getDownloadFileForSong(this.song); File partialFile = downloadFile.getPartialFile(); if (downloadFile.isWorkDone()) @@ -417,7 +409,7 @@ public class SongView extends UpdateView implements Checkable viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable); viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable); - boolean playing = downloadService.getCurrentPlaying() == downloadFile; + boolean playing = mediaPlayerControllerLazy.getValue().getCurrentPlaying() == downloadFile; if (playing) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java index c8aafa47..d08e062f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/VisualizerView.java @@ -27,8 +27,11 @@ import android.view.View; import org.moire.ultrasonic.audiofx.VisualizerController; import org.moire.ultrasonic.domain.PlayerState; -import org.moire.ultrasonic.service.DownloadService; -import org.moire.ultrasonic.service.DownloadServiceImpl; +import org.moire.ultrasonic.service.MediaPlayerController; + +import kotlin.Lazy; + +import static org.koin.java.standalone.KoinJavaComponent.inject; /** * A simple class that draws waveform data received from a @@ -39,7 +42,6 @@ import org.moire.ultrasonic.service.DownloadServiceImpl; */ public class VisualizerView extends View { - private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000; private final Paint paint = new Paint(); @@ -48,6 +50,8 @@ public class VisualizerView extends View private float[] points; private boolean active; + private Lazy mediaPlayerControllerLazy = inject(MediaPlayerController.class); + public VisualizerView(Context context) { super(context); @@ -97,10 +101,9 @@ public class VisualizerView extends View invalidate(); } - private static Visualizer getVizualizer() + private Visualizer getVizualizer() { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - VisualizerController visualizerController = downloadService == null ? null : downloadService.getVisualizerController(); + VisualizerController visualizerController = mediaPlayerControllerLazy.getValue().getVisualizerController(); return visualizerController == null ? null : visualizerController.getVisualizer(); } @@ -120,8 +123,7 @@ public class VisualizerView extends View return; } - DownloadService downloadService = DownloadServiceImpl.getInstance(); - if (downloadService != null && downloadService.getPlayerState() != PlayerState.STARTED) + if (mediaPlayerControllerLazy.getValue().getPlayerState() != PlayerState.STARTED) { return; } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index 8e1f9fa3..fe97c592 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -7,6 +7,7 @@ import org.moire.ultrasonic.di.appPermanentStorage import org.moire.ultrasonic.di.baseNetworkModule import org.moire.ultrasonic.di.directoriesModule import org.moire.ultrasonic.di.featureFlagsModule +import org.moire.ultrasonic.di.mediaPlayerModule import org.moire.ultrasonic.di.musicServiceModule class UApp : MultiDexApplication() { @@ -20,7 +21,8 @@ class UApp : MultiDexApplication() { appPermanentStorage, baseNetworkModule, featureFlagsModule, - musicServiceModule + musicServiceModule, + mediaPlayerModule ), extraProperties = mapOf( DiProperties.APP_CONTEXT to applicationContext diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt new file mode 100644 index 00000000..8553bc2c --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -0,0 +1,32 @@ +package org.moire.ultrasonic.di + +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module.module +import org.moire.ultrasonic.service.AudioFocusHandler +import org.moire.ultrasonic.service.DownloadQueueSerializer +import org.moire.ultrasonic.service.Downloader +import org.moire.ultrasonic.service.ExternalStorageMonitor +import org.moire.ultrasonic.service.JukeboxMediaPlayer +import org.moire.ultrasonic.service.LocalMediaPlayer +import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerControllerImpl +import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.util.ShufflePlayBuffer + +val mediaPlayerModule = module { + single { + MediaPlayerControllerImpl(androidContext(), get(), get(), get(), get(), get()) + } + + single { JukeboxMediaPlayer(androidContext(), get()) } + single { MediaPlayerLifecycleSupport(androidContext(), get(), get(), get()) } + single { DownloadQueueSerializer(androidContext()) } + single { ExternalStorageMonitor(androidContext()) } + single { ShufflePlayBuffer(androidContext()) } + single { Downloader(androidContext(), get(), get(), get()) } + single { LocalMediaPlayer(get(), androidContext()) } + single { AudioFocusHandler(get()) } + + // TODO Ideally this can be cleaned up when all circular references are removed. + single { MediaPlayerControllerImpl(androidContext(), get(), get(), get(), get(), get()) } +} diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index f188ccc9..5c5b9611 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -334,8 +334,8 @@ 0.00 MB -:-- 0:00 - O player MX não está instalado. Baixe da graça pela Play Store ou modifique as configurações de vídeo. - Baixar Player MX + O MX Player não está instalado. Baixe da graça pela Play Store ou modifique as configurações de vídeo. + Baixar MX Player Toque para selecionar a música Cartão SD indisponível Sem cartão SD @@ -371,7 +371,7 @@ Saudação Padrão Confira esta música que compartilhei do %s Compartilhar músicas via - Player MX + MX Player Padrão Flash Compartilhar diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index b5b89e5f..719cc782 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -334,8 +334,8 @@ 0.00 MB —:—— 0:00 - O player MX não está instalado. Descarregue da graça pela Play Store ou modifique as configurações de vídeo. - Descarregar Player MX + O MX Player não está instalado. Descarregue da graça pela Play Store ou modifique as configurações de vídeo. + Descarregar MX Player Toque para selecionar a música Cartão SD indisponível Sem cartão SD @@ -371,7 +371,7 @@ Saudação Padrão Confira esta música que compartilhei do %s Compartilhar músicas via - Player MX + MX Player Padrão Flash Compartilhar