diff options
18 files changed, 454 insertions, 222 deletions
@@ -2,4 +2,5 @@ subsonic-android/.classpath subsonic-android/.project
subsonic-android/bin/*
subsonic-android/gen/*
-subsonic-android/private/*
\ No newline at end of file +subsonic-android/private/*
+subsonic-android/nbandroid/*
\ No newline at end of file diff --git a/DragSortListView b/DragSortListView -Subproject c4166a8fb4fd46688061ec915d4fa51de374e0a +Subproject 76426129c8d212be4fbf8174a2eb6e90ef37985 @@ -10,9 +10,6 @@ android update project --path ./ Roadmap of major planned features in rough order that I plan to work on them in (little features get sprinkled in wherever): -Gapless Playback (may not be possible to get perfect) - -Media Player chaining (in progress) - -4.1+: setNextMediaPlayer? HLS Video -Display video where album art is currently (double tap to fullscreen) -Videos can play inline with songs diff --git a/subsonic-android/AndroidManifest.xml b/subsonic-android/AndroidManifest.xml index 5ca6cf49..8b6b704d 100644 --- a/subsonic-android/AndroidManifest.xml +++ b/subsonic-android/AndroidManifest.xml @@ -2,8 +2,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="github.daneren2005.dsub"
android:installLocation="internalOnly"
- android:versionCode="36"
- android:versionName="3.7.4">
+ android:versionCode="38"
+ android:versionName="3.8.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
@@ -15,6 +15,8 @@ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
+
+ <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="16"/>
diff --git a/subsonic-android/res/layout/album_list_item.xml b/subsonic-android/res/layout/album_list_item.xml index d76f105f..0b84b4f3 100644 --- a/subsonic-android/res/layout/album_list_item.xml +++ b/subsonic-android/res/layout/album_list_item.xml @@ -1,8 +1,9 @@ <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
<ImageView
android:id="@+id/album_coverart"
diff --git a/subsonic-android/res/layout/download_playlist.xml b/subsonic-android/res/layout/download_playlist.xml index dc77826d..515728bb 100644 --- a/subsonic-android/res/layout/download_playlist.xml +++ b/subsonic-android/res/layout/download_playlist.xml @@ -20,24 +20,11 @@ android:padding="10dip"/>
<com.mobeta.android.dslv.DragSortListView
- xmlns:dslv="http://schemas.android.com/apk/res/github.daneren2005.dsub"
+ style="@style/DragDropListView"
android:id="@+id/download_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
- android:cacheColorHint="#00000000"
- dslv:drag_enabled="true"
- dslv:collapsed_height="1dp"
- dslv:drag_scroll_start="1.0"
- dslv:max_drag_scroll_speed="2.0"
- dslv:float_alpha="0.6"
- dslv:slide_shuffle_speed="0.3"
- dslv:track_drag_sort="false"
- dslv:use_default_controller="true"
- dslv:drag_handle_id="@id/drag_handle"
- dslv:sort_enabled="true"
- dslv:remove_enabled="false"
- dslv:remove_mode="flingRemove"
- dslv:drag_start_mode="onLongPress"/>
+ android:cacheColorHint="#00000000"/>
</LinearLayout>
\ No newline at end of file diff --git a/subsonic-android/res/layout/select_album.xml b/subsonic-android/res/layout/select_album.xml index a5802246..b688a95b 100644 --- a/subsonic-android/res/layout/select_album.xml +++ b/subsonic-android/res/layout/select_album.xml @@ -19,13 +19,14 @@ android:layout_height="wrap_content"
android:padding="10dip"/>
- <ListView android:id="@+id/select_album_entries"
- android:textFilterEnabled="true"
- android:layout_width="fill_parent"
- android:layout_height="0dip"
- android:layout_weight="1.0"
- android:fastScrollEnabled="true"
- />
+ <com.mobeta.android.dslv.DragSortListView
+ style="@style/DragDropListView"
+ android:id="@+id/select_album_entries"
+ android:textFilterEnabled="true"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
<include layout="@layout/button_bar"/>
diff --git a/subsonic-android/res/values/styles.xml b/subsonic-android/res/values/styles.xml index 7ac30ad1..2d53a8d9 100644 --- a/subsonic-android/res/values/styles.xml +++ b/subsonic-android/res/values/styles.xml @@ -25,4 +25,20 @@ <item name="android:background">@drawable/menubar_button</item> <item name="android:textColor">?android:textColorPrimary</item> </style> + + <style name="DragDropListView"> + <item name="drag_enabled">true</item> + <item name="collapsed_height">1dp</item> + <item name="drag_scroll_start">1.0</item> + <item name="max_drag_scroll_speed">2.0</item> + <item name="float_alpha">0.6</item> + <item name="slide_shuffle_speed">0.3</item> + <item name="track_drag_sort">false</item> + <item name="use_default_controller">true</item> + <item name="drag_handle_id">@id/drag_handle</item> + <item name="sort_enabled">true</item> + <item name="remove_enabled">false</item> + <item name="remove_mode">flingRemove</item> + <item name="drag_start_mode">onLongPress</item> + </style> </resources>
\ No newline at end of file diff --git a/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java index 954a3f02..f487b6e6 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java @@ -83,12 +83,13 @@ import github.daneren2005.dsub.view.AutoRepeatButton; import java.util.ArrayList; import java.util.concurrent.ScheduledFuture; import com.mobeta.android.dslv.*; +import github.daneren2005.dsub.service.DownloadServiceImpl; public class DownloadActivity extends SubsonicTabActivity implements OnGestureListener { private static final String TAG = DownloadActivity.class.getSimpleName(); private static final int DIALOG_SAVE_PLAYLIST = 100; - private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5; + private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10; private static final int COLOR_BUTTON_ENABLED = Color.rgb(51, 181, 229); private static final int COLOR_BUTTON_DISABLED = Color.rgb(206, 213, 211); private static final int INCREMENT_TIME = 5000; @@ -983,50 +984,50 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - - DownloadService downloadService = getDownloadService(); - if (downloadService == null) { - return false; - } + DownloadService downloadService = getDownloadService(); + if (downloadService == null) { + return false; + } // Right to Left swipe if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) { - downloadService.next(); - onCurrentChanged(); - onProgressChanged(); - } + warnIfNetworkOrStorageUnavailable(); + if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) { + downloadService.next(); + onCurrentChanged(); + onProgressChanged(); + } return true; } // Left to Right swipe - if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - downloadService.previous(); - onCurrentChanged(); - onProgressChanged(); + else if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + downloadService.previous(); + onCurrentChanged(); + onProgressChanged(); return true; } - // Top to Bottom swipe - if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - downloadService.seekTo(downloadService.getPlayerPosition() + 30000); - onProgressChanged(); - return true; - } - - // Bottom to Top swipe - if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { - warnIfNetworkOrStorageUnavailable(); - downloadService.seekTo(downloadService.getPlayerPosition() - 8000); - onProgressChanged(); - return true; - } + // Top to Bottom swipe + else if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + downloadService.pause(); + onProgressChanged(); + return true; + } + + // Bottom to Top swipe + else if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) { + warnIfNetworkOrStorageUnavailable(); + start(); + onCurrentChanged(); + onProgressChanged(); + return true; + } - return false; - } + return false; + } private void toggleNowPlaying() { nowPlaying = !nowPlaying; diff --git a/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java index 926467ec..5016135c 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java @@ -38,6 +38,7 @@ import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.service.*; import github.daneren2005.dsub.util.*; +import com.mobeta.android.dslv.*; import java.io.File; import java.util.*; @@ -46,7 +47,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity { private static final String TAG = SelectAlbumActivity.class.getSimpleName(); - private ListView entryList; + private DragSortListView entryList; private View footer; private View emptyView; private boolean hideButtons = false; @@ -54,6 +55,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity { private Boolean licenseValid; private boolean showHeader = true; private EntryAdapter entryAdapter; + private List<MusicDirectory.Entry> entries; /** * Called when the activity is first created. @@ -63,7 +65,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity { super.onCreate(savedInstanceState); setContentView(R.layout.select_album); - entryList = (ListView) findViewById(R.id.select_album_entries); + entryList = (DragSortListView) findViewById(R.id.select_album_entries); footer = LayoutInflater.from(this).inflate(R.layout.select_album_footer, entryList, false); entryList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); @@ -87,6 +89,20 @@ public class SelectAlbumActivity extends SubsonicTabActivity { } } }); + entryList.setDropListener(new DragSortListView.DropListener() { + @Override + public void drop(int from, int to) { + int max = entries.size(); + if(to >= max) { + to = max - 1; + } + else if(to < 0) { + to = 0; + } + entries.add(to, entries.remove(from)); + entryAdapter.notifyDataSetChanged(); + } + }); moreButton = (Button) footer.findViewById(R.id.select_album_more); emptyView = findViewById(R.id.select_album_empty); @@ -387,6 +403,10 @@ public class SelectAlbumActivity extends SubsonicTabActivity { } else if ("starred".equals(albumListType)) { setTitle(R.string.main_albums_starred); } + + if (!"starred".equals(albumListType)) { + entryList.setDragEnabled(false); + } new LoadTask() { @Override @@ -678,7 +698,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity { @Override protected void done(Pair<MusicDirectory, Boolean> result) { - List<MusicDirectory.Entry> entries = result.getFirst().getChildren(); + entries = result.getFirst().getChildren(); int songCount = 0; for (MusicDirectory.Entry entry : entries) { diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java index 59f78252..3fa38bdf 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java @@ -93,6 +93,9 @@ public class DownloadFile { public synchronized void download() { FileUtil.createDirectoryForParent(saveFile); failed = false; + if(!partialFile.exists()) { + bitRate = Util.getMaxBitrate(context); + } downloadTask = new DownloadTask(); downloadTask.start(); } @@ -317,6 +320,9 @@ public class DownloadFile { wifiLock.release(); } new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace(); + if(DownloadServiceImpl.getInstance() != null) { + ((DownloadServiceImpl)DownloadServiceImpl.getInstance()).checkDownloads(); + } } } diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java index fab4d6a1..ad6591a5 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java @@ -55,6 +55,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.media.AudioManager; import android.media.MediaPlayer; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; @@ -84,6 +85,9 @@ public class DownloadServiceImpl extends Service implements DownloadService { private final IBinder binder = new SimpleServiceBinder<DownloadService>(this); private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private boolean nextSetup = false; + private boolean isPartial = true; private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>(); private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>(); private final Handler handler = new Handler(); @@ -95,9 +99,12 @@ public class DownloadServiceImpl extends Service implements DownloadService { private final Scrobbler scrobbler = new Scrobbler(); private final JukeboxService jukeboxService = new JukeboxService(this); private DownloadFile currentPlaying; + private DownloadFile nextPlaying; private DownloadFile currentDownloading; private CancellableTask bufferTask; + private CancellableTask nextPlayingTask; private PlayerState playerState = IDLE; + private PlayerState nextPlayerState = IDLE; private boolean shufflePlay; private long revision; private static DownloadService instance; @@ -119,20 +126,10 @@ public class DownloadServiceImpl extends Service implements DownloadService { private int timerDuration; static { - try { - EqualizerController.checkAvailable(); - equalizerAvailable = true; - } catch (Throwable t) { - equalizerAvailable = false; - } - } - static { - try { - VisualizerController.checkAvailable(); - visualizerAvailable = true; - } catch (Throwable t) { - visualizerAvailable = false; - } + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + equalizerAvailable = true; + visualizerAvailable = true; + } } @Override @@ -149,6 +146,17 @@ public class DownloadServiceImpl extends Service implements DownloadService { return false; } }); + + nextMediaPlayer = new MediaPlayer(); + nextMediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK); + + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) { + handleErrorNext(new Exception("MediaPlayer error: " + what + " (" + more + ")")); + return false; + } + }); Util.registerMediaButtonEventReceiver(this); @@ -191,9 +199,6 @@ public class DownloadServiceImpl extends Service implements DownloadService { @Override public void onDestroy() { super.onDestroy(); - if(proxy != null) { - proxy.stop(); - } if(currentPlaying != null) currentPlaying.setPlaying(false); if(sleepTimer != null){ sleepTimer.cancel(); @@ -201,6 +206,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { } lifecycleSupport.onDestroy(); mediaPlayer.release(); + nextMediaPlayer.release(); shufflePlayBuffer.shutdown(); if (equalizerController != null) { equalizerController.release(); @@ -212,6 +218,13 @@ public class DownloadServiceImpl extends Service implements DownloadService { mRemoteControl.unregister(this); mRemoteControl = null; } + + if(bufferTask != null) { + bufferTask.cancel(); + } + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + } instance = null; } @@ -316,6 +329,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { revision++; lifecycleSupport.serializeDownloadQueue(); updateJukeboxPlaylist(); + setNextPlaying(); } @Override @@ -326,6 +340,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { @Override public void setRepeatMode(RepeatMode repeatMode) { Util.setRepeatMode(this, repeatMode); + setNextPlaying(); } @Override @@ -423,6 +438,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { lifecycleSupport.serializeDownloadQueue(); } updateJukeboxPlaylist(); + setNextPlaying(); } @Override @@ -445,6 +461,9 @@ public class DownloadServiceImpl extends Service implements DownloadService { revision++; lifecycleSupport.serializeDownloadQueue(); updateJukeboxPlaylist(); + if(downloadFile == nextPlaying) { + setNextPlaying(); + } } @Override @@ -489,6 +508,37 @@ public class DownloadServiceImpl extends Service implements DownloadService { Util.hidePlayingNotification(this, this, handler); } } + + synchronized void setNextPlaying() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + index = index + 1; + break; + case ALL: + index = (index + 1) % size(); + break; + case SINGLE: + break; + default: + break; + } + } + + if(index < size() && index != -1) { + nextPlaying = downloadList.get(index); + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.start(); + } else { + nextPlaying = null; + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + } + nextPlayingTask = null; + setNextPlayerState(IDLE); + } + } @Override public synchronized int getCurrentPlayingIndex() { @@ -544,8 +594,10 @@ public class DownloadServiceImpl extends Service implements DownloadService { reset(); setCurrentPlaying(null, false); } else { + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + } setCurrentPlaying(index, start); - checkDownloads(); if (start) { if (jukeboxEnabled) { jukeboxService.skip(getCurrentPlayingIndex(), 0); @@ -554,12 +606,29 @@ public class DownloadServiceImpl extends Service implements DownloadService { bufferAndPlay(); } } + checkDownloads(); + setNextPlaying(); } } + + private synchronized void playNext(boolean start) { + // Swap the media players since nextMediaPlayer is ready to play + if(start) { + nextMediaPlayer.start(); + } else { + Log.i(TAG, "nextMediaPlayer already playing"); + } + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying, true); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false); + setNextPlaying(); + } /** Plays or resumes the playback, depending on the current player state. */ - public synchronized void togglePlayPause() - { + public synchronized void togglePlayPause() { if (playerState == PAUSED || playerState == COMPLETED) { start(); } else if (playerState == STOPPED || playerState == IDLE) { @@ -576,6 +645,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); } else { mediaPlayer.seekTo(position); + cachedPosition = position; } } catch (Exception x) { handleError(x); @@ -659,10 +729,6 @@ public class DownloadServiceImpl extends Service implements DownloadService { if (bufferTask != null) { bufferTask.cancel(); } - if(proxy != null) { - proxy.stop(); - proxy = null; - } try { setPlayerState(IDLE); mediaPlayer.setOnErrorListener(null); @@ -682,7 +748,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { if (jukeboxEnabled) { return jukeboxService.getPositionSeconds() * 1000; } else { - return mediaPlayer.getCurrentPosition(); + return cachedPosition; } } catch (Exception x) { handleError(x); @@ -713,7 +779,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { return playerState; } - synchronized void setPlayerState(PlayerState playerState) { + public synchronized void setPlayerState(final PlayerState playerState) { Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); if (playerState == PAUSED) { @@ -747,28 +813,34 @@ public class DownloadServiceImpl extends Service implements DownloadService { Runnable runnable = new Runnable() { @Override public void run() { - handler.post(new Runnable() { - @Override - public void run() { - if(mediaPlayer != null && getPlayerState() == STARTED) { - try { - cachedPosition = mediaPlayer.getCurrentPosition(); - } catch(Exception e) { - executorService.shutdown(); - } - } + if(mediaPlayer != null && playerState == STARTED) { + try { + cachedPosition = mediaPlayer.getCurrentPosition(); + } catch(Exception e) { + executorService.shutdown(); } - }); + } } }; executorService = Executors.newSingleThreadScheduledExecutor(); executorService.scheduleWithFixedDelay(runnable, 200L, 200L, TimeUnit.MILLISECONDS); - } else { + } else if(playerState != STARTED) { if(executorService != null && !executorService.isShutdown()) { executorService.shutdownNow(); } } } + + private synchronized void setPlayerStateCompleted() { + Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")"); + this.playerState = PlayerState.COMPLETED; + scrobbler.scrobble(this, currentPlaying, true); + } + + private synchronized void setNextPlayerState(PlayerState playerState) { + Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")"); + this.nextPlayerState = playerState; + } @Override public void setSuggestedPlaylistName(String name) { @@ -839,127 +911,187 @@ public class DownloadServiceImpl extends Service implements DownloadService { } private synchronized void bufferAndPlay() { - reset(); + if(playerState != PREPARED) { + reset(); - bufferTask = new BufferTask(currentPlaying, 0); - bufferTask.start(); + bufferTask = new BufferTask(currentPlaying, 0); + bufferTask.start(); + } else { + doPlay(currentPlaying, 0, true); + } } private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { try { final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - final boolean isPartial = file.equals(downloadFile.getPartialFile()); + isPartial = file.equals(downloadFile.getPartialFile()); downloadFile.updateModificationDate(); - mediaPlayer.setOnCompletionListener(null); - mediaPlayer.reset(); - setPlayerState(IDLE); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - String dataSource = file.getPath(); - if(isPartial) { - if (proxy == null) { - proxy = new StreamProxy(this); - proxy.start(); - } - dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), dataSource); - Log.i(TAG, "Data Source: " + dataSource); - } - mediaPlayer.setDataSource(dataSource); - setPlayerState(PREPARING); - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { - public void onPrepared(MediaPlayer mediaPlayer) { - try { - setPlayerState(PREPARED); + if(playerState == PlayerState.PREPARED) { + if (start) { + mediaPlayer.start(); + setPlayerState(STARTED); + } else { + setPlayerState(PAUSED); + } + } else { + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + setPlayerState(IDLE); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + String dataSource = file.getPath(); + if(isPartial) { + if (proxy == null) { + proxy = new StreamProxy(this); + proxy.start(); + } + dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), dataSource); + Log.i(TAG, "Data Source: " + dataSource); + } + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + Log.i(TAG, "Buffered " + percent + "%"); + } + }); - synchronized (DownloadServiceImpl.this) { - if (position != 0) { - Log.i(TAG, "Restarting player from position " + position); - mediaPlayer.seekTo(position); - } - cachedPosition = position; + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mediaPlayer) { + try { + setPlayerState(PREPARED); - if (start) { - mediaPlayer.start(); - setPlayerState(STARTED); - } else { - setPlayerState(PAUSED); + synchronized (DownloadServiceImpl.this) { + if (position != 0) { + Log.i(TAG, "Restarting player from position " + position); + mediaPlayer.seekTo(position); + } + cachedPosition = position; + + if (start) { + mediaPlayer.start(); + setPlayerState(STARTED); + } else { + setPlayerState(PAUSED); + } } + + lifecycleSupport.serializeDownloadQueue(); + } catch (Exception x) { + handleError(x); } + } + }); + } - lifecycleSupport.serializeDownloadQueue(); + setupHandlers(downloadFile, isPartial); + + if(playerState == PREPARING) { + mediaPlayer.prepareAsync(); + } + } catch (Exception x) { + handleError(x); + } + } + + private synchronized void setupNext(final DownloadFile downloadFile) { + try { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + nextMediaPlayer.setOnCompletionListener(null); + nextMediaPlayer.reset(); + setNextPlayerState(IDLE); + nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + nextMediaPlayer.setDataSource(file.getPath()); + setNextPlayerState(PREPARING); + + nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mediaPlayer) { + try { + setNextPlayerState(PREPARED); + // TODO: Whenever the completing early issue is fixed, remove !isPartial to get gapless playback on streams as well + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && playerState == PlayerState.STARTED && !isPartial) { + DownloadServiceImpl.this.mediaPlayer.setNextMediaPlayer(nextMediaPlayer); + nextSetup = true; + } } catch (Exception x) { - handleError(x); + handleErrorNext(x); } } }); - - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mediaPlayer) { - - // Acquire a temporary wakelock, since when we return from - // this callback the MediaPlayer will release its wakelock - // and allow the device to go to sleep. - wakeLock.acquire(60000); - - setPlayerState(COMPLETED); - - if (!file.equals(downloadFile.getPartialFile())) { - onSongCompleted(); - return; - } - - // If file is not completely downloaded, restart the playback from the current position. - int pos = cachedPosition; - synchronized (DownloadServiceImpl.this) { - int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; - Log.i(TAG, "Ending position " + pos + " of " + duration); - if(downloadFile.isWorkDone()) { - // Reached the end of the song - if(Math.abs(duration - pos) < 10000) { - onSongCompleted(); - // Complete was called early even though file is fully buffered - } else { - Log.i(TAG, "Requesting restart from " + pos + " of " + duration); - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); - } - } else { - Log.i(TAG, "Requesting restart from " + pos + " of " + duration); - reset(); - bufferTask = new BufferTask(downloadFile, pos); - bufferTask.start(); - } - } - } - }); - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { - Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile); - int pos = cachedPosition; - reset(); - downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); - downloadFile.setPlaying(true); + Log.w(TAG, "Error on playing next " + "(" + what + ", " + extra + "): " + downloadFile); return true; } }); - mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(MediaPlayer mp, int percent) { - Log.i(TAG, "Buffered " + percent + "%"); - } - }); - - mediaPlayer.prepareAsync(); + nextMediaPlayer.prepareAsync(); } catch (Exception x) { - handleError(x); + handleErrorNext(x); } - } + } + + private synchronized void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) { + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile); + int pos = cachedPosition; + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + return true; + } + }); + + final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + setPlayerStateCompleted(); + + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock.acquire(60000); + + int pos = cachedPosition; + Log.i(TAG, "Ending position " + pos + " of " + duration); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) { + if(nextPlaying != null && nextPlayerState == PlayerState.PREPARED) { + if(!nextSetup) { + playNext(true); + } else { + nextSetup = false; + playNext(false); + } + } else { + onSongCompleted(); + } + return; + } + + // If file is not completely downloaded, restart the playback from the current position. + synchronized (DownloadServiceImpl.this) { + if(downloadFile.isWorkDone()) { + // Complete was called early even though file is fully buffered + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } else { + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + bufferTask = new BufferTask(downloadFile, pos); + bufferTask.start(); + } + } + } + }); + } @Override public void setSleepTimerDuration(int duration){ @@ -1029,6 +1161,11 @@ public class DownloadServiceImpl extends Service implements DownloadService { mediaPlayer.reset(); setPlayerState(IDLE); } + private void handleErrorNext(Exception x) { + Log.w(TAG, "Next Media player error: " + x, x); + nextMediaPlayer.reset(); + setNextPlayerState(IDLE); + } protected synchronized void checkDownloads() { if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { @@ -1046,7 +1183,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) { return; } - + // Need to download current playing? if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { // Cancel current download, if necessary. @@ -1061,6 +1198,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { // Find a suitable target for download. else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) { + currentDownloading = null; int n = size(); int preloaded = 0; @@ -1075,6 +1213,9 @@ public class DownloadServiceImpl extends Service implements DownloadService { currentDownloading = downloadFile; currentDownloading.download(); cleanupCandidates.add(currentDownloading); + if(i == (start + 1)) { + setNextPlayerState(DOWNLOADING); + } break; } } else if (currentPlaying != downloadFile) { @@ -1191,7 +1332,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { } @Override - public void execute() { + public void execute() { setPlayerState(DOWNLOADING); while (!bufferComplete()) { @@ -1216,4 +1357,47 @@ public class DownloadServiceImpl extends Service implements DownloadService { return "BufferTask (" + downloadFile + ")"; } } + + private class CheckCompletionTask extends CancellableTask { + private final DownloadFile downloadFile; + private final File partialFile; + + public CheckCompletionTask(DownloadFile downloadFile) { + setNextPlayerState(PlayerState.IDLE); + this.downloadFile = downloadFile; + if(downloadFile != null) { + partialFile = downloadFile.getPartialFile(); + } else { + partialFile = null; + } + } + + @Override + public void execute() { + if(downloadFile == null) { + return; + } + + while (!bufferComplete()) { + Util.sleepQuietly(5000L); + if (isCancelled()) { + return; + } + } + + // Do something + setupNext(downloadFile); + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + ")"); + return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); + } + + @Override + public String toString() { + return "CheckCompletionTask (" + downloadFile + ")"; + } + } } diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java index e04fc00c..765e216b 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java @@ -167,7 +167,7 @@ public class DownloadServiceLifecycleSupport { public void onDestroy() { executorService.shutdown(); - serializeDownloadQueue(); + serializeDownloadQueueNow(); downloadService.clear(false); downloadService.unregisterReceiver(ejectEventReceiver); downloadService.unregisterReceiver(headsetEventReceiver); @@ -184,6 +184,19 @@ public class DownloadServiceLifecycleSupport { public void serializeDownloadQueue() { new SerializeTask().execute(); } + + public void serializeDownloadQueueNow() { + List<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, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); + } private void deserializeDownloadQueue() { State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER); @@ -224,7 +237,9 @@ public class DownloadServiceLifecycleSupport { break; case RemoteControlClient.FLAG_KEY_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PLAY: - downloadService.start(); + if(downloadService.getPlayerState() != PlayerState.STARTED) { + downloadService.start(); + } break; case RemoteControlClient.FLAG_KEY_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PAUSE: @@ -274,18 +289,8 @@ public class DownloadServiceLifecycleSupport { private class SerializeTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { - List<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, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); - FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); - + serializeDownloadQueueNow(); return null; } } -}
\ No newline at end of file +} diff --git a/subsonic-android/src/github/daneren2005/dsub/service/JukeboxService.java b/subsonic-android/src/github/daneren2005/dsub/service/JukeboxService.java index feeae2a7..96b82336 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/JukeboxService.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/JukeboxService.java @@ -119,7 +119,9 @@ public class JukeboxService { // Track change? Integer index = jukeboxStatus.getCurrentPlayingIndex(); if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) { + downloadService.setPlayerState(PlayerState.COMPLETED); downloadService.setCurrentPlaying(index, true); + downloadService.setPlayerState(PlayerState.STARTED); } } diff --git a/subsonic-android/src/github/daneren2005/dsub/updates/Updater.java b/subsonic-android/src/github/daneren2005/dsub/updates/Updater.java index 69cdb642..3f8876b9 100644 --- a/subsonic-android/src/github/daneren2005/dsub/updates/Updater.java +++ b/subsonic-android/src/github/daneren2005/dsub/updates/Updater.java @@ -50,6 +50,7 @@ public class Updater { if(version > lastVersion) {
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
Log.i(TAG, "Updating from version " + lastVersion + " to " + version);
for(Updater updater: updaters) {
diff --git a/subsonic-android/src/github/daneren2005/dsub/util/CacheCleaner.java b/subsonic-android/src/github/daneren2005/dsub/util/CacheCleaner.java index 3b88f00e..c69a595c 100644 --- a/subsonic-android/src/github/daneren2005/dsub/util/CacheCleaner.java +++ b/subsonic-android/src/github/daneren2005/dsub/util/CacheCleaner.java @@ -66,6 +66,10 @@ public class CacheCleaner { } private long getMinimumDelete(List<File> files) { + if(files.size() == 0) { + return 0L; + } + long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L; long bytesUsedBySubsonic = 0L; diff --git a/subsonic-android/src/github/daneren2005/dsub/util/ModalBackgroundTask.java b/subsonic-android/src/github/daneren2005/dsub/util/ModalBackgroundTask.java index 1954c474..8873b62c 100644 --- a/subsonic-android/src/github/daneren2005/dsub/util/ModalBackgroundTask.java +++ b/subsonic-android/src/github/daneren2005/dsub/util/ModalBackgroundTask.java @@ -85,7 +85,11 @@ public abstract class ModalBackgroundTask<T> extends BackgroundTask<T> { getHandler().post(new Runnable() { @Override public void run() { - progressDialog.dismiss(); + try { + progressDialog.dismiss(); + } catch(Exception e) { + Log.w(TAG, "Failed to dismiss dialog"); + } done(result); } }); diff --git a/subsonic-android/src/github/daneren2005/dsub/view/UpdateView.java b/subsonic-android/src/github/daneren2005/dsub/view/UpdateView.java index 9cc627cc..9fbaccf6 100644 --- a/subsonic-android/src/github/daneren2005/dsub/view/UpdateView.java +++ b/subsonic-android/src/github/daneren2005/dsub/view/UpdateView.java @@ -34,9 +34,9 @@ public class UpdateView extends LinearLayout { public UpdateView(Context context) {
super(context);
- setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.FILL_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
+ setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
INSTANCES.put(this, null);
int instanceCount = INSTANCES.size();
|