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
mBlockLayoutRequests = true;
setSelectionFromTop(movePos, top - padTop);
// This cast is a workaround of an API bug, see https://issuetracker.google.com/issues/37045361
((ListView)DragSortListView.this).setSelectionFromTop(movePos, top - padTop);
DragSortListView.this.layoutChildren();
invalidate();

View File

@ -960,7 +960,7 @@
<issue
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>"
errorLine2=" ~~~~~~~~~~~~~">
<location
@ -971,7 +971,7 @@
<issue
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>"
errorLine2=" ~~~~~~~~~~~~~~~">
<location

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,6 @@ import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import androidx.appcompat.app.ActionBar;
import android.util.Log;
import android.view.*;
@ -39,6 +38,7 @@ import android.widget.*;
import net.simonvt.menudrawer.MenuDrawer;
import net.simonvt.menudrawer.Position;
import org.koin.java.standalone.KoinJavaComponent;
import static org.koin.java.standalone.KoinJavaComponent.inject;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -56,6 +56,8 @@ import java.io.PrintWriter;
import java.util.*;
import java.util.regex.Pattern;
import kotlin.Lazy;
/**
* @author Sindre Mehus
*/
@ -74,6 +76,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
private static final String STATE_ACTIVE_POSITION = "org.moire.ultrasonic.activePosition";
private static final int DIALOG_ASK_FOR_SHARE_DETAILS = 102;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
public MenuDrawer menuDrawer;
private int activePosition = 1;
private int menuActiveViewId;
@ -97,8 +102,6 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
applyTheme();
super.onCreate(bundle);
// This should always succeed as it is called when Ultrasonic is in the foreground
startService(new Intent(this, DownloadServiceImpl.class));
setVolumeControlStream(AudioManager.STREAM_MUSIC);
if (bundle != null)
@ -155,7 +158,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
applyTheme();
instance = this;
Util.registerMediaButtonEventReceiver(this);
Util.registerMediaButtonEventReceiver(this, false);
// Lifecycle support's constructor registers some event receivers so it should be created early
lifecycleSupport.getValue().onCreate();
// Make sure to update theme
if (theme != null && !theme.equals(Util.getTheme(this)))
@ -190,7 +195,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
@Override
protected void onDestroy()
{
Util.unregisterMediaButtonEventReceiver(this);
Util.unregisterMediaButtonEventReceiver(this, false);
super.onDestroy();
destroyed = true;
nowPlayingView = null;
@ -203,11 +208,11 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
boolean isJukebox = getMediaPlayerController() != null && getMediaPlayerController().isJukeboxEnabled();
if (isVolumeAdjust && isJukebox)
{
getDownloadService().adjustJukeboxVolume(isVolumeUp);
getMediaPlayerController().adjustJukeboxVolume(isVolumeUp);
return true;
}
@ -257,27 +262,22 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
if (nowPlayingView != null)
{
final DownloadService downloadService = DownloadServiceImpl.getInstance();
PlayerState playerState = mediaPlayerControllerLazy.getValue().getPlayerState();
if (downloadService != null)
if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED))
{
PlayerState playerState = downloadService.getPlayerState();
DownloadFile file = mediaPlayerControllerLazy.getValue().getCurrentPlaying();
if (playerState.equals(PlayerState.PAUSED) || playerState.equals(PlayerState.STARTED))
if (file != null)
{
DownloadFile file = downloadService.getCurrentPlaying();
if (file != null)
{
final Entry song = file.getSong();
showNowPlaying(SubsonicTabActivity.this, downloadService, song, playerState);
}
}
else
{
hideNowPlaying();
final Entry song = file.getSong();
showNowPlaying(SubsonicTabActivity.this, mediaPlayerControllerLazy.getValue(), song, playerState);
}
}
else
{
hideNowPlaying();
}
}
return null;
@ -306,9 +306,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
}
}
private void showNowPlaying(final Context context, final DownloadService downloadService, final Entry song, final PlayerState playerState)
private void showNowPlaying(final Context context, final MediaPlayerController mediaPlayerController, final Entry song, final PlayerState playerState)
{
if (context == null || downloadService == null || song == null || playerState == null)
if (context == null || mediaPlayerController == null || song == null || playerState == null)
{
return;
}
@ -387,7 +387,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
ImageView nowPlayingControlPlay = (ImageView) nowPlayingView.findViewById(R.id.now_playing_control_play);
SwipeDetector swipeDetector = new SwipeDetector(SubsonicTabActivity.this, downloadService);
SwipeDetector swipeDetector = new SwipeDetector(SubsonicTabActivity.this, mediaPlayerController);
setOnTouchListenerOnUiThread(nowPlayingView, swipeDetector);
setOnClickListenerOnUiThread(nowPlayingView, new OnClickListener()
@ -403,7 +403,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
@Override
public void onClick(View view)
{
downloadService.togglePlayPause();
mediaPlayerController.togglePlayPause();
}
});
@ -762,33 +762,9 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
}
}
public DownloadService getDownloadService()
public MediaPlayerController getMediaPlayerController()
{
// If service is not available, request it to start and wait for it.
for (int i = 0; i < 5; i++)
{
DownloadService downloadService = DownloadServiceImpl.getInstance();
if (downloadService != null)
{
return downloadService;
}
Log.w(TAG, "DownloadService not running. Attempting to start it.");
try
{
startService(new Intent(this, DownloadServiceImpl.class));
}
catch (IllegalStateException exception)
{
Log.w(TAG, "getDownloadService couldn't start DownloadServiceImpl because the application was in the background.");
return null;
}
Util.sleepQuietly(50L);
}
return DownloadServiceImpl.getInstance();
return mediaPlayerControllerLazy.getValue();
}
protected void warnIfNetworkOrStorageUnavailable()
@ -839,7 +815,7 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
void download(final boolean append, final boolean save, final boolean autoPlay, final boolean playNext, final boolean shuffle, final List<Entry> songs)
{
if (getDownloadService() == null)
if (getMediaPlayerController() == null)
{
return;
}
@ -851,16 +827,16 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
{
if (!append && !playNext)
{
getDownloadService().clear();
getMediaPlayerController().clear();
}
warnIfNetworkOrStorageUnavailable();
getDownloadService().download(songs, save, autoPlay, playNext, shuffle, false);
getMediaPlayerController().download(songs, save, autoPlay, playNext, shuffle, false);
String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
if (playlistName != null)
{
getDownloadService().setSuggestedPlaylistName(playlistName);
getMediaPlayerController().setSuggestedPlaylistName(playlistName);
}
if (autoPlay)
@ -1015,23 +991,23 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
Collections.sort(songs, new EntryByDiscAndTrackComparator());
}
DownloadService downloadService = getDownloadService();
if (!songs.isEmpty() && downloadService != null)
MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (!songs.isEmpty() && mediaPlayerController != null)
{
if (!append && !playNext && !unpin && !background)
{
downloadService.clear();
mediaPlayerController.clear();
}
warnIfNetworkOrStorageUnavailable();
if (!background)
{
if (unpin)
{
downloadService.unpin(songs);
mediaPlayerController.unpin(songs);
}
else
{
downloadService.download(songs, save, autoplay, playNext, shuffle, false);
mediaPlayerController.download(songs, save, autoplay, playNext, shuffle, false);
if (!append && Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this))
{
startActivityForResultWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
@ -1042,11 +1018,11 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
{
if (unpin)
{
downloadService.unpin(songs);
mediaPlayerController.unpin(songs);
}
else
{
downloadService.downloadBackground(songs, save);
mediaPlayerController.downloadBackground(songs, save);
}
}
}
@ -1374,15 +1350,15 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
protected class SwipeDetector implements OnTouchListener
{
public SwipeDetector(SubsonicTabActivity activity, final DownloadService downloadService)
public SwipeDetector(SubsonicTabActivity activity, final MediaPlayerController mediaPlayerController)
{
this.downloadService = downloadService;
this.mediaPlayerController = mediaPlayerController;
this.activity = activity;
}
private static final int MIN_DISTANCE = 30;
private float downX, downY, upX, upY;
private DownloadService downloadService;
private MediaPlayerController mediaPlayerController;
private SubsonicTabActivity activity;
@Override
@ -1409,12 +1385,12 @@ public class SubsonicTabActivity extends ResultActivity implements OnClickListen
// left or right
if (deltaX < 0)
{
downloadService.previous();
mediaPlayerController.previous();
return false;
}
if (deltaX > 0)
{
downloadService.next();
mediaPlayerController.next();
return false;
}
}

View File

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

View File

@ -17,8 +17,9 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadService;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
public class UltraSonicAppWidgetProvider extends AppWidgetProvider
@ -67,13 +68,13 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
}
/**
* Handle a change notification coming over from {@link DownloadService}
* Handle a change notification coming over from {@link MediaPlayerController}
*/
public void notifyChange(Context context, DownloadService service, boolean playing, boolean setAlbum)
public void notifyChange(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum)
{
if (hasInstances(context))
{
performUpdate(context, service, null, playing, setAlbum);
performUpdate(context, currentSong, null, playing, setAlbum);
}
}
@ -96,15 +97,14 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
/**
* Update all active widget instances by pushing changes
*/
private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing, boolean setAlbum)
private void performUpdate(Context context, MusicDirectory.Entry currentSong, int[] appWidgetIds, boolean playing, boolean setAlbum)
{
final Resources res = context.getResources();
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong();
String title = currentPlaying == null ? null : currentPlaying.getTitle();
String artist = currentPlaying == null ? null : currentPlaying.getArtist();
String album = currentPlaying == null ? null : currentPlaying.getAlbum();
String title = currentSong == null ? null : currentSong.getTitle();
String artist = currentSong == null ? null : currentSong.getArtist();
String album = currentSong == null ? null : currentSong.getAlbum();
CharSequence errorState = null;
// Show error message?
@ -117,7 +117,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
{
errorState = res.getText(R.string.widget_sdcard_missing);
}
else if (currentPlaying == null)
else if (currentSong == null)
{
errorState = res.getText(R.string.widget_initial_text);
}
@ -157,7 +157,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
// Set the cover art
try
{
Bitmap bitmap = currentPlaying == null ? null : FileUtil.getAlbumArtBitmap(context, currentPlaying, 240, true);
Bitmap bitmap = currentSong == null ? null : FileUtil.getAlbumArtBitmap(context, currentSong, 240, true);
if (bitmap == null)
{
@ -176,7 +176,7 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
}
// Link actions buttons to intents
linkButtons(context, views, currentPlaying != null);
linkButtons(context, views, currentSong != null);
pushUpdate(context, appWidgetIds, views);
}
@ -194,27 +194,30 @@ public class UltraSonicAppWidgetProvider extends AppWidgetProvider
Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
// Emulate media button clicks.
intent = new Intent("1");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);
views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
intent = new Intent("2"); // Use a unique action name to ensure a different PendingIntent to be created.
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);
views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
intent = new Intent("3"); // Use a unique action name to ensure a different PendingIntent to be created.
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);
views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
}
}

