diff options
8 files changed, 339 insertions, 75 deletions
diff --git a/subsonic-android/AndroidManifest.xml b/subsonic-android/AndroidManifest.xml index b8513db8..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="37"
- android:versionName="3.7.5">
+ 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/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java index 46a32ed4..5016135c 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java @@ -403,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 diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java index 834b85d9..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(); } diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java index 91a3a5ea..4e7bbbb0 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java @@ -61,6 +61,7 @@ import android.os.IBinder; import android.os.PowerManager; import android.util.Log; import github.daneren2005.dsub.activity.SubsonicTabActivity; +import java.net.URLEncoder; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -120,25 +121,16 @@ public class DownloadServiceImpl extends Service implements DownloadService { private boolean showVisualization; private boolean jukeboxEnabled; private ScheduledExecutorService executorService; + private StreamProxy proxy; private Timer sleepTimer; 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 @@ -505,7 +497,6 @@ public class DownloadServiceImpl extends Service implements DownloadService { if (currentPlaying != null) { Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - currentPlaying.setPlaying(true); mRemoteControl.updateMetadata(this, currentPlaying.getSong()); } else { Util.broadcastNewTrackInfo(this, null); @@ -535,6 +526,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { } } + nextSetup = false; if(index < size() && index != -1) { nextPlaying = downloadList.get(index); nextPlayingTask = new CheckCompletionTask(nextPlaying); @@ -634,6 +626,12 @@ public class DownloadServiceImpl extends Service implements DownloadService { setPlayerState(PlayerState.STARTED); setupHandlers(currentPlaying, false); setNextPlaying(); + + // Proxy should not be being used here since the next player was already setup to play + if(proxy != null) { + proxy.stop(); + proxy = null; + } } /** Plays or resumes the playback, depending on the current player state. */ @@ -654,6 +652,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); } else { mediaPlayer.seekTo(position); + cachedPosition = position; } } catch (Exception x) { handleError(x); @@ -756,7 +755,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); @@ -787,7 +786,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { return playerState; } - public synchronized void setPlayerState(PlayerState playerState) { + public synchronized void setPlayerState(final PlayerState playerState) { Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); if (playerState == PAUSED) { @@ -821,23 +820,18 @@ 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(); } @@ -936,58 +930,69 @@ public class DownloadServiceImpl extends Service implements DownloadService { private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { try { + downloadFile.setPlaying(true); final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); isPartial = file.equals(downloadFile.getPartialFile()); downloadFile.updateModificationDate(); - if(playerState == PlayerState.PREPARED) { - if (start) { - mediaPlayer.start(); - setPlayerState(STARTED); - } else { - setPlayerState(PAUSED); + 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(); } - } else { - mediaPlayer.setOnCompletionListener(null); - mediaPlayer.reset(); - setPlayerState(IDLE); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setDataSource(file.getPath()); - setPlayerState(PREPARING); - - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { - public void onPrepared(MediaPlayer mediaPlayer) { - try { - setPlayerState(PREPARED); - - 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); - } + dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); + Log.i(TAG, "Data Source: " + dataSource); + } else if(proxy != null) { + proxy.stop(); + proxy = null; + } + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + Log.i(TAG, "Buffered " + percent + "%"); + if(percent == 100) { + mediaPlayer.setOnBufferingUpdateListener(null); + } + } + }); + + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mediaPlayer) { + try { + setPlayerState(PREPARED); + + synchronized (DownloadServiceImpl.this) { + if (position != 0) { + Log.i(TAG, "Restarting player from position " + position); + mediaPlayer.seekTo(position); } + cachedPosition = position; - lifecycleSupport.serializeDownloadQueue(); - } catch (Exception x) { - handleError(x); + if (start) { + mediaPlayer.start(); + setPlayerState(STARTED); + } else { + setPlayerState(PAUSED); + } } + + lifecycleSupport.serializeDownloadQueue(); + } catch (Exception x) { + handleError(x); } - }); - } + } + }); setupHandlers(downloadFile, isPartial); - if(playerState == PREPARING) { - mediaPlayer.prepareAsync(); - } + mediaPlayer.prepareAsync(); } catch (Exception x) { handleError(x); } @@ -1007,8 +1012,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { 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) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && playerState == PlayerState.STARTED) { DownloadServiceImpl.this.mediaPlayer.setNextMediaPlayer(nextMediaPlayer); nextSetup = true; } @@ -1376,6 +1380,8 @@ public class DownloadServiceImpl extends Service implements DownloadService { return; } + // Do an initial sleep so this prepare can't compete with main prepare + Util.sleepQuietly(5000L); while (!bufferComplete()) { Util.sleepQuietly(5000L); if (isCancelled()) { diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java index 6c9a84ad..765e216b 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java @@ -237,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: diff --git a/subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java b/subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java new file mode 100644 index 00000000..1b422d5a --- /dev/null +++ b/subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java @@ -0,0 +1,246 @@ +package github.daneren2005.dsub.service;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.net.UnknownHostException;
+import java.util.StringTokenizer;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.message.BasicHttpRequest;
+
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.Constants;
+
+public class StreamProxy implements Runnable {
+ private static final String TAG = StreamProxy.class.getSimpleName();
+
+ private Thread thread;
+ private boolean isRunning;
+ private ServerSocket socket;
+ private int port;
+ private DownloadService downloadService;
+
+ public StreamProxy(DownloadService downloadService) {
+
+ // Create listening socket
+ try {
+ socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
+ socket.setSoTimeout(5000);
+ port = socket.getLocalPort();
+ this.downloadService = downloadService;
+ } catch (UnknownHostException e) { // impossible
+ } catch (IOException e) {
+ Log.e(TAG, "IOException initializing server", e);
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void start() {
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void stop() {
+ isRunning = false;
+ thread.interrupt();
+ try {
+ thread.join(5000);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Exception stopping server", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ isRunning = true;
+ while (isRunning) {
+ try {
+ Socket client = socket.accept();
+ if (client == null) {
+ continue;
+ }
+ Log.d(TAG, "client connected");
+
+ StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
+ if (task.processRequest()) {
+ task.execute();
+ }
+
+ } catch (SocketTimeoutException e) {
+ // Do nothing
+ } catch (IOException e) {
+ Log.e(TAG, "Error connecting to client", e);
+ }
+ }
+ Log.d(TAG, "Proxy interrupted. Shutting down.");
+ }
+
+ private class StreamToMediaPlayerTask extends AsyncTask<String, Void, Integer> {
+
+ String localPath;
+ Socket client;
+ int cbSkip;
+
+ public StreamToMediaPlayerTask(Socket client) {
+ this.client = client;
+ }
+
+ private HttpRequest readRequest() {
+ HttpRequest request = null;
+ InputStream is;
+ String firstLine;
+ try {
+ is = client.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
+ firstLine = reader.readLine();
+ } catch (IOException e) {
+ Log.e(TAG, "Error parsing request", e);
+ return request;
+ }
+
+ if (firstLine == null) {
+ Log.i(TAG, "Proxy client closed connection without a request.");
+ return request;
+ }
+
+ StringTokenizer st = new StringTokenizer(firstLine);
+ String method = st.nextToken();
+ String uri = st.nextToken();
+ String realUri = uri.substring(1);
+ Log.i(TAG, realUri);
+ request = new BasicHttpRequest(method, realUri);
+ return request;
+ }
+
+ public boolean processRequest() {
+ HttpRequest request = readRequest();
+ if (request == null) {
+ return false;
+ }
+
+ // Read HTTP headers
+ Log.i(TAG, "Processing request");
+
+ try {
+ localPath = URLDecoder.decode(request.getRequestLine().getUri(), Constants.UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported encoding", e);
+ return false;
+ }
+
+ File file = new File(localPath);
+ if (!file.exists()) {
+ Log.e(TAG, "File " + localPath + " does not exist");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected Integer doInBackground(String... params) {
+ DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ MusicDirectory.Entry song = downloadFile.getSong();
+ long fileSize = downloadFile.getBitRate() * song.getDuration() * 1000 / 8;
+ Log.i(TAG, "Streaming fileSize: " + fileSize);
+
+ // Create HTTP header
+ String headers = "HTTP/1.0 200 OK\r\n";
+ headers += "Content-Type: " + "application/octet-stream" + "\r\n";
+
+ headers += "Connection: close\r\n";
+ headers += "\r\n";
+
+ long cbToSend = fileSize - cbSkip;
+ OutputStream output = null;
+ byte[] buff = new byte[64 * 1024];
+ try {
+ output = new BufferedOutputStream(client.getOutputStream(), 32*1024);
+ output.write(headers.getBytes());
+
+ if(!downloadFile.isWorkDone()) {
+ // Loop as long as there's stuff to send
+ while (isRunning && !client.isClosed()) {
+
+ // See if there's more to send
+ File file = new File(localPath);
+ int cbSentThisBatch = 0;
+ if (file.exists()) {
+ FileInputStream input = new FileInputStream(file);
+ input.skip(cbSkip);
+ int cbToSendThisBatch = input.available();
+ while (cbToSendThisBatch > 0) {
+ int cbToRead = Math.min(cbToSendThisBatch, buff.length);
+ int cbRead = input.read(buff, 0, cbToRead);
+ if (cbRead == -1) {
+ break;
+ }
+ cbToSendThisBatch -= cbRead;
+ cbToSend -= cbRead;
+ output.write(buff, 0, cbRead);
+ output.flush();
+ cbSkip += cbRead;
+ cbSentThisBatch += cbRead;
+ }
+ input.close();
+ }
+
+ // Done regardless of whether or not it thinks it is
+ if(downloadFile.isWorkDone() && cbSkip >= file.length()) {
+ break;
+ }
+
+ // If we did nothing this batch, block for a second
+ if (cbSentThisBatch == 0) {
+ Log.d(TAG, "Blocking until more data appears (" + cbToSend + ")");
+ Thread.sleep(1000);
+ }
+ }
+ } else {
+ Log.w(TAG, "Requesting data for completely downloaded file");
+ }
+ }
+ catch (SocketException socketException) {
+ Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
+ }
+ catch (Exception e) {
+ Log.e(TAG, "Exception thrown from streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+
+ // Cleanup
+ try {
+ if (output != null) {
+ output.close();
+ }
+ client.close();
+ }
+ catch (IOException e) {
+ Log.e(TAG, "IOException while cleaning up streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+
+ return 1;
+ }
+ }
+}
\ No newline at end of file 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/compat/RemoteControlClientICS.java b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java index e6528d1b..52361fd4 100644 --- a/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java +++ b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java @@ -62,7 +62,7 @@ public class RemoteControlClientICS extends RemoteControlClientHelper { // Update the remote controls mRemoteControl.editMetadata(true) .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum()) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getArtist()) .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle()) .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ? 0 : ((currentSong.getDuration() == null) ? 0 : currentSong.getDuration())) |