Merge branch 'nitehu-refactor/downloadservice' into develop

This commit is contained in:
Óscar García Amor 2020-08-11 09:56:11 +02:00
commit e7d4ca5331
45 changed files with 4040 additions and 3416 deletions

View File

@ -2933,7 +2933,9 @@ public class DragSortListView extends ListView {
// always do scroll // always do scroll
mBlockLayoutRequests = true; 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(); DragSortListView.this.layoutChildren();
invalidate(); invalidate();

View File

@ -960,7 +960,7 @@
<issue <issue
id="StaticFieldLeak" id="StaticFieldLeak"
message="This AsyncTask class should be static or leaks might occur (org.moire.ultrasonic.service.DownloadServiceLifecycleSupport.SerializeTask)" message="This AsyncTask class should be static or leaks might occur (org.moire.ultrasonic.service.MediaPlayerLifecycleSupport.SerializeTask)"
errorLine1=" private class SerializeTask extends AsyncTask&lt;Void, Void, Void>" errorLine1=" private class SerializeTask extends AsyncTask&lt;Void, Void, Void>"
errorLine2=" ~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~">
<location <location
@ -971,7 +971,7 @@
<issue <issue
id="StaticFieldLeak" id="StaticFieldLeak"
message="This AsyncTask class should be static or leaks might occur (org.moire.ultrasonic.service.DownloadServiceLifecycleSupport.DeserializeTask)" message="This AsyncTask class should be static or leaks might occur (org.moire.ultrasonic.service.MediaPlayerLifecycleSupport.DeserializeTask)"
errorLine1=" private class DeserializeTask extends AsyncTask&lt;Void, Void, Void>" errorLine1=" private class DeserializeTask extends AsyncTask&lt;Void, Void, Void>"
errorLine2=" ~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~">
<location <location

View File

@ -118,7 +118,7 @@
<activity android:name=".activity.ServerSettingsActivity" /> <activity android:name=".activity.ServerSettingsActivity" />
<service <service
android:name=".service.DownloadServiceImpl" android:name=".service.MediaPlayerService"
android:label="UltraSonic Download Service" android:label="UltraSonic Download Service"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>

View File

@ -34,6 +34,7 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
@ -206,8 +207,8 @@ public class BookmarkActivity extends SubsonicTabActivity
if (!getSelectedSongs(albumListView).isEmpty()) if (!getSelectedSongs(albumListView).isEmpty())
{ {
int position = songs.get(0).getBookmarkPosition(); int position = songs.get(0).getBookmarkPosition();
if (getDownloadService() == null) return; if (getMediaPlayerController() == null) return;
getDownloadService().restore(songs, 0, position, true, true); getMediaPlayerController().restore(songs, 0, position, true, true);
selectAll(false, false); selectAll(false, false);
} }
} }
@ -296,7 +297,8 @@ public class BookmarkActivity extends SubsonicTabActivity
private void enableButtons() private void enableButtons()
{ {
if (getDownloadService() == null) MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController == null)
{ {
return; return;
} }
@ -310,7 +312,7 @@ public class BookmarkActivity extends SubsonicTabActivity
for (MusicDirectory.Entry song : selection) for (MusicDirectory.Entry song : selection)
{ {
DownloadFile downloadFile = getDownloadService().forSong(song); DownloadFile downloadFile = mediaPlayerController.getDownloadFileForSong(song);
if (downloadFile.isWorkDone()) if (downloadFile.isWorkDone())
{ {
deleteEnabled = true; deleteEnabled = true;
@ -345,7 +347,7 @@ public class BookmarkActivity extends SubsonicTabActivity
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{ {
if (getDownloadService() == null) if (getMediaPlayerController() == null)
{ {
return; return;
} }
@ -356,7 +358,7 @@ public class BookmarkActivity extends SubsonicTabActivity
public void run() public void run()
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
getDownloadService().downloadBackground(songs, save); getMediaPlayerController().downloadBackground(songs, save);
if (save) if (save)
{ {
@ -382,19 +384,19 @@ public class BookmarkActivity extends SubsonicTabActivity
songs = getSelectedSongs(albumListView); songs = getSelectedSongs(albumListView);
} }
if (getDownloadService() != null) if (getMediaPlayerController() != null)
{ {
getDownloadService().delete(songs); getMediaPlayerController().delete(songs);
} }
} }
private void unpin() private void unpin()
{ {
if (getDownloadService() != null) if (getMediaPlayerController() != null)
{ {
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView); List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getDownloadService().unpin(songs); getMediaPlayerController().unpin(songs);
} }
} }

View File

@ -60,7 +60,7 @@ import org.moire.ultrasonic.domain.RepeatMode;
import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.service.DownloadFile; 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.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
@ -186,8 +186,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (!useFiveStarRating) ratingLinearLayout.setVisibility(View.GONE); if (!useFiveStarRating) ratingLinearLayout.setVisibility(View.GONE);
hollowStar = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_hollow); hollowStar = Util.getDrawableFromAttribute(this, R.attr.star_hollow);
fullStar = Util.getDrawableFromAttribute(SubsonicTabActivity.getInstance(), R.attr.star_full); fullStar = Util.getDrawableFromAttribute(this, R.attr.star_full);
fiveStar1ImageView.setOnClickListener(new View.OnClickListener() fiveStar1ImageView.setOnClickListener(new View.OnClickListener()
{ {
@ -257,7 +257,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
getDownloadService().previous(); getMediaPlayerController().previous();
return null; return null;
} }
@ -293,9 +293,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Boolean doInBackground() throws Throwable protected Boolean doInBackground() throws Throwable
{ {
if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) if (getMediaPlayerController().getCurrentPlayingNumberOnPlaylist() < getMediaPlayerController().getPlaylistSize() - 1)
{ {
getDownloadService().next(); getMediaPlayerController().next();
return true; return true;
} }
else else
@ -337,7 +337,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
getDownloadService().pause(); getMediaPlayerController().pause();
return null; return null;
} }
@ -361,7 +361,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
getDownloadService().reset(); getMediaPlayerController().reset();
return null; return null;
} }
@ -406,7 +406,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
public void onClick(final View view) public void onClick(final View view)
{ {
getDownloadService().shuffle(); getMediaPlayerController().shuffle();
Util.toast(DownloadActivity.this, R.string.download_menu_shuffle_notification); Util.toast(DownloadActivity.this, R.string.download_menu_shuffle_notification);
} }
}); });
@ -416,9 +416,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
public void onClick(final View view) 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(); onDownloadListChanged();
switch (repeatMode) switch (repeatMode)
@ -448,7 +448,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
getDownloadService().seekTo(getProgressBar().getProgress()); getMediaPlayerController().seekTo(getProgressBar().getProgress());
return null; return null;
} }
@ -483,7 +483,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
getDownloadService().play(position); getMediaPlayerController().play(position);
return null; return null;
} }
@ -499,15 +499,15 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
registerForContextMenu(playlistView); registerForContextMenu(playlistView);
final DownloadService downloadService = getDownloadService(); final MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (downloadService != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) if (mediaPlayerController != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false))
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
downloadService.setShufflePlayEnabled(true); mediaPlayerController.setShufflePlayEnabled(true);
} }
visualizerAvailable = (downloadService != null) && (downloadService.getVisualizerController() != null); visualizerAvailable = (mediaPlayerController != null) && (mediaPlayerController.getVisualizerController() != null);
equalizerAvailable = (downloadService != null) && (downloadService.getEqualizerController() != null); equalizerAvailable = (mediaPlayerController != null) && (mediaPlayerController.getEqualizerController() != null);
new Thread(new Runnable() new Thread(new Runnable()
{ {
@ -516,8 +516,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
{ {
try try
{ {
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
jukeboxAvailable = (downloadService != null) && (downloadService.isJukeboxAvailable()); jukeboxAvailable = (mediaPlayerController != null) && (mediaPlayerController.isJukeboxAvailable());
} }
catch (Exception e) catch (Exception e)
{ {
@ -549,7 +549,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
public boolean onTouch(final View view, final MotionEvent motionEvent) public boolean onTouch(final View view, final MotionEvent motionEvent)
{ {
visualizerView.setActive(!visualizerView.isActive()); visualizerView.setActive(!visualizerView.isActive());
getDownloadService().setShowVisualization(visualizerView.isActive()); getMediaPlayerController().setShowVisualization(visualizerView.isActive());
return true; return true;
} }
}); });
@ -565,9 +565,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
{ {
super.onResume(); 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); playlistFlipper.setDisplayedChild(1);
} }
@ -592,7 +592,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
executorService = Executors.newSingleThreadScheduledExecutor(); executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleWithFixedDelay(runnable, 0L, 250L, TimeUnit.MILLISECONDS); 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); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} }
@ -603,7 +603,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (visualizerView != null) if (visualizerView != null)
{ {
visualizerView.setActive(downloadService != null && downloadService.getShowVisualization()); visualizerView.setActive(mediaPlayerController != null && mediaPlayerController.getShowVisualization());
} }
invalidateOptionsMenu(); invalidateOptionsMenu();
@ -612,7 +612,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
// Scroll to current playing/downloading. // Scroll to current playing/downloading.
private void scrollToCurrent() private void scrollToCurrent()
{ {
if (getDownloadService() == null) if (getMediaPlayerController() == null)
{ {
return; 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++) for (int i = 0; i < count; i++)
{ {
if (currentDownloading == playlistView.getItemAtPosition(i)) if (currentDownloading == playlistView.getItemAtPosition(i))
@ -706,7 +706,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
{ {
if (id == DIALOG_SAVE_PLAYLIST) if (id == DIALOG_SAVE_PLAYLIST)
{ {
final String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null; final String playlistName = (getMediaPlayerController() != null) ? getMediaPlayerController().getSuggestedPlaylistName() : null;
if (playlistName != null) if (playlistName != null)
{ {
playlistNameView.setText(playlistName); playlistNameView.setText(playlistName);
@ -778,11 +778,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
visualizerMenuItem.setVisible(visualizerAvailable); 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) if (downloadFile != null)
{ {
@ -807,7 +807,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
} }
if (downloadService.getKeepScreenOn()) if (mediaPlayerController.getKeepScreenOn())
{ {
if (screenOption != null) if (screenOption != null)
{ {
@ -827,7 +827,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
jukeboxOption.setEnabled(jukeboxAvailable); jukeboxOption.setEnabled(jukeboxAvailable);
jukeboxOption.setVisible(jukeboxAvailable); jukeboxOption.setVisible(jukeboxAvailable);
if (downloadService.isJukeboxEnabled()) if (mediaPlayerController.isJukeboxEnabled())
{ {
jukeboxOption.setTitle(R.string.download_menu_jukebox_off); jukeboxOption.setTitle(R.string.download_menu_jukebox_off);
} }
@ -967,23 +967,23 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
startActivityForResultWithoutTransition(this, intent); startActivityForResultWithoutTransition(this, intent);
return true; return true;
case R.id.menu_remove: case R.id.menu_remove:
getDownloadService().remove(song); getMediaPlayerController().remove(song);
onDownloadListChanged(); onDownloadListChanged();
return true; return true;
case R.id.menu_item_screen_on_off: case R.id.menu_item_screen_on_off:
if (getDownloadService().getKeepScreenOn()) if (getMediaPlayerController().getKeepScreenOn())
{ {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getDownloadService().setKeepScreenOn(false); getMediaPlayerController().setKeepScreenOn(false);
} }
else else
{ {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getDownloadService().setKeepScreenOn(true); getMediaPlayerController().setKeepScreenOn(true);
} }
return true; return true;
case R.id.menu_shuffle: case R.id.menu_shuffle:
getDownloadService().shuffle(); getMediaPlayerController().shuffle();
Util.toast(this, R.string.download_menu_shuffle_notification); Util.toast(this, R.string.download_menu_shuffle_notification);
return true; return true;
case R.id.menu_item_equalizer: case R.id.menu_item_equalizer:
@ -1002,24 +1002,24 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
visualizerViewLayout.setVisibility(View.VISIBLE); 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); Util.toast(DownloadActivity.this, active ? R.string.download_visualizer_on : R.string.download_visualizer_off);
return true; return true;
case R.id.menu_item_jukebox: case R.id.menu_item_jukebox:
final boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled(); final boolean jukeboxEnabled = !getMediaPlayerController().isJukeboxEnabled();
getDownloadService().setJukeboxEnabled(jukeboxEnabled); getMediaPlayerController().setJukeboxEnabled(jukeboxEnabled);
Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false); Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
return true; return true;
case R.id.menu_item_toggle_list: case R.id.menu_item_toggle_list:
toggleFullScreenAlbumArt(); toggleFullScreenAlbumArt();
return true; return true;
case R.id.menu_item_clear_playlist: case R.id.menu_item_clear_playlist:
getDownloadService().setShufflePlayEnabled(false); getMediaPlayerController().setShufflePlayEnabled(false);
getDownloadService().clear(); getMediaPlayerController().clear();
onDownloadListChanged(); onDownloadListChanged();
return true; return true;
case R.id.menu_item_save_playlist: case R.id.menu_item_save_playlist:
if (!getDownloadService().getSongs().isEmpty()) if (getMediaPlayerController().getPlaylistSize() > 0)
{ {
showDialog(DIALOG_SAVE_PLAYLIST); showDialog(DIALOG_SAVE_PLAYLIST);
} }
@ -1077,7 +1077,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
} }
final String songId = currentSong.getId(); final String songId = currentSong.getId();
final int playerPosition = getDownloadService().getPlayerPosition(); final int playerPosition = getMediaPlayerController().getPlayerPosition();
currentSong.setBookmarkPosition(playerPosition); currentSong.setBookmarkPosition(playerPosition);
@ -1137,12 +1137,12 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
return true; return true;
case R.id.menu_item_share: case R.id.menu_item_share:
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
List<Entry> entries = new ArrayList<Entry>(); List<Entry> entries = new ArrayList<Entry>();
if (downloadService != null) if (mediaPlayerController != null)
{ {
List<DownloadFile> downloadServiceSongs = downloadService.getSongs(); List<DownloadFile> downloadServiceSongs = mediaPlayerController.getPlayList();
if (downloadServiceSongs != null) if (downloadServiceSongs != null)
{ {
@ -1170,17 +1170,18 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void update() private void update()
{ {
if (getDownloadService() == null) MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController == null)
{ {
return; return;
} }
if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) if (currentRevision != mediaPlayerController.getPlayListUpdateRevision())
{ {
onDownloadListChanged(); onDownloadListChanged();
} }
if (currentPlaying != getDownloadService().getCurrentPlaying()) if (currentPlaying != mediaPlayerController.getCurrentPlaying())
{ {
onCurrentChanged(); onCurrentChanged();
} }
@ -1192,14 +1193,14 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void savePlaylistInBackground(final String playlistName) private void savePlaylistInBackground(final String playlistName)
{ {
Util.toast(DownloadActivity.this, getResources().getString(R.string.download_playlist_saving, playlistName)); Util.toast(DownloadActivity.this, getResources().getString(R.string.download_playlist_saving, playlistName));
getDownloadService().setSuggestedPlaylistName(playlistName); getMediaPlayerController().setSuggestedPlaylistName(playlistName);
new SilentBackgroundTask<Void>(this) new SilentBackgroundTask<Void>(this)
{ {
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
final List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>(); final List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>();
for (final DownloadFile downloadFile : getDownloadService().getSongs()) for (final DownloadFile downloadFile : getMediaPlayerController().getPlayList())
{ {
entries.add(downloadFile.getSong()); entries.add(downloadFile.getSong());
} }
@ -1243,7 +1244,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void start() private void start()
{ {
final DownloadService service = getDownloadService(); final MediaPlayerController service = getMediaPlayerController();
final PlayerState state = service.getPlayerState(); final PlayerState state = service.getPlayerState();
if (state == PAUSED || state == COMPLETED || state == STOPPED) if (state == PAUSED || state == COMPLETED || state == STOPPED)
@ -1254,7 +1255,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
final int current = service.getCurrentPlayingIndex(); final int current = getMediaPlayerController().getCurrentPlayingNumberOnPlaylist();
if (current == -1) if (current == -1)
{ {
@ -1269,13 +1270,13 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void onDownloadListChanged() private void onDownloadListChanged()
{ {
final DownloadService downloadService = getDownloadService(); final MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (downloadService == null) if (mediaPlayerController == null)
{ {
return; return;
} }
final List<DownloadFile> list = downloadService.getSongs(); final List<DownloadFile> list = mediaPlayerController.getPlayList();
emptyTextView.setText(R.string.download_empty); emptyTextView.setText(R.string.download_empty);
final SongListAdapter adapter = new SongListAdapter(this, list); final SongListAdapter adapter = new SongListAdapter(this, list);
@ -1306,18 +1307,18 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
public void remove(int which) public void remove(int which)
{ {
DownloadFile item = adapter.getItem(which); DownloadFile item = adapter.getItem(which);
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (item == null || downloadService == null) if (item == null || mediaPlayerController == null)
{ {
return; return;
} }
DownloadFile currentPlaying = downloadService.getCurrentPlaying(); DownloadFile currentPlaying = mediaPlayerController.getCurrentPlaying();
if (currentPlaying == item) if (currentPlaying == item)
{ {
getDownloadService().next(); getMediaPlayerController().next();
} }
adapter.remove(item); adapter.remove(item);
@ -1333,9 +1334,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
}); });
emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
currentRevision = downloadService.getDownloadListUpdateRevision(); currentRevision = mediaPlayerController.getPlayListUpdateRevision();
switch (downloadService.getRepeatMode()) switch (mediaPlayerController.getRepeatMode())
{ {
case OFF: case OFF:
repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_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() private void onCurrentChanged()
{ {
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (downloadService == null) if (mediaPlayerController == null)
{ {
return; return;
} }
currentPlaying = downloadService.getCurrentPlaying(); currentPlaying = mediaPlayerController.getCurrentPlaying();
scrollToCurrent(); scrollToCurrent();
long totalDuration = downloadService.getDownloadListDuration(); long totalDuration = mediaPlayerController.getPlayListDuration();
long totalSongs = downloadService.getSongs().size(); long totalSongs = mediaPlayerController.getPlaylistSize();
int currentSongIndex = downloadService.getCurrentPlayingIndex() + 1; int currentSongIndex = mediaPlayerController.getCurrentPlayingNumberOnPlaylist() + 1;
String duration = Util.formatTotalDuration(totalDuration); String duration = Util.formatTotalDuration(totalDuration);
@ -1398,16 +1399,16 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void onSliderProgressChanged() private void onSliderProgressChanged()
{ {
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (downloadService == null || onProgressChangedTask != null) if (mediaPlayerController == null || onProgressChangedTask != null)
{ {
return; return;
} }
onProgressChangedTask = new SilentBackgroundTask<Void>(this) onProgressChangedTask = new SilentBackgroundTask<Void>(this)
{ {
DownloadService downloadService; MediaPlayerController mediaPlayerController;
boolean isJukeboxEnabled; boolean isJukeboxEnabled;
int millisPlayed; int millisPlayed;
Integer duration; Integer duration;
@ -1416,11 +1417,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
downloadService = getDownloadService(); this.mediaPlayerController = getMediaPlayerController();
isJukeboxEnabled = downloadService.isJukeboxEnabled(); isJukeboxEnabled = this.mediaPlayerController.isJukeboxEnabled();
millisPlayed = Math.max(0, downloadService.getPlayerPosition()); millisPlayed = Math.max(0, this.mediaPlayerController.getPlayerPosition());
duration = downloadService.getPlayerDuration(); duration = this.mediaPlayerController.getPlayerDuration();
playerState = getDownloadService().getPlayerState(); playerState = getMediaPlayerController().getPlayerState();
return null; return null;
} }
@ -1457,9 +1458,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
setActionBarSubtitle(R.string.download_playerstate_buffering); setActionBarSubtitle(R.string.download_playerstate_buffering);
break; break;
case STARTED: 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); setActionBarSubtitle(R.string.download_playerstate_playing_shuffle);
} }
@ -1503,7 +1504,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
break; 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(); displaySongRating();
onProgressChangedTask = null; onProgressChangedTask = null;
@ -1514,8 +1515,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private void changeProgress(final int ms) private void changeProgress(final int ms)
{ {
final DownloadService downloadService = getDownloadService(); final MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (downloadService == null) if (mediaPlayerController == null)
{ {
return; return;
} }
@ -1529,12 +1530,12 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable protected Void doInBackground() throws Throwable
{ {
msPlayed = Math.max(0, downloadService.getPlayerPosition()); msPlayed = Math.max(0, mediaPlayerController.getPlayerPosition());
duration = downloadService.getPlayerDuration(); duration = mediaPlayerController.getPlayerDuration();
final int msTotal = duration; final int msTotal = duration;
seekTo = msPlayed + ms > msTotal ? msTotal : msPlayed + ms; seekTo = msPlayed + ms > msTotal ? msTotal : msPlayed + ms;
downloadService.seekTo(seekTo); mediaPlayerController.seekTo(seekTo);
return null; 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) 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; return false;
} }
@ -1580,9 +1581,9 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (e1X - e2X > swipeDistance && absX > swipeVelocity) if (e1X - e2X > swipeDistance && absX > swipeVelocity)
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) if (mediaPlayerController.getCurrentPlayingNumberOnPlaylist() < mediaPlayerController.getPlaylistSize() - 1)
{ {
downloadService.next(); mediaPlayerController.next();
onCurrentChanged(); onCurrentChanged();
onSliderProgressChanged(); onSliderProgressChanged();
} }
@ -1593,7 +1594,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (e2X - e1X > swipeDistance && absX > swipeVelocity) if (e2X - e1X > swipeDistance && absX > swipeVelocity)
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
downloadService.previous(); mediaPlayerController.previous();
onCurrentChanged(); onCurrentChanged();
onSliderProgressChanged(); onSliderProgressChanged();
return true; return true;
@ -1603,7 +1604,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) if (e2Y - e1Y > swipeDistance && absY > swipeVelocity)
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
downloadService.seekTo(downloadService.getPlayerPosition() + 30000); mediaPlayerController.seekTo(mediaPlayerController.getPlayerPosition() + 30000);
onSliderProgressChanged(); onSliderProgressChanged();
return true; return true;
} }
@ -1612,7 +1613,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) if (e1Y - e2Y > swipeDistance && absY > swipeVelocity)
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
downloadService.seekTo(downloadService.getPlayerPosition() - 8000); mediaPlayerController.seekTo(mediaPlayerController.getPlayerPosition() - 8000);
onSliderProgressChanged(); onSliderProgressChanged();
return true; return true;
} }
@ -1663,6 +1664,6 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
return; return;
displaySongRating(); displaySongRating();
getDownloadService().setSongRating(rating); getMediaPlayerController().setSongRating(rating);
} }
} }

View File

@ -32,12 +32,15 @@ import android.widget.TextView;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.audiofx.EqualizerController; import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* Equalizer controls. * Equalizer controls.
* *
@ -52,6 +55,8 @@ public class EqualizerActivity extends ResultActivity
private EqualizerController equalizerController; private EqualizerController equalizerController;
private Equalizer equalizer; private Equalizer equalizer;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
@Override @Override
public void onCreate(Bundle bundle) public void onCreate(Bundle bundle)
{ {
@ -123,14 +128,7 @@ public class EqualizerActivity extends ResultActivity
private void setup() private void setup()
{ {
DownloadService instance = DownloadServiceImpl.getInstance(); equalizerController = mediaPlayerControllerLazy.getValue().getEqualizerController();
if (instance == null)
{
return;
}
equalizerController = instance.getEqualizerController();
equalizer = equalizerController.getEqualizer(); equalizer = equalizerController.getEqualizer();
initEqualizer(); initEqualizer();

View File

@ -34,8 +34,8 @@ import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
@ -46,7 +46,10 @@ import org.moire.ultrasonic.util.Util;
import java.util.Collections; import java.util.Collections;
import kotlin.Lazy;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.koin.java.standalone.KoinJavaComponent.inject;
public class MainActivity extends SubsonicTabActivity public class MainActivity extends SubsonicTabActivity
{ {
@ -67,6 +70,8 @@ public class MainActivity extends SubsonicTabActivity
private static boolean infoDialogDisplayed; private static boolean infoDialogDisplayed;
private static boolean shouldUseId3; private static boolean shouldUseId3;
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
/** /**
* Called when the activity is first created. * Called when the activity is first created.
*/ */
@ -79,9 +84,9 @@ public class MainActivity extends SubsonicTabActivity
{ {
setResult(Constants.RESULT_CLOSE_ALL); setResult(Constants.RESULT_CLOSE_ALL);
if (getDownloadService() != null) if (getMediaPlayerController() != null)
{ {
getDownloadService().stopJukeboxService(); getMediaPlayerController().stopJukeboxService();
} }
if (getImageLoader() != null) if (getImageLoader() != null)
@ -456,7 +461,7 @@ public class MainActivity extends SubsonicTabActivity
private void setActiveServer(final int instance) private void setActiveServer(final int instance)
{ {
final DownloadService service = getDownloadService(); final MediaPlayerController service = getMediaPlayerController();
if (Util.getActiveServer(this) != instance) if (Util.getActiveServer(this) != instance)
{ {
@ -476,8 +481,8 @@ public class MainActivity extends SubsonicTabActivity
private void exit() private void exit()
{ {
stopService(new Intent(this, DownloadServiceImpl.class)); lifecycleSupport.getValue().onDestroy();
Util.unregisterMediaButtonEventReceiver(this); Util.unregisterMediaButtonEventReceiver(this, false);
finish(); finish();
} }

View File

@ -37,7 +37,7 @@ import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.SearchCriteria; import org.moire.ultrasonic.domain.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult; 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.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask; import org.moire.ultrasonic.util.BackgroundTask;
@ -322,7 +322,7 @@ public class SearchActivity extends SubsonicTabActivity
{ {
songs.add(entry); songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getDownloadService().unpin(songs); getMediaPlayerController().unpin(songs);
} }
break; break;
case R.id.menu_item_share: case R.id.menu_item_share:
@ -341,7 +341,7 @@ public class SearchActivity extends SubsonicTabActivity
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{ {
if (getDownloadService() == null) if (getMediaPlayerController() == null)
{ {
return; return;
} }
@ -352,7 +352,7 @@ public class SearchActivity extends SubsonicTabActivity
public void run() public void run()
{ {
warnIfNetworkOrStorageUnavailable(); 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) private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext)
{ {
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (downloadService != null) if (mediaPlayerController != null)
{ {
if (!append && !playNext) 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) 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)); Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));

View File

@ -40,6 +40,7 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.AlbumHeader; import org.moire.ultrasonic.util.AlbumHeader;
@ -1010,7 +1011,8 @@ public class SelectAlbumActivity extends SubsonicTabActivity
private void enableButtons() private void enableButtons()
{ {
if (getDownloadService() == null) MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController == null)
{ {
return; return;
} }
@ -1024,7 +1026,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity
for (MusicDirectory.Entry song : selection) for (MusicDirectory.Entry song : selection)
{ {
DownloadFile downloadFile = getDownloadService().forSong(song); DownloadFile downloadFile = mediaPlayerController.getDownloadFileForSong(song);
if (downloadFile.isWorkDone()) if (downloadFile.isWorkDone())
{ {
deleteEnabled = true; deleteEnabled = true;
@ -1061,7 +1063,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{ {
if (getDownloadService() == null) if (getMediaPlayerController() == null)
{ {
return; return;
} }
@ -1072,7 +1074,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity
public void run() public void run()
{ {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
getDownloadService().downloadBackground(songs, save); getMediaPlayerController().downloadBackground(songs, save);
if (save) if (save)
{ {
@ -1098,19 +1100,19 @@ public class SelectAlbumActivity extends SubsonicTabActivity
songs = getSelectedSongs(albumListView); songs = getSelectedSongs(albumListView);
} }
if (getDownloadService() != null) if (getMediaPlayerController() != null)
{ {
getDownloadService().delete(songs); getMediaPlayerController().delete(songs);
} }
} }
private void unpin() private void unpin()
{ {
if (getDownloadService() != null) if (getMediaPlayerController() != null)
{ {
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView); List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
Util.toast(SelectAlbumActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size())); Util.toast(SelectAlbumActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getDownloadService().unpin(songs); getMediaPlayerController().unpin(songs);
} }
} }

View File

@ -126,7 +126,7 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
List<Playlist> playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this); List<Playlist> playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
if (!Util.isOffline(SelectPlaylistActivity.this)) if (!Util.isOffline(SelectPlaylistActivity.this))
new CacheCleaner(SelectPlaylistActivity.this, getDownloadService()).cleanPlaylists(playlists); new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists);
return playlists; return playlists;
} }

View File

@ -29,7 +29,6 @@ import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.*;
@ -39,6 +38,7 @@ import android.widget.*;
import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.MenuDrawer;
import net.simonvt.menudrawer.Position; import net.simonvt.menudrawer.Position;
import org.koin.java.standalone.KoinJavaComponent; import org.koin.java.standalone.KoinJavaComponent;
import static org.koin.java.standalone.KoinJavaComponent.inject;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -56,6 +56,8 @@ import java.io.PrintWriter;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import kotlin.Lazy;
/** /**
* @author Sindre Mehus * @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 String STATE_ACTIVE_POSITION = "org.moire.ultrasonic.activePosition";
private static final int DIALOG_ASK_FOR_SHARE_DETAILS = 102; private static final int DIALOG_ASK_FOR_SHARE_DETAILS = 102;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
public MenuDrawer menuDrawer; public MenuDrawer menuDrawer;
private int activePosition = 1; private int activePosition = 1;
private int menuActiveViewId; private int menuActiveViewId;
@ -97,8 +102,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
applyTheme(); applyTheme();
super.onCreate(bundle); 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); setVolumeControlStream(AudioManager.STREAM_MUSIC);
if (bundle != null) if (bundle != null)
@ -155,7 +158,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
applyTheme(); applyTheme();
instance = this; 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 // Make sure to update theme
if (theme != null && !theme.equals(Util.getTheme(this))) if (theme != null && !theme.equals(Util.getTheme(this)))
@ -190,7 +195,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
@Override @Override
protected void onDestroy() protected void onDestroy()
{ {
Util.unregisterMediaButtonEventReceiver(this); Util.unregisterMediaButtonEventReceiver(this, false);
super.onDestroy(); super.onDestroy();
destroyed = true; destroyed = true;
nowPlayingView = null; nowPlayingView = null;
@ -203,11 +208,11 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN; boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP; boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
boolean isVolumeAdjust = isVolumeDown || isVolumeUp; boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled(); boolean isJukebox = getMediaPlayerController() != null && getMediaPlayerController().isJukeboxEnabled();
if (isVolumeAdjust && isJukebox) if (isVolumeAdjust && isJukebox)
{ {
getDownloadService().adjustJukeboxVolume(isVolumeUp); getMediaPlayerController().adjustJukeboxVolume(isVolumeUp);
return true; return true;
} }
@ -257,27 +262,22 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
if (nowPlayingView != null) 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(); final Entry song = file.getSong();
showNowPlaying(SubsonicTabActivity.this, mediaPlayerControllerLazy.getValue(), song, playerState);
if (file != null)
{
final Entry song = file.getSong();
showNowPlaying(SubsonicTabActivity.this, downloadService, song, playerState);
}
}
else
{
hideNowPlaying();
} }
} }
else
{
hideNowPlaying();
}
} }
return null; 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; return;
} }
@ -387,7 +387,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
ImageView nowPlayingControlPlay = (ImageView) nowPlayingView.findViewById(R.id.now_playing_control_play); 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); setOnTouchListenerOnUiThread(nowPlayingView, swipeDetector);
setOnClickListenerOnUiThread(nowPlayingView, new OnClickListener() setOnClickListenerOnUiThread(nowPlayingView, new OnClickListener()
@ -403,7 +403,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
@Override @Override
public void onClick(View view) 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. return mediaPlayerControllerLazy.getValue();
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();
} }
protected void warnIfNetworkOrStorageUnavailable() 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<Entry> songs) void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List<Entry> songs)
{ {
if (getDownloadService() == null) if (getMediaPlayerController() == null)
{ {
return; return;
} }
@ -851,16 +827,16 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
{ {
if (!append && !playNext) if (!append && !playNext)
{ {
getDownloadService().clear(); getMediaPlayerController().clear();
} }
warnIfNetworkOrStorageUnavailable(); 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); String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
if (playlistName != null) if (playlistName != null)
{ {
getDownloadService().setSuggestedPlaylistName(playlistName); getMediaPlayerController().setSuggestedPlaylistName(playlistName);
} }
if (autoPlay) if (autoPlay)
@ -1015,23 +991,23 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
Collections.sort(songs, new EntryByDiscAndTrackComparator()); Collections.sort(songs, new EntryByDiscAndTrackComparator());
} }
DownloadService downloadService = getDownloadService(); MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (!songs.isEmpty() && downloadService != null) if (!songs.isEmpty() && mediaPlayerController != null)
{ {
if (!append && !playNext && !unpin && !background) if (!append && !playNext && !unpin && !background)
{ {
downloadService.clear(); mediaPlayerController.clear();
} }
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
if (!background) if (!background)
{ {
if (unpin) if (unpin)
{ {
downloadService.unpin(songs); mediaPlayerController.unpin(songs);
} }
else else
{ {
downloadService.download(songs, save, autoplay, playNext, shuffle, false); mediaPlayerController.download(songs, save, autoplay, playNext, shuffle, false);
if (!append && Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this)) if (!append && Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this))
{ {
startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class); startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
@ -1042,11 +1018,11 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
{ {
if (unpin) if (unpin)
{ {
downloadService.unpin(songs); mediaPlayerController.unpin(songs);
} }
else 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 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; this.activity = activity;
} }
private static final int MIN_DISTANCE = 30; private static final int MIN_DISTANCE = 30;
private float downX, downY, upX, upY; private float downX, downY, upX, upY;
private DownloadService downloadService; private MediaPlayerController mediaPlayerController;
private SubsonicTabActivity activity; private SubsonicTabActivity activity;
@Override @Override
@ -1409,12 +1385,12 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
// left or right // left or right
if (deltaX < 0) if (deltaX < 0)
{ {
downloadService.previous(); mediaPlayerController.previous();
return false; return false;
} }
if (deltaX > 0) if (deltaX > 0)
{ {
downloadService.next(); mediaPlayerController.next();
return false; return false;
} }
} }

View File

@ -16,12 +16,15 @@ import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.provider.SearchSuggestionProvider; import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.util.*; import org.moire.ultrasonic.util.*;
import java.io.File; import java.io.File;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* Shows main app settings. * Shows main app settings.
*/ */
@ -62,6 +65,8 @@ public class SettingsFragment extends PreferenceFragment
private SharedPreferences settings; private SharedPreferences settings;
private int activeServers; private int activeServers;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -370,10 +375,10 @@ public class SettingsFragment extends PreferenceFragment
private void setMediaButtonsEnabled(boolean enabled) { private void setMediaButtonsEnabled(boolean enabled) {
if (enabled) { if (enabled) {
lockScreenEnabled.setEnabled(true); lockScreenEnabled.setEnabled(true);
Util.registerMediaButtonEventReceiver(getActivity()); Util.registerMediaButtonEventReceiver(getActivity(), false);
} else { } else {
lockScreenEnabled.setEnabled(false); lockScreenEnabled.setEnabled(false);
Util.unregisterMediaButtonEventReceiver(getActivity()); Util.unregisterMediaButtonEventReceiver(getActivity(), false);
} }
} }
@ -401,7 +406,6 @@ public class SettingsFragment extends PreferenceFragment
} }
// Clear download queue. // Clear download queue.
DownloadService downloadService = DownloadServiceImpl.getInstance(); mediaPlayerControllerLazy.getValue().clear();
downloadService.clear();
} }
} }

View File

@ -17,8 +17,9 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity; import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity; import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.FileUtil;
public class UltraSonicAppWidgetProvider extends AppWidgetProvider 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)) 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 * 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 Resources res = context.getResources();
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId); final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong(); String title = currentSong == null ? null : currentSong.getTitle();
String title = currentPlaying == null ? null : currentPlaying.getTitle(); String artist = currentSong == null ? null : currentSong.getArtist();
String artist = currentPlaying == null ? null : currentPlaying.getArtist(); String album = currentSong == null ? null : currentSong.getAlbum();
String album = currentPlaying == null ? null : currentPlaying.getAlbum();
CharSequence errorState = null; CharSequence errorState = null;
// Show error message? // Show error message?
@ -117,7 +117,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
{ {
errorState = res.getText(R.string.widget_sdcard_missing); errorState = res.getText(R.string.widget_sdcard_missing);
} }
else if (currentPlaying == null) else if (currentSong == null)
{ {
errorState = res.getText(R.string.widget_initial_text); errorState = res.getText(R.string.widget_initial_text);
} }
@ -157,7 +157,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
// Set the cover art // Set the cover art
try 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) if (bitmap == null)
{ {
@ -176,7 +176,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
} }
// Link actions buttons to intents // Link actions buttons to intents
linkButtons(context, views, currentPlaying != null); linkButtons(context, views, currentSong != null);
pushUpdate(context, appWidgetIds, views); pushUpdate(context, appWidgetIds, views);
} }
@ -194,27 +194,30 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class); Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
intent.setAction("android.intent.action.MAIN"); intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER"); 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_coverart, pendingIntent);
views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
// Emulate media button clicks. // Emulate media button clicks.
intent = new Intent("1"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); //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)); 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); 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 = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); //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)); 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); 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 = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); //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)); 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); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
} }
} }

View File

@ -5,31 +5,26 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
public class A2dpIntentReceiver extends BroadcastReceiver public class A2dpIntentReceiver extends BroadcastReceiver
{ {
private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse"; private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse";
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
@Override @Override
public void onReceive(Context context, Intent intent) public void onReceive(Context context, Intent intent)
{ {
if (mediaPlayerControllerLazy.getValue().getCurrentPlaying() == null)
DownloadService downloadService = DownloadServiceImpl.getInstance();
if (downloadService == null)
{ {
return; return;
} }
if (downloadService.getCurrentPlaying() == null) Entry song = mediaPlayerControllerLazy.getValue().getCurrentPlaying().getSong();
{
return;
}
Entry song = downloadService.getCurrentPlaying().getSong();
if (song == null) if (song == null)
{ {
@ -39,8 +34,8 @@ public class A2dpIntentReceiver extends BroadcastReceiver
Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE); Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE);
Integer duration = song.getDuration(); Integer duration = song.getDuration();
Integer playerPosition = downloadService.getPlayerPosition(); int playerPosition = mediaPlayerControllerLazy.getValue().getPlayerPosition();
Integer listSize = downloadService.getDownloads().size(); int listSize = mediaPlayerControllerLazy.getValue().getPlaylistSize();
if (duration != null) if (duration != null)
{ {
@ -50,17 +45,13 @@ public class A2dpIntentReceiver extends BroadcastReceiver
avrcpIntent.putExtra("position", (long) playerPosition); avrcpIntent.putExtra("position", (long) playerPosition);
avrcpIntent.putExtra("ListSize", (long) listSize); avrcpIntent.putExtra("ListSize", (long) listSize);
switch (downloadService.getPlayerState()) switch (mediaPlayerControllerLazy.getValue().getPlayerState())
{ {
case STARTED: case STARTED:
avrcpIntent.putExtra("playing", true); avrcpIntent.putExtra("playing", true);
break; break;
case STOPPED: case STOPPED:
avrcpIntent.putExtra("playing", false);
break;
case PAUSED: case PAUSED:
avrcpIntent.putExtra("playing", false);
break;
case COMPLETED: case COMPLETED:
avrcpIntent.putExtra("playing", false); avrcpIntent.putExtra("playing", false);
break; break;

View File

@ -24,7 +24,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import org.moire.ultrasonic.service.DownloadServiceImpl; import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.util.Util;
/** /**
@ -65,13 +65,13 @@ public class BluetoothIntentReceiver extends BroadcastReceiver
if (connected) if (connected)
{ {
Log.i(TAG, "Connected to Bluetooth device, requesting media button focus."); Log.i(TAG, "Connected to Bluetooth device, requesting media button focus.");
Util.registerMediaButtonEventReceiver(context); Util.registerMediaButtonEventReceiver(context, false);
} }
if (disconnected) if (disconnected)
{ {
Log.i(TAG, "Disconnected from Bluetooth device, requesting pause."); Log.i(TAG, "Disconnected from Bluetooth device, requesting pause.");
context.sendBroadcast(new Intent(DownloadServiceImpl.CMD_PAUSE)); context.sendBroadcast(new Intent(Constants.CMD_PAUSE));
} }
} }
} }

View File

@ -21,82 +21,63 @@ package org.moire.ultrasonic.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.Log; 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 org.moire.ultrasonic.util.Util;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class MediaButtonIntentReceiver extends BroadcastReceiver public class MediaButtonIntentReceiver extends BroadcastReceiver
{ {
private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
@Override @Override
public void onReceive(Context context, Intent intent) 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(); try
{
if (extras == null) Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE);
{
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);
serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
lifecycleSupport.getValue().receiveIntent(serviceIntent);
try if (isOrderedBroadcast())
{ {
context.startService(serviceIntent); abortBroadcast();
}
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.
} }
} }
catch (Exception x)
{
// Ignored.
}
} }
} }

View File

@ -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<MediaPlayerController> 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");
}
}
}

View File

@ -0,0 +1,11 @@
package org.moire.ultrasonic.service;
/**
* Abstract class for consumers with two parameters
* @param <T> The type of the first object to consume
* @param <U> The type of the second object to consume
*/
public abstract class BiConsumer<T, U>
{
public abstract void accept(T t, U u);
}

View File

@ -0,0 +1,10 @@
package org.moire.ultrasonic.service;
/**
* Abstract class for consumers with one parameter
* @param <T> The type of the object to consume
*/
public abstract class Consumer<T>
{
public abstract void accept(T t);
}

View File

@ -24,6 +24,7 @@ import android.os.PowerManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.util.CacheCleaner; import org.moire.ultrasonic.util.CacheCleaner;
import org.moire.ultrasonic.util.CancellableTask; import org.moire.ultrasonic.util.CancellableTask;
@ -37,11 +38,13 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import kotlin.Lazy;
import kotlin.Pair; import kotlin.Pair;
import static android.content.Context.POWER_SERVICE; import static android.content.Context.POWER_SERVICE;
import static android.os.PowerManager.ON_AFTER_RELEASE; import static android.os.PowerManager.ON_AFTER_RELEASE;
import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK; import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
@ -49,7 +52,6 @@ import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK;
*/ */
public class DownloadFile public class DownloadFile
{ {
private static final String TAG = DownloadFile.class.getSimpleName(); private static final String TAG = DownloadFile.class.getSimpleName();
private final Context context; private final Context context;
private final MusicDirectory.Entry song; private final MusicDirectory.Entry song;
@ -66,6 +68,8 @@ public class DownloadFile
private volatile boolean saveWhenDone; private volatile boolean saveWhenDone;
private volatile boolean completeWhenDone; private volatile boolean completeWhenDone;
private Lazy<Downloader> downloader = inject(Downloader.class);
public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) public DownloadFile(Context context, MusicDirectory.Entry song, boolean save)
{ {
super(); super();
@ -282,6 +286,7 @@ public class DownloadFile
this.isPlaying = isPlaying; this.isPlaying = isPlaying;
} }
@NotNull
@Override @Override
public String toString() public String toString()
{ {
@ -304,7 +309,7 @@ public class DownloadFile
{ {
PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE); PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE);
wakeLock = pm.newWakeLock(SCREEN_DIM_WAKE_LOCK | ON_AFTER_RELEASE, toString()); 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)); Log.i(TAG, String.format("Acquired wake lock %s", wakeLock));
} }
@ -439,15 +444,13 @@ public class DownloadFile
wifiLock.release(); wifiLock.release();
} }
new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace(); new CacheCleaner(context).cleanSpace();
if (DownloadServiceImpl.getInstance() != null) downloader.getValue().checkDownloads();
{
((DownloadServiceImpl) DownloadServiceImpl.getInstance()).checkDownloads();
}
} }
} }
@NotNull
@Override @Override
public String toString() public String toString()
{ {

View File

@ -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<DownloadFile> songs, int currentPlayingIndex, int currentPlayingPosition)
{
if (!setup.get())
{
return;
}
new SerializeTask().execute(songs, currentPlayingIndex, currentPlayingPosition);
}
public void serializeDownloadQueueNow(Iterable<DownloadFile> 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<State> afterDeserialized)
{
new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, afterDeserialized);
}
public void deserializeDownloadQueueNow(Consumer<State> 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<Object, Void, Void>
{
@Override
protected Void doInBackground(Object... params)
{
if (lock.tryLock())
{
try
{
Thread.currentThread().setName("SerializeTask");
serializeDownloadQueueNow((Iterable<DownloadFile>)params[0], (int)params[1], (int)params[2]);
}
finally
{
lock.unlock();
}
}
return null;
}
}
private class DeserializeTask extends AsyncTask<Object, Void, Void>
{
@Override
protected Void doInBackground(Object... params)
{
try
{
Thread.currentThread().setName("DeserializeTask");
lock.lock();
deserializeDownloadQueueNow((Consumer<State>)params[0]);
setup.set(true);
}
finally
{
lock.unlock();
}
return null;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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<DownloadFile> songs = new ArrayList<DownloadFile>(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<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
private int currentPlayingIndex;
private int currentPlayingPosition;
}
private class SerializeTask extends AsyncTask<Void, Void, Void>
{
@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<Void, Void, Void>
{
@Override
protected Void doInBackground(Void... params)
{
try
{
Thread.currentThread().setName("DeserializeTask");
lock.lock();
deserializeDownloadQueueNow();
setup.set(true);
}
finally
{
lock.unlock();
}
return null;
}
}
}

View File

@ -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<DownloadFile> downloadList = new ArrayList<>();
public final List<DownloadFile> 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> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private final List<DownloadFile> cleanupCandidates = new ArrayList<>();
private final LRUCache<MusicDirectory.Entry, DownloadFile> 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<DownloadFile> getDownloads()
{
List<DownloadFile> 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<MusicDirectory.Entry> 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<MusicDirectory.Entry> 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<DownloadFile> 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));
}
}
}
}

View File

@ -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; }
}

View File

@ -27,6 +27,7 @@ import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast; import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.domain.JukeboxStatus; 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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; 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. * Provides an asynchronous interface to the remote jukebox on the Subsonic server.
* *
* @author Sindre Mehus * @author Sindre Mehus
* @version $Id$ * @version $Id$
*/ */
public class JukeboxService public class JukeboxMediaPlayer
{ {
private static final String TAG = JukeboxMediaPlayer.class.getSimpleName();
private static final String TAG = JukeboxService.class.getSimpleName();
private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L; private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L;
private final Handler handler = new Handler();
private final TaskQueue tasks = new TaskQueue(); private final TaskQueue tasks = new TaskQueue();
private final DownloadServiceImpl downloadService;
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> statusUpdateFuture; private ScheduledFuture<?> statusUpdateFuture;
private final AtomicLong timeOfLastUpdate = new AtomicLong(); private final AtomicLong timeOfLastUpdate = new AtomicLong();
@ -67,6 +69,12 @@ public class JukeboxService
private VolumeToast volumeToast; private VolumeToast volumeToast;
private AtomicBoolean running = new AtomicBoolean(); private AtomicBoolean running = new AtomicBoolean();
private Thread serviceThread; private Thread serviceThread;
private boolean enabled = false;
private Context context;
// TODO: These create circular references, try to refactor
private Lazy<MediaPlayerControllerImpl> mediaPlayerControllerLazy = inject(MediaPlayerControllerImpl.class);
private final Downloader downloader;
// TODO: Report warning if queue fills up. // TODO: Report warning if queue fills up.
// TODO: Create shutdown method? // TODO: Create shutdown method?
@ -74,9 +82,10 @@ public class JukeboxService
// TODO: Persist RC state? // TODO: Persist RC state?
// TODO: Minimize status updates. // 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() public void startJukeboxService()
@ -149,7 +158,7 @@ public class JukeboxService
try try
{ {
if (!Util.isOffline(downloadService)) if (!Util.isOffline(context))
{ {
task = tasks.take(); task = tasks.take();
JukeboxStatus status = task.execute(); JukeboxStatus status = task.execute();
@ -177,9 +186,9 @@ public class JukeboxService
// Track change? // Track change?
Integer index = jukeboxStatus.getCurrentPlayingIndex(); 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()); Log.w(TAG, x.toString());
handler.post(new Runnable() new Handler().post(new Runnable()
{ {
@Override @Override
public void run() public void run()
{ {
Util.toast(downloadService, resourceId, false); Util.toast(context, resourceId, false);
} }
}); });
downloadService.setJukeboxEnabled(false); mediaPlayerControllerLazy.getValue().setJukeboxEnabled(false);
} }
public void updatePlaylist() public void updatePlaylist()
{ {
if (!enabled) return;
tasks.remove(Skip.class); tasks.remove(Skip.class);
tasks.remove(Stop.class); tasks.remove(Stop.class);
tasks.remove(Start.class); tasks.remove(Start.class);
List<String> ids = new ArrayList<String>(); List<String> ids = new ArrayList<>();
for (DownloadFile file : downloadService.getDownloads()) for (DownloadFile file : downloader.getDownloads())
{ {
ids.add(file.getSong().getId()); ids.add(file.getSong().getId());
} }
@ -248,7 +259,7 @@ public class JukeboxService
} }
tasks.add(new Skip(index, offsetSeconds)); tasks.add(new Skip(index, offsetSeconds));
downloadService.setPlayerState(PlayerState.STARTED); mediaPlayerControllerLazy.getValue().setPlayerState(PlayerState.STARTED);
} }
public void stop() public void stop()
@ -280,16 +291,14 @@ public class JukeboxService
tasks.remove(SetGain.class); tasks.remove(SetGain.class);
tasks.add(new SetGain(gain)); tasks.add(new SetGain(gain));
if (volumeToast == null) if (volumeToast == null) volumeToast = new VolumeToast(context);
{
volumeToast = new VolumeToast(downloadService);
}
volumeToast.setVolume(gain); volumeToast.setVolume(gain);
} }
private MusicService getMusicService() private MusicService getMusicService()
{ {
return MusicServiceFactory.getMusicService(downloadService); return MusicServiceFactory.getMusicService(context);
} }
public int getPositionSeconds() public int getPositionSeconds()
@ -318,13 +327,16 @@ public class JukeboxService
} }
stop(); stop();
}
downloadService.setPlayerState(PlayerState.IDLE); public boolean isEnabled()
{
return enabled;
} }
private static class TaskQueue private static class TaskQueue
{ {
private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<JukeboxTask>(); private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<>();
void add(JukeboxTask jukeboxTask) 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; abstract JukeboxStatus execute() throws Exception;
@NotNull
@Override @Override
public String toString() public String toString()
{ {
@ -380,7 +393,7 @@ public class JukeboxService
@Override @Override
JukeboxStatus execute() throws Exception JukeboxStatus execute() throws Exception
{ {
return getMusicService().getJukeboxStatus(downloadService, null); return getMusicService().getJukeboxStatus(context, null);
} }
} }
@ -396,7 +409,7 @@ public class JukeboxService
@Override @Override
JukeboxStatus execute() throws Exception JukeboxStatus execute() throws Exception
{ {
return getMusicService().updateJukeboxPlaylist(ids, downloadService, null); return getMusicService().updateJukeboxPlaylist(ids, context, null);
} }
} }
@ -414,7 +427,7 @@ public class JukeboxService
@Override @Override
JukeboxStatus execute() throws Exception 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 @Override
JukeboxStatus execute() throws Exception JukeboxStatus execute() throws Exception
{ {
return getMusicService().stopJukebox(downloadService, null); return getMusicService().stopJukebox(context, null);
} }
} }
@ -432,7 +445,7 @@ public class JukeboxService
@Override @Override
JukeboxStatus execute() throws Exception JukeboxStatus execute() throws Exception
{ {
return getMusicService().startJukebox(downloadService, null); return getMusicService().startJukebox(context, null);
} }
} }
@ -449,7 +462,7 @@ public class JukeboxService
@Override @Override
JukeboxStatus execute() throws Exception JukeboxStatus execute() throws Exception
{ {
return getMusicService().setJukeboxGain(gain, downloadService, null); return getMusicService().setJukeboxGain(gain, context, null);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -27,12 +27,14 @@ import org.moire.ultrasonic.domain.RepeatMode;
import java.util.List; 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 * @author Sindre Mehus
* @version $Id$ * @version $Id$
*/ */
public interface DownloadService public interface MediaPlayerController
{ {
void download(List<Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist); void download(List<Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist);
void downloadBackground(List<Entry> songs, boolean save); void downloadBackground(List<Entry> songs, boolean save);
@ -53,38 +55,14 @@ public interface DownloadService
boolean getShowVisualization(); boolean getShowVisualization();
boolean getEqualizerAvailable();
boolean getVisualizerAvailable();
void setShowVisualization(boolean showVisualization); void setShowVisualization(boolean showVisualization);
void clear(); void clear();
void clearBackground();
void clearIncomplete(); void clearIncomplete();
int size();
void remove(int which);
void remove(DownloadFile downloadFile); void remove(DownloadFile downloadFile);
long getDownloadListDuration();
List<DownloadFile> getSongs();
List<DownloadFile> getDownloads();
List<DownloadFile> getBackgroundDownloads();
int getCurrentPlayingIndex();
DownloadFile getCurrentPlaying();
DownloadFile getCurrentDownloading();
void play(int index); void play(int index);
void seekTo(int position); void seekTo(int position);
@ -111,10 +89,6 @@ public interface DownloadService
void unpin(List<Entry> songs); void unpin(List<Entry> songs);
DownloadFile forSong(Entry song);
long getDownloadListUpdateRevision();
void setSuggestedPlaylistName(String name); void setSuggestedPlaylistName(String name);
String getSuggestedPlaylistName(); String getSuggestedPlaylistName();
@ -127,8 +101,6 @@ public interface DownloadService
boolean isJukeboxAvailable(); boolean isJukeboxAvailable();
boolean isSharingAvailable();
void setJukeboxEnabled(boolean b); void setJukeboxEnabled(boolean b);
void adjustJukeboxVolume(boolean up); void adjustJukeboxVolume(boolean up);
@ -137,15 +109,27 @@ public interface DownloadService
void setVolume(float volume); void setVolume(float volume);
void swap(boolean mainList, int from, int to);
void restore(List<Entry> songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist); void restore(List<Entry> songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist);
void stopJukeboxService(); void stopJukeboxService();
void startJukeboxService();
void updateNotification(); void updateNotification();
void setSongRating(final int rating); void setSongRating(final int rating);
DownloadFile getCurrentPlaying();
int getPlaylistSize();
int getCurrentPlayingNumberOnPlaylist();
DownloadFile getCurrentDownloading();
List<DownloadFile> getPlayList();
long getPlayListUpdateRevision();
long getPlayListDuration();
DownloadFile getDownloadFileForSong(Entry song);
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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> 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<MusicDirectory.Entry> 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<MediaPlayerService>() {
@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<MediaPlayerService>() {
@Override
public void accept(MediaPlayerService mediaPlayerService) {
mediaPlayerService.play(index, true);
}
});
}
public synchronized void play()
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, new Consumer<MediaPlayerService>() {
@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<MediaPlayerService>() {
@Override
public void accept(MediaPlayerService mediaPlayerService) {
mediaPlayerService.togglePlayPause();
}
});
}
@Override
public synchronized void start()
{
MediaPlayerService.executeOnStartedMediaPlayerService(context, new Consumer<MediaPlayerService>() {
@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<MusicDirectory.Entry> 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<MusicDirectory.Entry> 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<DownloadFile> 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<MusicDirectory.Entry> songs)
{
for (MusicDirectory.Entry song : songs)
{
downloader.getDownloadFileForSong(song).delete();
}
}
@Override
public synchronized void unpin(List<MusicDirectory.Entry> 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<DownloadFile> 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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<State>() {
@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;
}
}
};
}

View File

@ -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> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private Lazy<DownloadQueueSerializer> downloadQueueSerializerLazy = inject(DownloadQueueSerializer.class);
private Lazy<ShufflePlayBuffer> shufflePlayBufferLazy = inject(ShufflePlayBuffer.class);
private Lazy<Downloader> downloaderLazy = inject(Downloader.class);
private Lazy<LocalMediaPlayer> 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<MediaPlayerService> 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<DownloadFile>() {
@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<PlayerState, DownloadFile>() {
@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<DownloadFile>() {
@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;
}
}

View File

@ -584,12 +584,6 @@ public class OfflineMusicService extends RESTMusicService
@Override @Override
public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception 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; Reader reader = null;
BufferedReader buffer = null; BufferedReader buffer = null;
try try

View File

@ -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<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
public int currentPlayingIndex;
public int currentPlayingPosition;
}

View File

@ -0,0 +1,10 @@
package org.moire.ultrasonic.service;
/**
* Abstract class for supplying items to a consumer
* @param <T> The type of the item supplied
*/
public abstract class Supplier<T>
{
public abstract T get();
}

View File

@ -7,7 +7,7 @@ import android.util.Log;
import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.Downloader;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,10 +19,18 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
* @version $Id$ * @version $Id$
*/ */
/**
* Responsible for cleaning up files from the offline download cache on the filesystem
*/
public class CacheCleaner public class CacheCleaner
{ {
@ -30,12 +38,11 @@ public class CacheCleaner
private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L; private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
private final Context context; private final Context context;
private final DownloadService downloadService; private Lazy<Downloader> downloader = inject(Downloader.class);
public CacheCleaner(Context context, DownloadService downloadService) public CacheCleaner(Context context)
{ {
this.context = context; this.context = context;
this.downloadService = downloadService;
} }
public void clean() public void clean()
@ -219,7 +226,7 @@ public class CacheCleaner
{ {
Set<File> filesToNotDelete = new HashSet<File>(5); Set<File> filesToNotDelete = new HashSet<File>(5);
for (DownloadFile downloadFile : downloadService.getDownloads()) for (DownloadFile downloadFile : downloader.getValue().getDownloads())
{ {
filesToNotDelete.add(downloadFile.getPartialFile()); filesToNotDelete.add(downloadFile.getPartialFile());
filesToNotDelete.add(downloadFile.getCompleteFile()); filesToNotDelete.add(downloadFile.getCompleteFile());
@ -234,12 +241,6 @@ public class CacheCleaner
@Override @Override
protected Void doInBackground(Void... params) protected Void doInBackground(Void... params)
{ {
if (downloadService == null)
{
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
return null;
}
try try
{ {
Thread.currentThread().setName("BackgroundCleanup"); Thread.currentThread().setName("BackgroundCleanup");
@ -268,12 +269,6 @@ public class CacheCleaner
@Override @Override
protected Void doInBackground(Void... params) protected Void doInBackground(Void... params)
{ {
if (downloadService == null)
{
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
return null;
}
try try
{ {
Thread.currentThread().setName("BackgroundSpaceCleanup"); Thread.currentThread().setName("BackgroundSpaceCleanup");

View File

@ -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_IS_ALBUM = "subsonic.isalbum";
public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos"; 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. // Notification IDs.
public static final int NOTIFICATION_ID_PLAYING = 100; public static final int NOTIFICATION_ID_PLAYING = 100;
@ -140,6 +149,8 @@ public final class Constants
// URL for project donations. // URL for project donations.
public static final String DONATION_URL = "http://www.subsonic.org/pages/premium.jsp"; 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 ALBUM_ART_FILE = "folder.jpeg";
public static final String STARRED = "starred"; public static final String STARRED = "starred";
public static final String ALPHABETICAL_BY_NAME = "alphabeticalByName"; public static final String ALPHABETICAL_BY_NAME = "alphabeticalByName";

View File

@ -37,19 +37,24 @@ import java.util.concurrent.TimeUnit;
*/ */
public class ShufflePlayBuffer public class ShufflePlayBuffer
{ {
private static final String TAG = ShufflePlayBuffer.class.getSimpleName(); private static final String TAG = ShufflePlayBuffer.class.getSimpleName();
private static final int CAPACITY = 50; private static final int CAPACITY = 50;
private static final int REFILL_THRESHOLD = 40; private static final int REFILL_THRESHOLD = 40;
private final ScheduledExecutorService executorService;
private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>(); private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
private final Context context; private final Context context;
private ScheduledExecutorService executorService;
private int currentServer; private int currentServer;
public boolean isEnabled = false;
public ShufflePlayBuffer(Context context) public ShufflePlayBuffer(Context context)
{ {
this.context = context; this.context = context;
}
public void onCreate()
{
executorService = Executors.newSingleThreadScheduledExecutor(); executorService = Executors.newSingleThreadScheduledExecutor();
Runnable runnable = new Runnable() Runnable runnable = new Runnable()
{ {
@ -60,6 +65,13 @@ public class ShufflePlayBuffer
} }
}; };
executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS); executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
Log.i(TAG, "ShufflePlayBuffer created");
}
public void onDestroy()
{
executorService.shutdown();
Log.i(TAG, "ShufflePlayBuffer destroyed");
} }
public List<MusicDirectory.Entry> get(int size) public List<MusicDirectory.Entry> get(int size)
@ -78,13 +90,9 @@ public class ShufflePlayBuffer
return result; return result;
} }
public void shutdown()
{
executorService.shutdown();
}
private void refill() private void refill()
{ {
if (!isEnabled) return;
// Check if active server has changed. // Check if active server has changed.
clearBufferIfNecessary(); clearBufferIfNecessary();

View File

@ -4,7 +4,7 @@ import android.util.Log;
import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadFile; 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.BufferedOutputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -32,9 +32,9 @@ public class StreamProxy implements Runnable
private boolean isRunning; private boolean isRunning;
private ServerSocket socket; private ServerSocket socket;
private int port; private int port;
private DownloadService downloadService; private Supplier<DownloadFile> currentPlaying;
public StreamProxy(DownloadService downloadService) public StreamProxy(Supplier<DownloadFile> currentPlaying)
{ {
// Create listening socket // 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 = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
socket.setSoTimeout(5000); socket.setSoTimeout(5000);
port = socket.getLocalPort(); port = socket.getLocalPort();
this.downloadService = downloadService; this.currentPlaying = currentPlaying;
} }
catch (UnknownHostException e) catch (UnknownHostException e)
{ // impossible { // impossible
@ -170,7 +170,7 @@ public class StreamProxy implements Runnable
public void run() public void run()
{ {
Log.i(TAG, "Streaming song in background"); Log.i(TAG, "Streaming song in background");
DownloadFile downloadFile = downloadService.getCurrentPlaying(); DownloadFile downloadFile = currentPlaying == null? null : currentPlaying.get();
MusicDirectory.Entry song = downloadFile.getSong(); MusicDirectory.Entry song = downloadFile.getSong();
long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8; long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8;
Log.i(TAG, String.format("Streaming fileSize: %d", fileSize)); Log.i(TAG, String.format("Streaming fileSize: %d", fileSize));

View File

@ -54,8 +54,7 @@ import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry; import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver; import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import java.io.*; 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_PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
public static final String CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged"; public static final String CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged";
private static boolean hasFocus; private static boolean mediaButtonsRegisteredForUI;
private static boolean pauseFocus; private static boolean mediaButtonsRegisteredForService;
private static boolean lowerFocus;
private static final Map<Integer, Version> SERVER_REST_VERSIONS = new ConcurrentHashMap<Integer, Version>(); private static final Map<Integer, Version> SERVER_REST_VERSIONS = new ConcurrentHashMap<Integer, Version>();
@ -891,19 +889,29 @@ public class Util extends DownloadActivity
return Bitmap.createScaledBitmap(bitmap, size, getScaledHeight(bitmap, size), true); 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 (getMediaButtonsPreference(context))
{ {
if (isService) mediaButtonsRegisteredForService = true;
else mediaButtonsRegisteredForUI = true;
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName())); 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 audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName())); audioManager.unregisterMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()));
Log.i(TAG, "MediaButtonEventReceiver unregistered.");
} }
public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult) public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult)
@ -958,7 +966,7 @@ public class Util extends DownloadActivity
context.sendBroadcast(intent); 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)) if (!Util.getShouldSendBluetoothNotifications(context))
{ {
@ -968,17 +976,9 @@ public class Util extends DownloadActivity
Entry song = null; Entry song = null;
Intent avrcpIntent = new Intent(CM_AVRCP_METADATA_CHANGED); Intent avrcpIntent = new Intent(CM_AVRCP_METADATA_CHANGED);
if (downloadService != null) if (currentPlaying != null) song = currentPlaying.getSong();
{
DownloadFile entry = downloadService.getCurrentPlaying();
if (entry != null) if (song == null)
{
song = entry.getSong();
}
}
if (downloadService == null || song == null)
{ {
avrcpIntent.putExtra("track", ""); avrcpIntent.putExtra("track", "");
avrcpIntent.putExtra("track_name", ""); avrcpIntent.putExtra("track_name", "");
@ -1011,9 +1011,6 @@ public class Util extends DownloadActivity
String artist = song.getArtist(); String artist = song.getArtist();
String album = song.getAlbum(); String album = song.getAlbum();
Integer duration = song.getDuration(); 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", title);
avrcpIntent.putExtra("track_name", title); avrcpIntent.putExtra("track_name", title);
@ -1045,38 +1042,31 @@ public class Util extends DownloadActivity
context.sendBroadcast(avrcpIntent); 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; return;
} }
DownloadFile currentPlaying = downloadService.getCurrentPlaying(); if (currentSong != null)
if (currentPlaying != null)
{ {
Intent avrcpIntent = new Intent(CM_AVRCP_PLAYSTATE_CHANGED); Intent avrcpIntent = new Intent(CM_AVRCP_PLAYSTATE_CHANGED);
Entry song = currentPlaying.getSong(); if (currentSong == null)
if (song == null)
{ {
return; return;
} }
if (song != currentSong) if (currentSong != currentSong)
{ {
currentSong = song; Util.currentSong = currentSong;
} }
String title = song.getTitle(); String title = currentSong.getTitle();
String artist = song.getArtist(); String artist = currentSong.getArtist();
String album = song.getAlbum(); String album = currentSong.getAlbum();
Integer duration = song.getDuration(); Integer duration = currentSong.getDuration();
Integer listSize = downloadService.getDownloads().size();
Integer id = downloadService.getCurrentPlayingIndex() + 1;
Integer playerPosition = downloadService.getPlayerPosition();
avrcpIntent.putExtra("track", title); avrcpIntent.putExtra("track", title);
avrcpIntent.putExtra("track_name", title); avrcpIntent.putExtra("track_name", title);
@ -1089,7 +1079,7 @@ public class Util extends DownloadActivity
if (Util.getShouldSendBluetoothAlbumArt(context)) if (Util.getShouldSendBluetoothAlbumArt(context))
{ {
File albumArtFile = FileUtil.getAlbumArtFile(context, song); File albumArtFile = FileUtil.getAlbumArtFile(context, currentSong);
avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath()); avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath());
avrcpIntent.putExtra("cover", albumArtFile.getAbsolutePath()); avrcpIntent.putExtra("cover", albumArtFile.getAbsolutePath());
} }
@ -1187,60 +1177,6 @@ public class Util extends DownloadActivity
return size; 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) public static int getMinDisplayMetric(Context context)
{ {
DisplayMetrics metrics = context.getResources().getDisplayMetrics(); DisplayMetrics metrics = context.getResources().getDisplayMetrics();
@ -1289,58 +1225,58 @@ public class Util extends DownloadActivity
views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
// Emulate media button clicks. // Emulate media button clicks.
intent = new Intent("1"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); 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); views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
intent = new Intent("2"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); 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); views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
intent = new Intent("3"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); 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); views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
intent = new Intent("4"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); 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); views.setOnClickPendingIntent(R.id.control_stop, pendingIntent);
intent = new Intent("RATE_1"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1)); 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); views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent);
intent = new Intent("RATE_2"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2)); 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); views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent);
intent = new Intent("RATE_3"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3)); 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); views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent);
intent = new Intent("RATE_4"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4)); 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); views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent);
intent = new Intent("RATE_5"); intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5)); 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); views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent);
} }

