aboutsummaryrefslogtreecommitdiff
path: root/subsonic-android
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2013-03-15 18:15:48 -0700
committerScott Jackson <daneren2005@gmail.com>2013-03-15 18:15:48 -0700
commit0e67d318aec1857c492a19189c5c75411cc3bab7 (patch)
tree8f8868a752a796c4eaadb0bedf2c0aafb0472688 /subsonic-android
parent563a6645470a5d9d1a28e7ae49df1801e2bbc294 (diff)
parent01da1b4686794b25ba40f910e1f3d0ad713561fe (diff)
downloaddsub-0e67d318aec1857c492a19189c5c75411cc3bab7.tar.gz
dsub-0e67d318aec1857c492a19189c5c75411cc3bab7.tar.bz2
dsub-0e67d318aec1857c492a19189c5c75411cc3bab7.zip
Merge master into Fragments
Diffstat (limited to 'subsonic-android')
-rw-r--r--subsonic-android/AndroidManifest.xml6
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java4
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/service/DownloadFile.java3
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java148
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java4
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/service/StreamProxy.java246
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/updates/Updater.java1
-rw-r--r--subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java2
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()))