View File

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

View File

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

View File

@ -21,82 +21,63 @@ package org.moire.ultrasonic.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.KeyEvent;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
import kotlin.Lazy;
import static org.koin.java.standalone.KoinJavaComponent.inject;
/**
* @author Sindre Mehus
*/
public class MediaButtonIntentReceiver extends BroadcastReceiver
{
private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
@Override
public void onReceive(Context context, Intent intent)
{
if (Util.getMediaButtonsPreference(context))
String intentAction = intent.getAction();
// If media button are turned off and we received a media button, exit
if (!Util.getMediaButtonsPreference(context) &&
Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) return;
// Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets
if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction) &&
!Constants.CMD_PROCESS_KEYCODE.equals(intentAction)) return;
Bundle extras = intent.getExtras();
if (extras == null)
{
String intentAction = intent.getAction();
return;
}
if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) return;
Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT);
Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
Bundle extras = intent.getExtras();
if (extras == null)
{
return;
}
Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT);
Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
Intent serviceIntent = new Intent(context, DownloadServiceImpl.class);
try
{
Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE);
serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
lifecycleSupport.getValue().receiveIntent(serviceIntent);
try
if (isOrderedBroadcast())
{
context.startService(serviceIntent);
}
catch (IllegalStateException exception)
{
Log.i(TAG, "MediaButtonIntentReceiver couldn't start DownloadServiceImpl because the application was in the background.");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
KeyEvent keyEvent = (KeyEvent) event;
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getRepeatCount() == 0)
{
int keyCode = keyEvent.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY)
{
// TODO: The only time it is OK to start DownloadServiceImpl as a foreground service is when we now it will display its notification.
// When DownloadServiceImpl is refactored to a proper foreground service, this can be removed.
context.startForegroundService(serviceIntent);
Log.i(TAG, "MediaButtonIntentReceiver started DownloadServiceImpl as foreground service");
}
}
}
}
try
{
if (isOrderedBroadcast())
{
abortBroadcast();
}
}
catch (Exception x)
{
// Ignored.
abortBroadcast();
}
}
catch (Exception x)
{
// Ignored.
}
}
}

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

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

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;
/**
* This interface contains all functions which are necessary for the Application UI
* to control the Media Player implementation.
*
* @author Sindre Mehus
* @version $Id$
*/
public interface DownloadService
public interface MediaPlayerController
{
void download(List<Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, boolean newPlaylist);
void downloadBackground(List<Entry> songs, boolean save);
@ -53,38 +55,14 @@ public interface DownloadService
boolean getShowVisualization();
boolean getEqualizerAvailable();
boolean getVisualizerAvailable();
void setShowVisualization(boolean showVisualization);
void clear();
void clearBackground();
void clearIncomplete();
int size();
void remove(int which);
void remove(DownloadFile downloadFile);
long getDownloadListDuration();
List<DownloadFile> getSongs();
List<DownloadFile> getDownloads();
List<DownloadFile> getBackgroundDownloads();
int getCurrentPlayingIndex();
DownloadFile getCurrentPlaying();
DownloadFile getCurrentDownloading();
void play(int index);
void seekTo(int position);
@ -111,10 +89,6 @@ public interface DownloadService
void unpin(List<Entry> songs);
DownloadFile forSong(Entry song);
long getDownloadListUpdateRevision();
void setSuggestedPlaylistName(String name);
String getSuggestedPlaylistName();
@ -127,8 +101,6 @@ public interface DownloadService
boolean isJukeboxAvailable();
boolean isSharingAvailable();
void setJukeboxEnabled(boolean b);
void adjustJukeboxVolume(boolean up);
@ -137,15 +109,27 @@ public interface DownloadService
void setVolume(float volume);
void swap(boolean mainList, int from, int to);
void restore(List<Entry> songs, int currentPlayingIndex, int currentPlayingPosition, boolean autoPlay, boolean newPlaylist);
void stopJukeboxService();
void startJukeboxService();
void updateNotification();
void setSongRating(final int rating);
DownloadFile getCurrentPlaying();
int getPlaylistSize();
int getCurrentPlayingNumberOnPlaylist();
DownloadFile getCurrentDownloading();
List<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
public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception
{
DownloadService downloadService = DownloadServiceImpl.getInstance();
if (downloadService == null)
{
return new MusicDirectory();
}
Reader reader = null;
BufferedReader buffer = null;
try

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

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_VIDEOS = "subsonic.videos";
// Names for Intent Actions
public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE";
public static final String CMD_PLAY = "org.moire.ultrasonic.CMD_PLAY";
public static final String CMD_TOGGLEPAUSE = "org.moire.ultrasonic.CMD_TOGGLEPAUSE";
public static final String CMD_PAUSE = "org.moire.ultrasonic.CMD_PAUSE";
public static final String CMD_STOP = "org.moire.ultrasonic.CMD_STOP";
public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS";
public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT";
// Notification IDs.
public static final int NOTIFICATION_ID_PLAYING = 100;
@ -140,6 +149,8 @@ public final class Constants
// URL for project donations.
public static final String DONATION_URL = "http://www.subsonic.org/pages/premium.jsp";
public static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser";
public static final String ALBUM_ART_FILE = "folder.jpeg";
public static final String STARRED = "starred";
public static final String ALPHABETICAL_BY_NAME = "alphabeticalByName";

View File

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

View File

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

View File

@ -54,8 +54,7 @@ import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicServiceFactory;
import java.io.*;
@ -93,9 +92,8 @@ public class Util extends DownloadActivity
public static final String CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
public static final String CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged";
private static boolean hasFocus;
private static boolean pauseFocus;
private static boolean lowerFocus;
private static boolean mediaButtonsRegisteredForUI;
private static boolean mediaButtonsRegisteredForService;
private static final Map<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);
}
public static void registerMediaButtonEventReceiver(Context context)
public static void registerMediaButtonEventReceiver(Context context, boolean isService)
{
if (getMediaButtonsPreference(context))
{
if (isService) mediaButtonsRegisteredForService = true;
else mediaButtonsRegisteredForUI = true;
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()));
}
}
public static void unregisterMediaButtonEventReceiver(Context context)
public static void unregisterMediaButtonEventReceiver(Context context, boolean isService)
{
if (isService) mediaButtonsRegisteredForService = false;
else mediaButtonsRegisteredForUI = false;
// Do not unregister while there is an active part of the app which needs the control
if (mediaButtonsRegisteredForService || mediaButtonsRegisteredForUI) return;
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterMediaButtonEventReceiver(new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()));
Log.i(TAG, "MediaButtonEventReceiver unregistered.");
}
public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult)
@ -958,7 +966,7 @@ public class Util extends DownloadActivity
context.sendBroadcast(intent);
}
public static void broadcastA2dpMetaDataChange(Context context, DownloadService downloadService)
public static void broadcastA2dpMetaDataChange(Context context, int playerPosition, DownloadFile currentPlaying, int listSize, int id)
{
if (!Util.getShouldSendBluetoothNotifications(context))
{
@ -968,17 +976,9 @@ public class Util extends DownloadActivity
Entry song = null;
Intent avrcpIntent = new Intent(CM_AVRCP_METADATA_CHANGED);
if (downloadService != null)
{
DownloadFile entry = downloadService.getCurrentPlaying();
if (currentPlaying != null) song = currentPlaying.getSong();
if (entry != null)
{
song = entry.getSong();
}
}
if (downloadService == null || song == null)
if (song == null)
{
avrcpIntent.putExtra("track", "");
avrcpIntent.putExtra("track_name", "");
@ -1011,9 +1011,6 @@ public class Util extends DownloadActivity
String artist = song.getArtist();
String album = song.getAlbum();
Integer duration = song.getDuration();
Integer listSize = downloadService.getDownloads().size();
Integer id = downloadService.getCurrentPlayingIndex() + 1;
Integer playerPosition = downloadService.getPlayerPosition();
avrcpIntent.putExtra("track", title);
avrcpIntent.putExtra("track_name", title);
@ -1045,38 +1042,31 @@ public class Util extends DownloadActivity
context.sendBroadcast(avrcpIntent);
}
public static void broadcastA2dpPlayStatusChange(Context context, PlayerState state, DownloadService downloadService)
public static void broadcastA2dpPlayStatusChange(Context context, PlayerState state, Entry currentSong, Integer listSize, Integer id, Integer playerPosition)
{
if (!Util.getShouldSendBluetoothNotifications(context) || downloadService == null)
if (!Util.getShouldSendBluetoothNotifications(context))
{
return;
}
DownloadFile currentPlaying = downloadService.getCurrentPlaying();
if (currentPlaying != null)
if (currentSong != null)
{
Intent avrcpIntent = new Intent(CM_AVRCP_PLAYSTATE_CHANGED);
Entry song = currentPlaying.getSong();
if (song == null)
if (currentSong == null)
{
return;
}
if (song != currentSong)
if (currentSong != currentSong)
{
currentSong = song;
Util.currentSong = currentSong;
}
String title = song.getTitle();
String artist = song.getArtist();
String album = song.getAlbum();
Integer duration = song.getDuration();
Integer listSize = downloadService.getDownloads().size();
Integer id = downloadService.getCurrentPlayingIndex() + 1;
Integer playerPosition = downloadService.getPlayerPosition();
String title = currentSong.getTitle();
String artist = currentSong.getArtist();
String album = currentSong.getAlbum();
Integer duration = currentSong.getDuration();
avrcpIntent.putExtra("track", title);
avrcpIntent.putExtra("track_name", title);
@ -1089,7 +1079,7 @@ public class Util extends DownloadActivity
if (Util.getShouldSendBluetoothAlbumArt(context))
{
File albumArtFile = FileUtil.getAlbumArtFile(context, song);
File albumArtFile = FileUtil.getAlbumArtFile(context, currentSong);
avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath());
avrcpIntent.putExtra("cover", albumArtFile.getAbsolutePath());
}
@ -1187,60 +1177,6 @@ public class Util extends DownloadActivity
return size;
}
public static void requestAudioFocus(final Context context)
{
if (!hasFocus)
{
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
hasFocus = true;
audioManager.requestAudioFocus(new OnAudioFocusChangeListener()
{
@Override
public void onAudioFocusChange(int focusChange)
{
DownloadService downloadService = (DownloadService) context;
if ((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !downloadService.isJukeboxEnabled())
{
if (downloadService.getPlayerState() == PlayerState.STARTED)
{
SharedPreferences preferences = getPreferences(context);
int lossPref = Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1"));
if (lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK))
{
lowerFocus = true;
downloadService.setVolume(0.1f);
}
else if (lossPref == 0 || (lossPref == 1))
{
pauseFocus = true;
downloadService.pause();
}
}
}
else if (focusChange == AudioManager.AUDIOFOCUS_GAIN)
{
if (pauseFocus)
{
pauseFocus = false;
downloadService.start();
}
else if (lowerFocus)
{
lowerFocus = false;
downloadService.setVolume(1.0f);
}
}
else if (focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isJukeboxEnabled())
{
hasFocus = false;
downloadService.pause();
audioManager.abandonAudioFocus(this);
}
}
}, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
}
}
public static int getMinDisplayMetric(Context context)
{
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
@ -1289,58 +1225,58 @@ public class Util extends DownloadActivity
views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
// Emulate media button clicks.
intent = new Intent("1");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 1, intent, 0);
views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
intent = new Intent("2");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 2, intent, 0);
views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
intent = new Intent("3");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 3, intent, 0);
views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
intent = new Intent("4");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 4, intent, 0);
views.setOnClickPendingIntent(R.id.control_stop, pendingIntent);
intent = new Intent("RATE_1");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_1));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 5, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_1, pendingIntent);
intent = new Intent("RATE_2");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_2));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 6, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_2, pendingIntent);
intent = new Intent("RATE_3");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_3));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 7, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_3, pendingIntent);
intent = new Intent("RATE_4");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_4));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 8, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_4, pendingIntent);
intent = new Intent("RATE_5");
intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setPackage(context.getPackageName());
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_5));
pendingIntent = PendingIntent.getService(context, 0, intent, 0);
pendingIntent = PendingIntent.getBroadcast(context, 9, intent, 0);
views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent);
}

View File

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

View File

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

View File

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

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.no_time">-:--</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_button">Baixar Player MX</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 MX Player</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_missing">Sem cartão SD</string>
@ -371,7 +371,7 @@
<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_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_flash">Flash</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.no_time">&#8212;:&#8212;&#8212;</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_button">Descarregar Player MX</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 MX Player</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_missing">Sem cartão SD</string>
@ -371,7 +371,7 @@
<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_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_flash">Flash</string>
<string name="menu.share">Compartilhar</string>