View File

@ -37,8 +37,7 @@ import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.featureflags.Feature; import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage; import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.service.DownloadFile; import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.util.Util;
@ -46,6 +45,10 @@ import org.moire.ultrasonic.util.VideoPlayerType;
import java.io.File; import java.io.File;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* Used to display songs in a {@code ListView}. * Used to display songs in a {@code ListView}.
* *
@ -72,13 +75,14 @@ public class SongView extends UpdateView implements Checkable
private ImageType leftImageType; private ImageType leftImageType;
private ImageType rightImageType; private ImageType rightImageType;
private Drawable rightImage; private Drawable rightImage;
private DownloadService downloadService;
private DownloadFile downloadFile; private DownloadFile downloadFile;
private boolean playing; private boolean playing;
private EntryAdapter.SongViewHolder viewHolder; private EntryAdapter.SongViewHolder viewHolder;
private boolean maximized = false; private boolean maximized = false;
private boolean useFiveStarRating; private boolean useFiveStarRating;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
public SongView(Context context) public SongView(Context context)
{ {
super(context); super(context);
@ -164,10 +168,7 @@ public class SongView extends UpdateView implements Checkable
this.song = song; this.song = song;
if (downloadService != null) this.downloadFile = mediaPlayerControllerLazy.getValue().getDownloadFileForSong(song);
{
this.downloadFile = downloadService.forSong(song);
}
StringBuilder artist = new StringBuilder(60); StringBuilder artist = new StringBuilder(60);
@ -311,10 +312,6 @@ public class SongView extends UpdateView implements Checkable
@Override @Override
protected void updateBackground() protected void updateBackground()
{ {
if (downloadService == null)
{
downloadService = DownloadServiceImpl.getInstance();
}
} }
@Override @Override
@ -322,12 +319,7 @@ public class SongView extends UpdateView implements Checkable
{ {
updateBackground(); updateBackground();
if (downloadService == null) downloadFile = mediaPlayerControllerLazy.getValue().getDownloadFileForSong(this.song);
{
return;
}
downloadFile = downloadService.forSong(this.song);
File partialFile = downloadFile.getPartialFile(); File partialFile = downloadFile.getPartialFile();
if (downloadFile.isWorkDone()) if (downloadFile.isWorkDone())
@ -417,7 +409,7 @@ public class SongView extends UpdateView implements Checkable
viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable); viewHolder.fiveStar4.setImageDrawable(rating > 3 ? starDrawable : starHollowDrawable);
viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable); viewHolder.fiveStar5.setImageDrawable(rating > 4 ? starDrawable : starHollowDrawable);
boolean playing = downloadService.getCurrentPlaying() == downloadFile; boolean playing = mediaPlayerControllerLazy.getValue().getCurrentPlaying() == downloadFile;
if (playing) if (playing)
{ {

View File

@ -27,8 +27,11 @@ import android.view.View;
import org.moire.ultrasonic.audiofx.VisualizerController; import org.moire.ultrasonic.audiofx.VisualizerController;
import org.moire.ultrasonic.domain.PlayerState; import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.service.DownloadService; import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/** /**
* A simple class that draws waveform data received from a * 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 public class VisualizerView extends View
{ {
private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000; private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000;
private final Paint paint = new Paint(); private final Paint paint = new Paint();
@ -48,6 +50,8 @@ public class VisualizerView extends View
private float[] points; private float[] points;
private boolean active; private boolean active;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
public VisualizerView(Context context) public VisualizerView(Context context)
{ {
super(context); super(context);
@ -97,10 +101,9 @@ public class VisualizerView extends View
invalidate(); invalidate();
} }
private static Visualizer getVizualizer() private Visualizer getVizualizer()
{ {
DownloadService downloadService = DownloadServiceImpl.getInstance(); VisualizerController visualizerController = mediaPlayerControllerLazy.getValue().getVisualizerController();
VisualizerController visualizerController = downloadService == null ? null : downloadService.getVisualizerController();
return visualizerController == null ? null : visualizerController.getVisualizer(); return visualizerController == null ? null : visualizerController.getVisualizer();
} }
@ -120,8 +123,7 @@ public class VisualizerView extends View
return; return;
} }
DownloadService downloadService = DownloadServiceImpl.getInstance(); if (mediaPlayerControllerLazy.getValue().getPlayerState() != PlayerState.STARTED)
if (downloadService != null && downloadService.getPlayerState() != PlayerState.STARTED)
{ {
return; return;
} }

View File

@ -7,6 +7,7 @@ import org.moire.ultrasonic.di.appPermanentStorage
import org.moire.ultrasonic.di.baseNetworkModule import org.moire.ultrasonic.di.baseNetworkModule
import org.moire.ultrasonic.di.directoriesModule import org.moire.ultrasonic.di.directoriesModule
import org.moire.ultrasonic.di.featureFlagsModule import org.moire.ultrasonic.di.featureFlagsModule
import org.moire.ultrasonic.di.mediaPlayerModule
import org.moire.ultrasonic.di.musicServiceModule import org.moire.ultrasonic.di.musicServiceModule
class UApp : MultiDexApplication() { class UApp : MultiDexApplication() {
@ -20,7 +21,8 @@ class UApp : MultiDexApplication() {
appPermanentStorage, appPermanentStorage,
baseNetworkModule, baseNetworkModule,
featureFlagsModule, featureFlagsModule,
musicServiceModule musicServiceModule,
mediaPlayerModule
), ),
extraProperties = mapOf( extraProperties = mapOf(
DiProperties.APP_CONTEXT to applicationContext DiProperties.APP_CONTEXT to applicationContext

View File

@ -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<MediaPlayerController> {
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()) }
}

View File

@ -334,8 +334,8 @@
<string name="util.bytes_format.megabyte">0.00 MB</string> <string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string> <string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string> <string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">O player MX não está instalado. Baixe da graça pela Play Store ou modifique as configurações de vídeo.</string> <string name="video.get_mx_player_text">O MX Player não está instalado. Baixe da graça pela Play Store ou modifique as configurações de vídeo.</string>
<string name="video.get_mx_player_button">Baixar Player MX</string> <string name="video.get_mx_player_button">Baixar MX Player</string>
<string name="widget.initial_text">Toque para selecionar a música</string> <string name="widget.initial_text">Toque para selecionar a música</string>
<string name="widget.sdcard_busy">Cartão SD indisponível</string> <string name="widget.sdcard_busy">Cartão SD indisponível</string>
<string name="widget.sdcard_missing">Sem cartão SD</string> <string name="widget.sdcard_missing">Sem cartão SD</string>
@ -371,7 +371,7 @@
<string name="settings.share_greeting_default">Saudação Padrão</string> <string name="settings.share_greeting_default">Saudação Padrão</string>
<string name="share_default_greeting">Confira esta música que compartilhei do %s</string> <string name="share_default_greeting">Confira esta música que compartilhei do %s</string>
<string name="share_via">Compartilhar músicas via</string> <string name="share_via">Compartilhar músicas via</string>
<string name="settings.video_mx_player">Player MX</string> <string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Padrão</string> <string name="settings.video_default">Padrão</string>
<string name="settings.video_flash">Flash</string> <string name="settings.video_flash">Flash</string>
<string name="menu.share">Compartilhar</string> <string name="menu.share">Compartilhar</string>

View File

@ -334,8 +334,8 @@
<string name="util.bytes_format.megabyte">0.00 MB</string> <string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">&#8212;:&#8212;&#8212;</string> <string name="util.no_time">&#8212;:&#8212;&#8212;</string>
<string name="util.zero_time">0:00</string> <string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">O player MX não está instalado. Descarregue da graça pela Play Store ou modifique as configurações de vídeo.</string> <string name="video.get_mx_player_text">O MX Player não está instalado. Descarregue da graça pela Play Store ou modifique as configurações de vídeo.</string>
<string name="video.get_mx_player_button">Descarregar Player MX</string> <string name="video.get_mx_player_button">Descarregar MX Player</string>
<string name="widget.initial_text">Toque para selecionar a música</string> <string name="widget.initial_text">Toque para selecionar a música</string>
<string name="widget.sdcard_busy">Cartão SD indisponível</string> <string name="widget.sdcard_busy">Cartão SD indisponível</string>
<string name="widget.sdcard_missing">Sem cartão SD</string> <string name="widget.sdcard_missing">Sem cartão SD</string>
@ -371,7 +371,7 @@
<string name="settings.share_greeting_default">Saudação Padrão</string> <string name="settings.share_greeting_default">Saudação Padrão</string>
<string name="share_default_greeting">Confira esta música que compartilhei do %s</string> <string name="share_default_greeting">Confira esta música que compartilhei do %s</string>
<string name="share_via">Compartilhar músicas via</string> <string name="share_via">Compartilhar músicas via</string>
<string name="settings.video_mx_player">Player MX</string> <string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Padrão</string> <string name="settings.video_default">Padrão</string>
<string name="settings.video_flash">Flash</string> <string name="settings.video_flash">Flash</string>
<string name="menu.share">Compartilhar</string> <string name="menu.share">Compartilhar</string>