From a1a18f77a50804e0127dfa4b0f5240c49c541184 Mon Sep 17 00:00:00 2001
From: Scott Jackson
Date: Mon, 2 Jul 2012 21:24:02 -0700
Subject: Initial Commit
---
.../androidapp/service/CachedMusicService.java | 237 ++++++
.../subsonic/androidapp/service/DownloadFile.java | 323 +++++++
.../androidapp/service/DownloadService.java | 112 +++
.../androidapp/service/DownloadServiceImpl.java | 930 +++++++++++++++++++++
.../service/DownloadServiceLifecycleSupport.java | 266 ++++++
.../androidapp/service/JukeboxService.java | 356 ++++++++
.../androidapp/service/MediaStoreService.java | 109 +++
.../subsonic/androidapp/service/MusicService.java | 91 ++
.../androidapp/service/MusicServiceFactory.java | 36 +
.../androidapp/service/OfflineException.java | 32 +
.../androidapp/service/OfflineMusicService.java | 244 ++++++
.../androidapp/service/RESTMusicService.java | 768 +++++++++++++++++
.../subsonic/androidapp/service/Scrobbler.java | 52 ++
.../androidapp/service/ServerTooOldException.java | 51 ++
.../androidapp/service/parser/AbstractParser.java | 138 +++
.../androidapp/service/parser/AlbumListParser.java | 62 ++
.../androidapp/service/parser/ErrorParser.java | 49 ++
.../androidapp/service/parser/IndexesParser.java | 104 +++
.../service/parser/JukeboxStatusParser.java | 62 ++
.../androidapp/service/parser/LicenseParser.java | 62 ++
.../androidapp/service/parser/LyricsParser.java | 65 ++
.../service/parser/MusicDirectoryEntryParser.java | 59 ++
.../service/parser/MusicDirectoryParser.java | 71 ++
.../service/parser/MusicFoldersParser.java | 69 ++
.../androidapp/service/parser/PlaylistParser.java | 62 ++
.../androidapp/service/parser/PlaylistsParser.java | 67 ++
.../service/parser/RandomSongsParser.java | 62 ++
.../service/parser/SearchResult2Parser.java | 75 ++
.../service/parser/SearchResultParser.java | 67 ++
.../service/parser/SubsonicRESTException.java | 19 +
.../androidapp/service/parser/VersionParser.java | 47 ++
.../androidapp/service/ssl/SSLSocketFactory.java | 497 +++++++++++
.../service/ssl/TrustManagerDecorator.java | 65 ++
.../service/ssl/TrustSelfSignedStrategy.java | 44 +
.../androidapp/service/ssl/TrustStrategy.java | 57 ++
35 files changed, 5410 insertions(+)
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/CachedMusicService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadFile.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/JukeboxService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MediaStoreService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicServiceFactory.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineException.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/RESTMusicService.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/Scrobbler.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ServerTooOldException.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/AbstractParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/AlbumListParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/ErrorParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/IndexesParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/JukeboxStatusParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/LicenseParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/LyricsParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/MusicDirectoryEntryParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/MusicDirectoryParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/MusicFoldersParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/PlaylistParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/PlaylistsParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/RandomSongsParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/SearchResult2Parser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/SearchResultParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/SubsonicRESTException.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/parser/VersionParser.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/SSLSocketFactory.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustManagerDecorator.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustSelfSignedStrategy.java
create mode 100644 subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustStrategy.java
(limited to 'subsonic-android/src/net/sourceforge/subsonic/androidapp/service')
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/CachedMusicService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/CachedMusicService.java
new file mode 100644
index 00000000..a06f3995
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/CachedMusicService.java
@@ -0,0 +1,237 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import net.sourceforge.subsonic.androidapp.domain.Indexes;
+import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
+import net.sourceforge.subsonic.androidapp.domain.Lyrics;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
+import net.sourceforge.subsonic.androidapp.domain.Playlist;
+import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+import net.sourceforge.subsonic.androidapp.domain.SearchResult;
+import net.sourceforge.subsonic.androidapp.domain.Version;
+import net.sourceforge.subsonic.androidapp.util.CancellableTask;
+import net.sourceforge.subsonic.androidapp.util.LRUCache;
+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
+import net.sourceforge.subsonic.androidapp.util.TimeLimitedCache;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class CachedMusicService implements MusicService {
+
+ private static final int MUSIC_DIR_CACHE_SIZE = 20;
+ private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes
+
+ private final MusicService musicService;
+ private final LRUCache> cachedMusicDirectories;
+ private final TimeLimitedCache cachedLicenseValid = new TimeLimitedCache(120, TimeUnit.SECONDS);
+ private final TimeLimitedCache cachedIndexes = new TimeLimitedCache(60 * 60, TimeUnit.SECONDS);
+ private final TimeLimitedCache> cachedPlaylists = new TimeLimitedCache>(60, TimeUnit.SECONDS);
+ private final TimeLimitedCache> cachedMusicFolders = new TimeLimitedCache>(10 * 3600, TimeUnit.SECONDS);
+ private String restUrl;
+
+ public CachedMusicService(MusicService musicService) {
+ this.musicService = musicService;
+ cachedMusicDirectories = new LRUCache>(MUSIC_DIR_CACHE_SIZE);
+ }
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ musicService.ping(context, progressListener);
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ Boolean result = cachedLicenseValid.get();
+ if (result == null) {
+ result = musicService.isLicenseValid(context, progressListener);
+ cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS);
+ }
+ return result;
+ }
+
+ @Override
+ public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ if (refresh) {
+ cachedMusicFolders.clear();
+ }
+ List result = cachedMusicFolders.get();
+ if (result == null) {
+ result = musicService.getMusicFolders(refresh, context, progressListener);
+ cachedMusicFolders.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ if (refresh) {
+ cachedIndexes.clear();
+ cachedMusicFolders.clear();
+ cachedMusicDirectories.clear();
+ }
+ Indexes result = cachedIndexes.get();
+ if (result == null) {
+ result = musicService.getIndexes(musicFolderId, refresh, context, progressListener);
+ cachedIndexes.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ TimeLimitedCache cache = refresh ? null : cachedMusicDirectories.get(id);
+ MusicDirectory dir = cache == null ? null : cache.get();
+ if (dir == null) {
+ dir = musicService.getMusicDirectory(id, refresh, context, progressListener);
+ cache = new TimeLimitedCache(TTL_MUSIC_DIR, TimeUnit.SECONDS);
+ cache.set(dir);
+ cachedMusicDirectories.put(id, cache);
+ }
+ return dir;
+ }
+
+ @Override
+ public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.search(criteria, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getPlaylist(id, context, progressListener);
+ }
+
+ @Override
+ public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ checkSettingsChanged(context);
+ List result = refresh ? null : cachedPlaylists.get();
+ if (result == null) {
+ result = musicService.getPlaylists(refresh, context, progressListener);
+ cachedPlaylists.set(result);
+ }
+ return result;
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception {
+ musicService.createPlaylist(id, name, entries, context, progressListener);
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getLyrics(artist, title, context, progressListener);
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ musicService.scrobble(id, submission, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getAlbumList(type, size, offset, context, progressListener);
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getRandomSongs(size, context, progressListener);
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception {
+ return musicService.getCoverArt(context, entry, size, saveToFile, progressListener);
+ }
+
+ @Override
+ public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception {
+ return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
+ }
+
+ @Override
+ public Version getLocalVersion(Context context) throws Exception {
+ return musicService.getLocalVersion(context);
+ }
+
+ @Override
+ public Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getLatestVersion(context, progressListener);
+ }
+
+ @Override
+ public String getVideoUrl(Context context, String id) {
+ return musicService.getVideoUrl(context, id);
+ }
+
+ @Override
+ public JukeboxStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.updateJukeboxPlaylist(ids, context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.skipJukebox(index, offsetSeconds, context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.stopJukebox(context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.startJukebox(context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getJukeboxStatus(context, progressListener);
+ }
+
+ @Override
+ public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.setJukeboxGain(gain, context, progressListener);
+ }
+
+ private void checkSettingsChanged(Context context) {
+ String newUrl = Util.getRestUrl(context, null);
+ if (!Util.equals(newUrl, restUrl)) {
+ cachedMusicFolders.clear();
+ cachedMusicDirectories.clear();
+ cachedLicenseValid.clear();
+ cachedIndexes.clear();
+ cachedPlaylists.clear();
+ restUrl = newUrl;
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadFile.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadFile.java
new file mode 100644
index 00000000..46373afe
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadFile.java
@@ -0,0 +1,323 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.util.CancellableTask;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+import net.sourceforge.subsonic.androidapp.util.Util;
+import net.sourceforge.subsonic.androidapp.util.CacheCleaner;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class DownloadFile {
+
+ private static final String TAG = DownloadFile.class.getSimpleName();
+ private final Context context;
+ private final MusicDirectory.Entry song;
+ private final File partialFile;
+ private final File completeFile;
+ private final File saveFile;
+
+ private final MediaStoreService mediaStoreService;
+ private CancellableTask downloadTask;
+ private boolean save;
+ private boolean failed;
+ private int bitRate;
+
+ public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
+ this.context = context;
+ this.song = song;
+ this.save = save;
+ saveFile = FileUtil.getSongFile(context, song);
+ bitRate = Util.getMaxBitrate(context);
+ partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ "." + bitRate + ".partial." + FileUtil.getExtension(saveFile.getName()));
+ completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".complete." + FileUtil.getExtension(saveFile.getName()));
+ mediaStoreService = new MediaStoreService(context);
+ }
+
+ public MusicDirectory.Entry getSong() {
+ return song;
+ }
+
+ /**
+ * Returns the effective bit rate.
+ */
+ public int getBitRate() {
+ if (bitRate > 0) {
+ return bitRate;
+ }
+ return song.getBitRate() == null ? 160 : song.getBitRate();
+ }
+
+ public synchronized void download() {
+ FileUtil.createDirectoryForParent(saveFile);
+ failed = false;
+ downloadTask = new DownloadTask();
+ downloadTask.start();
+ }
+
+ public synchronized void cancelDownload() {
+ if (downloadTask != null) {
+ downloadTask.cancel();
+ }
+ }
+
+ public File getCompleteFile() {
+ if (saveFile.exists()) {
+ return saveFile;
+ }
+
+ if (completeFile.exists()) {
+ return completeFile;
+ }
+
+ return saveFile;
+ }
+
+ public File getPartialFile() {
+ return partialFile;
+ }
+
+ public boolean isSaved() {
+ return saveFile.exists();
+ }
+
+ public synchronized boolean isCompleteFileAvailable() {
+ return saveFile.exists() || completeFile.exists();
+ }
+
+ public synchronized boolean isWorkDone() {
+ return saveFile.exists() || (completeFile.exists() && !save);
+ }
+
+ public synchronized boolean isDownloading() {
+ return downloadTask != null && downloadTask.isRunning();
+ }
+
+ public synchronized boolean isDownloadCancelled() {
+ return downloadTask != null && downloadTask.isCancelled();
+ }
+
+ public boolean shouldSave() {
+ return save;
+ }
+
+ public boolean isFailed() {
+ return failed;
+ }
+
+ public void delete() {
+ cancelDownload();
+ Util.delete(partialFile);
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ mediaStoreService.deleteFromMediaStore(this);
+ }
+
+ public void unpin() {
+ if (saveFile.exists()) {
+ saveFile.renameTo(completeFile);
+ }
+ }
+
+ public boolean cleanup() {
+ boolean ok = true;
+ if (completeFile.exists() || saveFile.exists()) {
+ ok = Util.delete(partialFile);
+ }
+ if (saveFile.exists()) {
+ ok &= Util.delete(completeFile);
+ }
+ return ok;
+ }
+
+ // In support of LRU caching.
+ public void updateModificationDate() {
+ updateModificationDate(saveFile);
+ updateModificationDate(partialFile);
+ updateModificationDate(completeFile);
+ }
+
+ private void updateModificationDate(File file) {
+ if (file.exists()) {
+ boolean ok = file.setLastModified(System.currentTimeMillis());
+ if (!ok) {
+ Log.w(TAG, "Failed to set last-modified date on " + file);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadFile (" + song + ")";
+ }
+
+ private class DownloadTask extends CancellableTask {
+
+ @Override
+ public void execute() {
+
+ InputStream in = null;
+ FileOutputStream out = null;
+ PowerManager.WakeLock wakeLock = null;
+ try {
+
+ if (Util.isScreenLitOnDownload(context)) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString());
+ wakeLock.acquire();
+ Log.i(TAG, "Acquired wake lock " + wakeLock);
+ }
+
+ if (saveFile.exists()) {
+ Log.i(TAG, saveFile + " already exists. Skipping.");
+ return;
+ }
+ if (completeFile.exists()) {
+ if (save) {
+ Util.atomicCopy(completeFile, saveFile);
+ } else {
+ Log.i(TAG, completeFile + " already exists. Skipping.");
+ }
+ return;
+ }
+
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ // Attempt partial HTTP GET, appending to the file if it exists.
+ HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
+ in = response.getEntity().getContent();
+ boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT;
+ if (partial) {
+ Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
+ }
+
+ out = new FileOutputStream(partialFile, partial);
+ long n = copy(in, out);
+ Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
+ out.flush();
+ out.close();
+
+ if (isCancelled()) {
+ throw new Exception("Download of '" + song + "' was cancelled");
+ }
+
+ downloadAndSaveCoverArt(musicService);
+
+ if (save) {
+ Util.atomicCopy(partialFile, saveFile);
+ mediaStoreService.saveInMediaStore(DownloadFile.this);
+ } else {
+ Util.atomicCopy(partialFile, completeFile);
+ }
+
+ } catch (Exception x) {
+ Util.close(out);
+ Util.delete(completeFile);
+ Util.delete(saveFile);
+ if (!isCancelled()) {
+ failed = true;
+ Log.w(TAG, "Failed to download '" + song + "'.", x);
+ }
+
+ } finally {
+ Util.close(in);
+ Util.close(out);
+ if (wakeLock != null) {
+ wakeLock.release();
+ Log.i(TAG, "Released wake lock " + wakeLock);
+ }
+ new CacheCleaner(context, DownloadServiceImpl.getInstance()).clean();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DownloadTask (" + song + ")";
+ }
+
+ private void downloadAndSaveCoverArt(MusicService musicService) throws Exception {
+ try {
+ if (song.getCoverArt() != null) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ int size = Math.min(metrics.widthPixels, metrics.heightPixels);
+ musicService.getCoverArt(context, song, size, true, null);
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to get cover art.", x);
+ }
+ }
+
+ private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException {
+
+ // Start a thread that will close the input stream if the task is
+ // cancelled, thus causing the copy() method to return.
+ new Thread() {
+ @Override
+ public void run() {
+ while (true) {
+ Util.sleepQuietly(3000L);
+ if (isCancelled()) {
+ Util.close(in);
+ return;
+ }
+ if (!isRunning()) {
+ return;
+ }
+ }
+ }
+ }.start();
+
+ byte[] buffer = new byte[1024 * 16];
+ long count = 0;
+ int n;
+ long lastLog = System.currentTimeMillis();
+
+ while (!isCancelled() && (n = in.read(buffer)) != -1) {
+ out.write(buffer, 0, n);
+ count += n;
+
+ long now = System.currentTimeMillis();
+ if (now - lastLog > 3000L) { // Only every so often.
+ Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
+ lastLog = now;
+ }
+ }
+ return count;
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java
new file mode 100644
index 00000000..b136bdbc
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadService.java
@@ -0,0 +1,112 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.util.List;
+
+import net.sourceforge.subsonic.androidapp.audiofx.EqualizerController;
+import net.sourceforge.subsonic.androidapp.audiofx.VisualizerController;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.PlayerState;
+import net.sourceforge.subsonic.androidapp.domain.RepeatMode;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public interface DownloadService {
+
+ void download(List songs, boolean save, boolean autoplay, boolean playNext);
+
+ void setShufflePlayEnabled(boolean enabled);
+
+ boolean isShufflePlayEnabled();
+
+ void shuffle();
+
+ RepeatMode getRepeatMode();
+
+ void setRepeatMode(RepeatMode repeatMode);
+
+ boolean getKeepScreenOn();
+
+ void setKeepScreenOn(boolean screenOn);
+
+ boolean getShowVisualization();
+
+ void setShowVisualization(boolean showVisualization);
+
+ void clear();
+
+ void clearIncomplete();
+
+ int size();
+
+ void remove(DownloadFile downloadFile);
+
+ List getDownloads();
+
+ int getCurrentPlayingIndex();
+
+ DownloadFile getCurrentPlaying();
+
+ DownloadFile getCurrentDownloading();
+
+ void play(int index);
+
+ void seekTo(int position);
+
+ void previous();
+
+ void next();
+
+ void pause();
+
+ void start();
+
+ void reset();
+
+ PlayerState getPlayerState();
+
+ int getPlayerPosition();
+
+ int getPlayerDuration();
+
+ void delete(List songs);
+
+ void unpin(List songs);
+
+ DownloadFile forSong(MusicDirectory.Entry song);
+
+ long getDownloadListUpdateRevision();
+
+ void setSuggestedPlaylistName(String name);
+
+ String getSuggestedPlaylistName();
+
+ EqualizerController getEqualizerController();
+
+ VisualizerController getVisualizerController();
+
+ boolean isJukeboxEnabled();
+
+ void setJukeboxEnabled(boolean b);
+
+ void adjustJukeboxVolume(boolean up);
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java
new file mode 100644
index 00000000..2e668fea
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java
@@ -0,0 +1,930 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.util.Log;
+import net.sourceforge.subsonic.androidapp.audiofx.EqualizerController;
+import net.sourceforge.subsonic.androidapp.audiofx.VisualizerController;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.PlayerState;
+import net.sourceforge.subsonic.androidapp.domain.RepeatMode;
+import net.sourceforge.subsonic.androidapp.util.CancellableTask;
+import net.sourceforge.subsonic.androidapp.util.LRUCache;
+import net.sourceforge.subsonic.androidapp.util.ShufflePlayBuffer;
+import net.sourceforge.subsonic.androidapp.util.SimpleServiceBinder;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static net.sourceforge.subsonic.androidapp.domain.PlayerState.*;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class DownloadServiceImpl extends Service implements DownloadService {
+
+ private static final String TAG = DownloadServiceImpl.class.getSimpleName();
+
+ public static final String CMD_PLAY = "net.sourceforge.subsonic.androidapp.CMD_PLAY";
+ public static final String CMD_TOGGLEPAUSE = "net.sourceforge.subsonic.androidapp.CMD_TOGGLEPAUSE";
+ public static final String CMD_PAUSE = "net.sourceforge.subsonic.androidapp.CMD_PAUSE";
+ public static final String CMD_STOP = "net.sourceforge.subsonic.androidapp.CMD_STOP";
+ public static final String CMD_PREVIOUS = "net.sourceforge.subsonic.androidapp.CMD_PREVIOUS";
+ public static final String CMD_NEXT = "net.sourceforge.subsonic.androidapp.CMD_NEXT";
+
+ private final IBinder binder = new SimpleServiceBinder(this);
+ private MediaPlayer mediaPlayer;
+ private final List downloadList = new ArrayList();
+ private final Handler handler = new Handler();
+ private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this);
+ private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this);
+
+ private final LRUCache downloadFileCache = new LRUCache(100);
+ private final List cleanupCandidates = new ArrayList();
+ private final Scrobbler scrobbler = new Scrobbler();
+ private final JukeboxService jukeboxService = new JukeboxService(this);
+ private DownloadFile currentPlaying;
+ private DownloadFile currentDownloading;
+ private CancellableTask bufferTask;
+ private PlayerState playerState = IDLE;
+ private boolean shufflePlay;
+ private long revision;
+ private static DownloadService instance;
+ private String suggestedPlaylistName;
+ private PowerManager.WakeLock wakeLock;
+ private boolean keepScreenOn = false;
+
+ private static boolean equalizerAvailable;
+ private static boolean visualizerAvailable;
+ private EqualizerController equalizerController;
+ private VisualizerController visualizerController;
+ private boolean showVisualization;
+ private boolean jukeboxEnabled;
+
+ static {
+ try {
+ EqualizerController.checkAvailable();
+ equalizerAvailable = true;
+ } catch (Throwable t) {
+ equalizerAvailable = false;
+ }
+ }
+ static {
+ try {
+ VisualizerController.checkAvailable();
+ visualizerAvailable = true;
+ } catch (Throwable t) {
+ visualizerAvailable = false;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mediaPlayer = new MediaPlayer();
+ mediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK);
+
+ mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mediaPlayer, int what, int more) {
+ handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")"));
+ return false;
+ }
+ });
+
+ if (equalizerAvailable) {
+ equalizerController = new EqualizerController(this, mediaPlayer);
+ if (!equalizerController.isAvailable()) {
+ equalizerController = null;
+ } else {
+ equalizerController.loadSettings();
+ }
+ }
+ if (visualizerAvailable) {
+ visualizerController = new VisualizerController(this, mediaPlayer);
+ if (!visualizerController.isAvailable()) {
+ visualizerController = null;
+ }
+ }
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
+ wakeLock.setReferenceCounted(false);
+
+ instance = this;
+ lifecycleSupport.onCreate();
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ super.onStart(intent, startId);
+ lifecycleSupport.onStart(intent);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ lifecycleSupport.onDestroy();
+ mediaPlayer.release();
+ shufflePlayBuffer.shutdown();
+ if (equalizerController != null) {
+ equalizerController.release();
+ }
+ if (visualizerController != null) {
+ visualizerController.release();
+ }
+
+ instance = null;
+ }
+
+ public static DownloadService getInstance() {
+ return instance;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ @Override
+ public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext) {
+ shufflePlay = false;
+ int offset = 1;
+
+ if (songs.isEmpty()) {
+ return;
+ }
+ if (playNext) {
+ if (autoplay && getCurrentPlayingIndex() >= 0) {
+ offset = 0;
+ }
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
+ offset++;
+ }
+ revision++;
+ } else {
+ for (MusicDirectory.Entry song : songs) {
+ DownloadFile downloadFile = new DownloadFile(this, song, save);
+ downloadList.add(downloadFile);
+ }
+ revision++;
+ }
+ updateJukeboxPlaylist();
+
+ if (autoplay) {
+ play(0);
+ } else {
+ if (currentPlaying == null) {
+ currentPlaying = downloadList.get(0);
+ }
+ checkDownloads();
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ }
+
+ private void updateJukeboxPlaylist() {
+ if (jukeboxEnabled) {
+ jukeboxService.updatePlaylist();
+ }
+ }
+
+ public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition) {
+ download(songs, false, false, false);
+ if (currentPlayingIndex != -1) {
+ play(currentPlayingIndex, false);
+ if (currentPlaying.isCompleteFileAvailable()) {
+ doPlay(currentPlaying, currentPlayingPosition, false);
+ }
+ }
+ }
+
+ @Override
+ public synchronized void setShufflePlayEnabled(boolean enabled) {
+ if (shufflePlay == enabled) {
+ return;
+ }
+
+ shufflePlay = enabled;
+ if (shufflePlay) {
+ clear();
+ checkDownloads();
+ }
+ }
+
+ @Override
+ public synchronized boolean isShufflePlayEnabled() {
+ return shufflePlay;
+ }
+
+ @Override
+ public synchronized void shuffle() {
+ Collections.shuffle(downloadList);
+ if (currentPlaying != null) {
+ downloadList.remove(getCurrentPlayingIndex());
+ downloadList.add(0, currentPlaying);
+ }
+ revision++;
+ lifecycleSupport.serializeDownloadQueue();
+ updateJukeboxPlaylist();
+ }
+
+ @Override
+ public RepeatMode getRepeatMode() {
+ return Util.getRepeatMode(this);
+ }
+
+ @Override
+ public void setRepeatMode(RepeatMode repeatMode) {
+ Util.setRepeatMode(this, repeatMode);
+ }
+
+ @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 DownloadFile forSong(MusicDirectory.Entry song) {
+ for (DownloadFile downloadFile : downloadList) {
+ if (downloadFile.getSong().equals(song)) {
+ return downloadFile;
+ }
+ }
+
+ DownloadFile downloadFile = downloadFileCache.get(song);
+ if (downloadFile == null) {
+ downloadFile = new DownloadFile(this, song, false);
+ downloadFileCache.put(song, downloadFile);
+ }
+ return downloadFile;
+ }
+
+ @Override
+ public synchronized void clear() {
+ clear(true);
+ }
+
+ @Override
+ public synchronized void clearIncomplete() {
+ reset();
+ Iterator iterator = downloadList.iterator();
+ while (iterator.hasNext()) {
+ DownloadFile downloadFile = iterator.next();
+ if (!downloadFile.isCompleteFileAvailable()) {
+ iterator.remove();
+ }
+ }
+ lifecycleSupport.serializeDownloadQueue();
+ updateJukeboxPlaylist();
+ }
+
+ @Override
+ public synchronized int size() {
+ return downloadList.size();
+ }
+
+ public synchronized void clear(boolean serialize) {
+ reset();
+ downloadList.clear();
+ revision++;
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ setCurrentPlaying(null, false);
+
+ if (serialize) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+ updateJukeboxPlaylist();
+ }
+
+ @Override
+ public synchronized void remove(DownloadFile downloadFile) {
+ if (downloadFile == currentDownloading) {
+ currentDownloading.cancelDownload();
+ currentDownloading = null;
+ }
+ if (downloadFile == currentPlaying) {
+ reset();
+ setCurrentPlaying(null, false);
+ }
+ downloadList.remove(downloadFile);
+ revision++;
+ lifecycleSupport.serializeDownloadQueue();
+ updateJukeboxPlaylist();
+ }
+
+ @Override
+ public synchronized void delete(List songs) {
+ for (MusicDirectory.Entry song : songs) {
+ forSong(song).delete();
+ }
+ }
+
+ @Override
+ public synchronized void unpin(List songs) {
+ for (MusicDirectory.Entry song : songs) {
+ forSong(song).unpin();
+ }
+ }
+
+ synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) {
+ try {
+ setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification);
+ } catch (IndexOutOfBoundsException x) {
+ // Ignored
+ }
+ }
+
+ synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) {
+ this.currentPlaying = currentPlaying;
+
+ if (currentPlaying != null) {
+ Util.broadcastNewTrackInfo(this, currentPlaying.getSong());
+ } else {
+ Util.broadcastNewTrackInfo(this, null);
+ }
+
+ if (currentPlaying != null && showNotification) {
+ Util.showPlayingNotification(this, this, handler, currentPlaying.getSong());
+ } else {
+ Util.hidePlayingNotification(this, this, handler);
+ }
+ }
+
+ @Override
+ public synchronized int getCurrentPlayingIndex() {
+ return downloadList.indexOf(currentPlaying);
+ }
+
+ @Override
+ public DownloadFile getCurrentPlaying() {
+ return currentPlaying;
+ }
+
+ @Override
+ public DownloadFile getCurrentDownloading() {
+ return currentDownloading;
+ }
+
+ @Override
+ public synchronized List getDownloads() {
+ return new ArrayList(downloadList);
+ }
+
+ /** Plays either the current song (resume) or the first/next one in queue. */
+ public synchronized void play()
+ {
+ int current = getCurrentPlayingIndex();
+ if (current == -1) {
+ play(0);
+ } else {
+ play(current);
+ }
+ }
+
+ @Override
+ public synchronized void play(int index) {
+ play(index, true);
+ }
+
+ private synchronized void play(int index, boolean start) {
+ if (index < 0 || index >= size()) {
+ reset();
+ setCurrentPlaying(null, false);
+ } else {
+ setCurrentPlaying(index, start);
+ checkDownloads();
+ if (start) {
+ if (jukeboxEnabled) {
+ jukeboxService.skip(getCurrentPlayingIndex(), 0);
+ setPlayerState(STARTED);
+ } else {
+ bufferAndPlay();
+ }
+ }
+ }
+ }
+
+ /** Plays or resumes the playback, depending on the current player state. */
+ public synchronized void togglePlayPause()
+ {
+ if (playerState == PAUSED || playerState == COMPLETED) {
+ start();
+ } else if (playerState == STOPPED || playerState == IDLE) {
+ play();
+ } else if (playerState == STARTED) {
+ pause();
+ }
+ }
+
+ @Override
+ public synchronized void seekTo(int position) {
+ try {
+ if (jukeboxEnabled) {
+ jukeboxService.skip(getCurrentPlayingIndex(), position / 1000);
+ } else {
+ mediaPlayer.seekTo(position);
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void previous() {
+ int index = 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 = getCurrentPlayingIndex();
+ if (index != -1) {
+ play(index + 1);
+ }
+ }
+
+ private void onSongCompleted() {
+ int index = getCurrentPlayingIndex();
+ if (index != -1) {
+ switch (getRepeatMode()) {
+ case OFF:
+ play(index + 1);
+ break;
+ case ALL:
+ play((index + 1) % size());
+ break;
+ case SINGLE:
+ play(index);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public synchronized void pause() {
+ try {
+ if (playerState == STARTED) {
+ if (jukeboxEnabled) {
+ jukeboxService.stop();
+ } else {
+ mediaPlayer.pause();
+ }
+ setPlayerState(PAUSED);
+ }
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void start() {
+ try {
+ if (jukeboxEnabled) {
+ jukeboxService.start();
+ } else {
+ mediaPlayer.start();
+ }
+ setPlayerState(STARTED);
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized void reset() {
+ if (bufferTask != null) {
+ bufferTask.cancel();
+ }
+ try {
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ @Override
+ public synchronized int getPlayerPosition() {
+ try {
+ if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) {
+ return 0;
+ }
+ if (jukeboxEnabled) {
+ return jukeboxService.getPositionSeconds() * 1000;
+ } else {
+ return mediaPlayer.getCurrentPosition();
+ }
+ } catch (Exception x) {
+ handleError(x);
+ return 0;
+ }
+ }
+
+ @Override
+ public synchronized int getPlayerDuration() {
+ if (currentPlaying != null) {
+ Integer duration = currentPlaying.getSong().getDuration();
+ if (duration != null) {
+ return duration * 1000;
+ }
+ }
+ if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) {
+ try {
+ return mediaPlayer.getDuration();
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public PlayerState getPlayerState() {
+ return playerState;
+ }
+
+ synchronized void setPlayerState(PlayerState playerState) {
+ Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")");
+
+ if (playerState == PAUSED) {
+ lifecycleSupport.serializeDownloadQueue();
+ }
+
+ boolean show = this.playerState == PAUSED && playerState == PlayerState.STARTED;
+ boolean hide = this.playerState == STARTED && playerState == PlayerState.PAUSED;
+ Util.broadcastPlaybackStatusChange(this, playerState);
+
+ this.playerState = playerState;
+ if (show) {
+ Util.showPlayingNotification(this, this, handler, currentPlaying.getSong());
+ } else if (hide) {
+ Util.hidePlayingNotification(this, this, handler);
+ }
+
+ if (playerState == STARTED) {
+ scrobbler.scrobble(this, currentPlaying, false);
+ } else if (playerState == COMPLETED) {
+ scrobbler.scrobble(this, currentPlaying, true);
+ }
+ }
+
+ @Override
+ public void setSuggestedPlaylistName(String name) {
+ this.suggestedPlaylistName = name;
+ }
+
+ @Override
+ public String getSuggestedPlaylistName() {
+ return suggestedPlaylistName;
+ }
+
+ @Override
+ public EqualizerController getEqualizerController() {
+ return equalizerController;
+ }
+
+ @Override
+ public VisualizerController getVisualizerController() {
+ return visualizerController;
+ }
+
+ @Override
+ public boolean isJukeboxEnabled() {
+ return jukeboxEnabled;
+ }
+
+ @Override
+ public void setJukeboxEnabled(boolean jukeboxEnabled) {
+ this.jukeboxEnabled = jukeboxEnabled;
+ jukeboxService.setEnabled(jukeboxEnabled);
+ if (jukeboxEnabled) {
+ reset();
+
+ // Cancel current download, if necessary.
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ }
+ }
+ }
+
+ @Override
+ public void adjustJukeboxVolume(boolean up) {
+ jukeboxService.adjustVolume(up);
+ }
+
+ private synchronized void bufferAndPlay() {
+ reset();
+
+ bufferTask = new BufferTask(currentPlaying, 0);
+ bufferTask.start();
+ }
+
+ private synchronized void doPlay(final DownloadFile downloadFile, int position, boolean start) {
+ try {
+ final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
+ downloadFile.updateModificationDate();
+ mediaPlayer.setOnCompletionListener(null);
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ mediaPlayer.setDataSource(file.getPath());
+ setPlayerState(PREPARING);
+ mediaPlayer.prepare();
+ setPlayerState(PREPARED);
+
+ 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 COMPLETED and not playing partial file, we are *really" finished
+ // with the song and can move on to the next.
+ if (!file.equals(downloadFile.getPartialFile())) {
+ onSongCompleted();
+ return;
+ }
+
+ // If file is not completely downloaded, restart the playback from the current position.
+ int pos = mediaPlayer.getCurrentPosition();
+ synchronized (DownloadServiceImpl.this) {
+
+ // Work-around for apparent bug on certain phones: If close (less than ten seconds) to the end
+ // of the song, skip to the next rather than restarting it.
+ Integer duration = downloadFile.getSong().getDuration() == null ? null : downloadFile.getSong().getDuration() * 1000;
+ if (duration != null) {
+ if (Math.abs(duration - pos) < 10000) {
+ Log.i(TAG, "Skipping restart from " + pos + " of " + duration);
+ onSongCompleted();
+ return;
+ }
+ }
+
+ Log.i(TAG, "Requesting restart from " + pos + " of " + duration);
+ reset();
+ bufferTask = new BufferTask(downloadFile, pos);
+ bufferTask.start();
+ }
+ }
+ });
+
+ if (position != 0) {
+ Log.i(TAG, "Restarting player from position " + position);
+ mediaPlayer.seekTo(position);
+ }
+
+ if (start) {
+ mediaPlayer.start();
+ setPlayerState(STARTED);
+ } else {
+ setPlayerState(PAUSED);
+ }
+ lifecycleSupport.serializeDownloadQueue();
+
+ } catch (Exception x) {
+ handleError(x);
+ }
+ }
+
+ private void handleError(Exception x) {
+ Log.w(TAG, "Media player error: " + x, x);
+ mediaPlayer.reset();
+ setPlayerState(IDLE);
+ }
+
+ protected synchronized void checkDownloads() {
+
+ if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) {
+ return;
+ }
+
+ if (shufflePlay) {
+ checkShufflePlay();
+ }
+
+ if (jukeboxEnabled || !Util.isNetworkConnected(this)) {
+ return;
+ }
+
+ if (downloadList.isEmpty()) {
+ return;
+ }
+
+ // Need to download current playing?
+ if (currentPlaying != null &&
+ currentPlaying != currentDownloading &&
+ !currentPlaying.isCompleteFileAvailable()) {
+
+ // Cancel current download, if necessary.
+ if (currentDownloading != null) {
+ currentDownloading.cancelDownload();
+ }
+
+ currentDownloading = currentPlaying;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ }
+
+ // Find a suitable target for download.
+ else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed()) {
+
+ int n = size();
+ if (n == 0) {
+ return;
+ }
+
+ int preloaded = 0;
+
+ int start = currentPlaying == null ? 0 : getCurrentPlayingIndex();
+ int i = start;
+ do {
+ DownloadFile downloadFile = downloadList.get(i);
+ if (!downloadFile.isWorkDone()) {
+ if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) {
+ currentDownloading = downloadFile;
+ currentDownloading.download();
+ cleanupCandidates.add(currentDownloading);
+ break;
+ }
+ } else if (currentPlaying != downloadFile) {
+ preloaded++;
+ }
+
+ i = (i + 1) % n;
+ } while (i != start);
+ }
+
+ // Delete obsolete .partial and .complete files.
+ cleanup();
+ }
+
+ private synchronized void checkShufflePlay() {
+
+ final int listSize = 20;
+ boolean wasEmpty = downloadList.isEmpty();
+
+ long revisionBefore = revision;
+
+ // First, ensure that list is at least 20 songs long.
+ int size = size();
+ if (size < listSize) {
+ for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) {
+ DownloadFile downloadFile = new DownloadFile(this, song, false);
+ downloadList.add(downloadFile);
+ revision++;
+ }
+ }
+
+ int currIndex = 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(this, song, false));
+ downloadList.get(0).cancelDownload();
+ downloadList.remove(0);
+ revision++;
+ }
+ }
+
+ if (revisionBefore != revision) {
+ updateJukeboxPlaylist();
+ }
+
+ if (wasEmpty && !downloadList.isEmpty()) {
+ play(0);
+ }
+ }
+
+ public long getDownloadListUpdateRevision() {
+ return revision;
+ }
+
+ private synchronized void cleanup() {
+ Iterator iterator = cleanupCandidates.iterator();
+ while (iterator.hasNext()) {
+ DownloadFile downloadFile = iterator.next();
+ if (downloadFile != currentPlaying && downloadFile != currentDownloading) {
+ if (downloadFile.cleanup()) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ private class BufferTask extends CancellableTask {
+
+ private static final int BUFFER_LENGTH_SECONDS = 5;
+
+ private final DownloadFile downloadFile;
+ private final int position;
+ private final long expectedFileSize;
+ private final File partialFile;
+
+ public BufferTask(DownloadFile downloadFile, int position) {
+ this.downloadFile = downloadFile;
+ this.position = position;
+ partialFile = downloadFile.getPartialFile();
+
+ // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
+ int bitRate = downloadFile.getBitRate();
+ long byteCount = Math.max(100000, bitRate * 1024 / 8 * BUFFER_LENGTH_SECONDS);
+
+ // Find out how large the file should grow before resuming playback.
+ expectedFileSize = partialFile.length() + byteCount;
+ }
+
+ @Override
+ public void execute() {
+ setPlayerState(DOWNLOADING);
+
+ while (!bufferComplete()) {
+ Util.sleepQuietly(1000L);
+ if (isCancelled()) {
+ return;
+ }
+ }
+ doPlay(downloadFile, position, true);
+ }
+
+ private boolean bufferComplete() {
+ boolean completeFileAvailable = downloadFile.isCompleteFileAvailable();
+ long size = partialFile.length();
+
+ Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")");
+ return completeFileAvailable || size >= expectedFileSize;
+ }
+
+ @Override
+ public String toString() {
+ return "BufferTask (" + downloadFile + ")";
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java
new file mode 100644
index 00000000..2010a4a1
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceLifecycleSupport.java
@@ -0,0 +1,266 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.PlayerState;
+import net.sourceforge.subsonic.androidapp.util.CacheCleaner;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * @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;
+
+ /**
+ * 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.newScheduledThreadPool(2);
+ executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS);
+
+ // Pause when headset is unplugged.
+ headsetEventReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "Headset event for: " + intent.getExtras().get("name"));
+ if (intent.getExtras().getInt("state") == 0) {
+ downloadService.pause();
+ }
+ }
+ };
+ downloadService.registerReceiver(headsetEventReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
+
+ // 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);
+
+ deserializeDownloadQueue();
+
+ new CacheCleaner(downloadService, downloadService).clean();
+ }
+
+ 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();
+ serializeDownloadQueue();
+ 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() {
+ State state = new State();
+ for (DownloadFile downloadFile : downloadService.getDownloads()) {
+ 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);
+ if (state == null) {
+ return;
+ }
+ Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
+ downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition);
+
+ // 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.reset();
+ 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) {
+ 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 songs = new ArrayList();
+ private int currentPlayingIndex;
+ private int currentPlayingPosition;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/JukeboxService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/JukeboxService.java
new file mode 100644
index 00000000..e3145f4e
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/JukeboxService.java
@@ -0,0 +1,356 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
+import net.sourceforge.subsonic.androidapp.domain.PlayerState;
+import net.sourceforge.subsonic.androidapp.service.parser.SubsonicRESTException;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Provides an asynchronous interface to the remote jukebox on the Subsonic server.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class JukeboxService {
+
+ private static final String TAG = JukeboxService.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();
+ private JukeboxStatus jukeboxStatus;
+ private float gain = 0.5f;
+ private VolumeToast volumeToast;
+
+ // TODO: Report warning if queue fills up.
+ // TODO: Create shutdown method?
+ // TODO: Disable repeat.
+ // TODO: Persist RC state?
+ // TODO: Minimize status updates.
+
+ public JukeboxService(DownloadServiceImpl downloadService) {
+ this.downloadService = downloadService;
+ new Thread() {
+ @Override
+ public void run() {
+ processTasks();
+ }
+ }.start();
+ }
+
+ private synchronized void startStatusUpdate() {
+ stopStatusUpdate();
+ Runnable updateTask = new Runnable() {
+ @Override
+ public void run() {
+ tasks.remove(GetStatus.class);
+ tasks.add(new GetStatus());
+ }
+ };
+ statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS,
+ STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ private synchronized void stopStatusUpdate() {
+ if (statusUpdateFuture != null) {
+ statusUpdateFuture.cancel(false);
+ statusUpdateFuture = null;
+ }
+ }
+
+ private void processTasks() {
+ while (true) {
+ JukeboxTask task = null;
+ try {
+ task = tasks.take();
+ JukeboxStatus status = task.execute();
+ onStatusUpdate(status);
+ } catch (Throwable x) {
+ onError(task, x);
+ }
+ }
+ }
+
+ private void onStatusUpdate(JukeboxStatus jukeboxStatus) {
+ timeOfLastUpdate.set(System.currentTimeMillis());
+ this.jukeboxStatus = jukeboxStatus;
+
+ // Track change?
+ Integer index = jukeboxStatus.getCurrentPlayingIndex();
+ if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) {
+ downloadService.setCurrentPlaying(index, true);
+ }
+ }
+
+ private void onError(JukeboxTask task, Throwable x) {
+ if (x instanceof ServerTooOldException && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_server_too_old);
+ } else if (x instanceof OfflineException && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_offline);
+ } else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop)) {
+ disableJukeboxOnError(x, R.string.download_jukebox_not_authorized);
+ } else {
+ Log.e(TAG, "Failed to process jukebox task: " + x, x);
+ }
+ }
+
+ private void disableJukeboxOnError(Throwable x, final int resourceId) {
+ Log.w(TAG, x.toString());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, resourceId, false);
+ }
+ });
+ downloadService.setJukeboxEnabled(false);
+ }
+
+ public void updatePlaylist() {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ List ids = new ArrayList();
+ for (DownloadFile file : downloadService.getDownloads()) {
+ ids.add(file.getSong().getId());
+ }
+ tasks.add(new SetPlaylist(ids));
+ }
+
+ public void skip(final int index, final int offsetSeconds) {
+ tasks.remove(Skip.class);
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ if (jukeboxStatus != null) {
+ jukeboxStatus.setPositionSeconds(offsetSeconds);
+ }
+ tasks.add(new Skip(index, offsetSeconds));
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+
+ public void stop() {
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ stopStatusUpdate();
+ tasks.add(new Stop());
+ }
+
+ public void start() {
+ tasks.remove(Stop.class);
+ tasks.remove(Start.class);
+
+ startStatusUpdate();
+ tasks.add(new Start());
+ }
+
+ public synchronized void adjustVolume(boolean up) {
+ float delta = up ? 0.1f : -0.1f;
+ gain += delta;
+ gain = Math.max(gain, 0.0f);
+ gain = Math.min(gain, 1.0f);
+
+ tasks.remove(SetGain.class);
+ tasks.add(new SetGain(gain));
+
+ if (volumeToast == null) {
+ volumeToast = new VolumeToast(downloadService);
+ }
+ volumeToast.setVolume(gain);
+ }
+
+ private MusicService getMusicService() {
+ return MusicServiceFactory.getMusicService(downloadService);
+ }
+
+ public int getPositionSeconds() {
+ if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0) {
+ return 0;
+ }
+
+ if (jukeboxStatus.isPlaying()) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L);
+ return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate;
+ }
+
+ return jukeboxStatus.getPositionSeconds();
+ }
+
+ public void setEnabled(boolean enabled) {
+ tasks.clear();
+ if (enabled) {
+ updatePlaylist();
+ }
+ stop();
+ downloadService.setPlayerState(PlayerState.IDLE);
+ }
+
+ private static class TaskQueue {
+
+ private final LinkedBlockingQueue queue = new LinkedBlockingQueue();
+
+ void add(JukeboxTask jukeboxTask) {
+ queue.add(jukeboxTask);
+ }
+
+ JukeboxTask take() throws InterruptedException {
+ return queue.take();
+ }
+
+ void remove(Class extends JukeboxTask> clazz) {
+ try {
+ Iterator iterator = queue.iterator();
+ while (iterator.hasNext()) {
+ JukeboxTask task = iterator.next();
+ if (clazz.equals(task.getClass())) {
+ iterator.remove();
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to clean-up task queue.", x);
+ }
+ }
+
+ void clear() {
+ queue.clear();
+ }
+ }
+
+ private abstract class JukeboxTask {
+
+ abstract JukeboxStatus execute() throws Exception;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+ }
+
+ private class GetStatus extends JukeboxTask {
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().getJukeboxStatus(downloadService, null);
+ }
+ }
+
+ private class SetPlaylist extends JukeboxTask {
+
+ private final List ids;
+
+ SetPlaylist(List ids) {
+ this.ids = ids;
+ }
+
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().updateJukeboxPlaylist(ids, downloadService, null);
+ }
+ }
+
+ private class Skip extends JukeboxTask {
+ private final int index;
+ private final int offsetSeconds;
+
+ Skip(int index, int offsetSeconds) {
+ this.index = index;
+ this.offsetSeconds = offsetSeconds;
+ }
+
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().skipJukebox(index, offsetSeconds, downloadService, null);
+ }
+ }
+
+ private class Stop extends JukeboxTask {
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().stopJukebox(downloadService, null);
+ }
+ }
+
+ private class Start extends JukeboxTask {
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().startJukebox(downloadService, null);
+ }
+ }
+
+ private class SetGain extends JukeboxTask {
+
+ private final float gain;
+
+ private SetGain(float gain) {
+ this.gain = gain;
+ }
+
+ @Override
+ JukeboxStatus execute() throws Exception {
+ return getMusicService().setJukeboxGain(gain, downloadService, null);
+ }
+ }
+
+ private static class VolumeToast extends Toast {
+
+ private final ProgressBar progressBar;
+
+ public VolumeToast(Context context) {
+ super(context);
+ setDuration(Toast.LENGTH_SHORT);
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.jukebox_volume, null);
+ progressBar = (ProgressBar) view.findViewById(R.id.jukebox_volume_progress_bar);
+
+ setView(view);
+ setGravity(Gravity.TOP, 0, 0);
+ }
+
+ public void setVolume(float volume) {
+ progressBar.setProgress(Math.round(100 * volume));
+ show();
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MediaStoreService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MediaStoreService.java
new file mode 100644
index 00000000..775fa3f5
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MediaStoreService.java
@@ -0,0 +1,109 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.io.File;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MediaStoreService {
+
+ private static final String TAG = MediaStoreService.class.getSimpleName();
+ private static final Uri ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart");
+
+ private final Context context;
+
+ public MediaStoreService(Context context) {
+ this.context = context;
+ }
+
+ public void saveInMediaStore(DownloadFile downloadFile) {
+ MusicDirectory.Entry song = downloadFile.getSong();
+ File songFile = downloadFile.getCompleteFile();
+
+ // Delete existing row in case the song has been downloaded before.
+ deleteFromMediaStore(downloadFile);
+
+ ContentResolver contentResolver = context.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.TITLE, song.getTitle());
+ values.put(MediaStore.Audio.AudioColumns.ARTIST, song.getArtist());
+ values.put(MediaStore.Audio.AudioColumns.ALBUM, song.getAlbum());
+ values.put(MediaStore.Audio.AudioColumns.TRACK, song.getTrack());
+ values.put(MediaStore.Audio.AudioColumns.YEAR, song.getYear());
+ values.put(MediaStore.MediaColumns.DATA, songFile.getAbsolutePath());
+ values.put(MediaStore.MediaColumns.MIME_TYPE, song.getContentType());
+ values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, 1);
+
+ Uri uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
+
+ // Look up album, and add cover art if found.
+ Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Audio.AudioColumns.ALBUM_ID}, null, null, null);
+ if (cursor.moveToFirst()) {
+ int albumId = cursor.getInt(0);
+ insertAlbumArt(albumId, downloadFile);
+ }
+ cursor.close();
+ }
+
+ public void deleteFromMediaStore(DownloadFile downloadFile) {
+ ContentResolver contentResolver = context.getContentResolver();
+ MusicDirectory.Entry song = downloadFile.getSong();
+ File file = downloadFile.getCompleteFile();
+
+ int n = contentResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Audio.AudioColumns.TITLE_KEY + "=? AND " +
+ MediaStore.MediaColumns.DATA + "=?",
+ new String[]{MediaStore.Audio.keyFor(song.getTitle()), file.getAbsolutePath()});
+ if (n > 0) {
+ Log.i(TAG, "Deleting media store row for " + song);
+ }
+ }
+
+ private void insertAlbumArt(int albumId, DownloadFile downloadFile) {
+ ContentResolver contentResolver = context.getContentResolver();
+
+ Cursor cursor = contentResolver.query(Uri.withAppendedPath(ALBUM_ART_URI, String.valueOf(albumId)), null, null, null, null);
+ if (!cursor.moveToFirst()) {
+
+ // No album art found, add it.
+ File albumArtFile = FileUtil.getAlbumArtFile(context, downloadFile.getSong());
+ if (albumArtFile.exists()) {
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId);
+ values.put(MediaStore.MediaColumns.DATA, albumArtFile.getPath());
+ contentResolver.insert(ALBUM_ART_URI, values);
+ Log.i(TAG, "Added album art: " + albumArtFile);
+ }
+ }
+ cursor.close();
+ }
+
+}
\ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicService.java
new file mode 100644
index 00000000..2acb4c65
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicService.java
@@ -0,0 +1,91 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import net.sourceforge.subsonic.androidapp.domain.Indexes;
+import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
+import net.sourceforge.subsonic.androidapp.domain.Lyrics;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
+import net.sourceforge.subsonic.androidapp.domain.Playlist;
+import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+import net.sourceforge.subsonic.androidapp.domain.SearchResult;
+import net.sourceforge.subsonic.androidapp.domain.Version;
+import net.sourceforge.subsonic.androidapp.util.CancellableTask;
+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface MusicService {
+
+ void ping(Context context, ProgressListener progressListener) throws Exception;
+
+ boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception;
+
+ List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception;
+
+ List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
+
+ void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception;
+
+ Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception;
+
+ void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception;
+
+ MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception;
+
+ Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception;
+
+ HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception;
+
+ Version getLocalVersion(Context context) throws Exception;
+
+ Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception;
+
+ String getVideoUrl(Context context, String id);
+
+ JukeboxStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception;
+
+ JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception;
+}
\ No newline at end of file
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicServiceFactory.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicServiceFactory.java
new file mode 100644
index 00000000..552d1d32
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/MusicServiceFactory.java
@@ -0,0 +1,36 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import android.content.Context;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MusicServiceFactory {
+
+ private static final MusicService REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService());
+ private static final MusicService OFFLINE_MUSIC_SERVICE = new OfflineMusicService();
+
+ public static MusicService getMusicService(Context context) {
+ return Util.isOffline(context) ? OFFLINE_MUSIC_SERVICE : REST_MUSIC_SERVICE;
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineException.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineException.java
new file mode 100644
index 00000000..49c000bf
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineException.java
@@ -0,0 +1,32 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+/**
+ * Thrown by service methods that are not available in offline mode.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class OfflineException extends Exception {
+
+ public OfflineException(String message) {
+ super(message);
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java
new file mode 100644
index 00000000..6a8ad6d0
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java
@@ -0,0 +1,244 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import net.sourceforge.subsonic.androidapp.domain.Artist;
+import net.sourceforge.subsonic.androidapp.domain.Indexes;
+import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
+import net.sourceforge.subsonic.androidapp.domain.Lyrics;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
+import net.sourceforge.subsonic.androidapp.domain.Playlist;
+import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+import net.sourceforge.subsonic.androidapp.domain.SearchResult;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class OfflineMusicService extends RESTMusicService {
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ return true;
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List artists = new ArrayList();
+ File root = FileUtil.getMusicDirectory(context);
+ for (File file : FileUtil.listFiles(root)) {
+ if (file.isDirectory()) {
+ Artist artist = new Artist();
+ artist.setId(file.getPath());
+ artist.setIndex(file.getName().substring(0, 1));
+ artist.setName(file.getName());
+ artists.add(artist);
+ }
+ }
+ return new Indexes(0L, Collections.emptyList(), artists);
+ }
+
+ @Override
+ public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ File dir = new File(id);
+ MusicDirectory result = new MusicDirectory();
+ result.setName(dir.getName());
+
+ Set names = new HashSet();
+
+ for (File file : FileUtil.listMusicFiles(dir)) {
+ String name = getName(file);
+ if (name != null & !names.contains(name)) {
+ names.add(name);
+ result.addChild(createEntry(context, file, name));
+ }
+ }
+ return result;
+ }
+
+ private String getName(File file) {
+ String name = file.getName();
+ if (file.isDirectory()) {
+ return name;
+ }
+
+ if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) {
+ return null;
+ }
+
+ name = name.replace(".complete", "");
+ return FileUtil.getBaseName(name);
+ }
+
+ private MusicDirectory.Entry createEntry(Context context, File file, String name) {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+ entry.setDirectory(file.isDirectory());
+ entry.setId(file.getPath());
+ entry.setParent(file.getParent());
+ entry.setSize(file.length());
+ String root = FileUtil.getMusicDirectory(context).getPath();
+ entry.setPath(file.getPath().replaceFirst("^" + root + "/" , ""));
+ if (file.isFile()) {
+ entry.setArtist(file.getParentFile().getParentFile().getName());
+ entry.setAlbum(file.getParentFile().getName());
+ }
+ entry.setTitle(name);
+ entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", "")));
+
+ File albumArt = FileUtil.getAlbumArtFile(context, entry);
+ if (albumArt.exists()) {
+ entry.setCoverArt(albumArt.getPath());
+ }
+ return entry;
+ }
+
+ @Override
+ public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception {
+ InputStream in = new FileInputStream(entry.getCoverArt());
+ try {
+ byte[] bytes = Util.toByteArray(in);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ return Bitmap.createScaledBitmap(bitmap, size, size, true);
+ } finally {
+ Util.close(in);
+ }
+ }
+
+ @Override
+ public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Music folders not available in offline mode");
+ }
+
+ @Override
+ public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Search not available in offline mode");
+ }
+
+ @Override
+ public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Playlists not available in offline mode");
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Playlists not available in offline mode");
+ }
+
+ @Override
+ public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Playlists not available in offline mode");
+ }
+
+ @Override
+ public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Lyrics not available in offline mode");
+ }
+
+ @Override
+ public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Scrobbling not available in offline mode");
+ }
+
+ @Override
+ public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Album lists not available in offline mode");
+ }
+
+ @Override
+ public String getVideoUrl(Context context, String id) {
+ return null;
+ }
+
+ @Override
+ public JukeboxStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
+ throw new OfflineException("Jukebox not available in offline mode");
+ }
+
+ @Override
+ public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception {
+ File root = FileUtil.getMusicDirectory(context);
+ List children = new LinkedList();
+ listFilesRecursively(root, children);
+ MusicDirectory result = new MusicDirectory();
+
+ if (children.isEmpty()) {
+ return result;
+ }
+ Random random = new Random();
+ for (int i = 0; i < size; i++) {
+ File file = children.get(random.nextInt(children.size()));
+ result.addChild(createEntry(context, file, getName(file)));
+ }
+
+ return result;
+ }
+
+ private void listFilesRecursively(File parent, List children) {
+ for (File file : FileUtil.listMusicFiles(parent)) {
+ if (file.isFile()) {
+ children.add(file);
+ } else {
+ listFilesRecursively(file, children);
+ }
+ }
+ }
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/RESTMusicService.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/RESTMusicService.java
new file mode 100644
index 00000000..1d99f2d9
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/RESTMusicService.java
@@ -0,0 +1,768 @@
+/*
+ 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 .
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.androidapp.service;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.conn.params.ConnPerRouteBean;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.scheme.SocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+import net.sourceforge.subsonic.androidapp.R;
+import net.sourceforge.subsonic.androidapp.domain.Indexes;
+import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
+import net.sourceforge.subsonic.androidapp.domain.Lyrics;
+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
+import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
+import net.sourceforge.subsonic.androidapp.domain.Playlist;
+import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+import net.sourceforge.subsonic.androidapp.domain.SearchResult;
+import net.sourceforge.subsonic.androidapp.domain.ServerInfo;
+import net.sourceforge.subsonic.androidapp.domain.Version;
+import net.sourceforge.subsonic.androidapp.service.parser.AlbumListParser;
+import net.sourceforge.subsonic.androidapp.service.parser.ErrorParser;
+import net.sourceforge.subsonic.androidapp.service.parser.IndexesParser;
+import net.sourceforge.subsonic.androidapp.service.parser.JukeboxStatusParser;
+import net.sourceforge.subsonic.androidapp.service.parser.LicenseParser;
+import net.sourceforge.subsonic.androidapp.service.parser.LyricsParser;
+import net.sourceforge.subsonic.androidapp.service.parser.MusicDirectoryParser;
+import net.sourceforge.subsonic.androidapp.service.parser.MusicFoldersParser;
+import net.sourceforge.subsonic.androidapp.service.parser.PlaylistParser;
+import net.sourceforge.subsonic.androidapp.service.parser.PlaylistsParser;
+import net.sourceforge.subsonic.androidapp.service.parser.RandomSongsParser;
+import net.sourceforge.subsonic.androidapp.service.parser.SearchResult2Parser;
+import net.sourceforge.subsonic.androidapp.service.parser.SearchResultParser;
+import net.sourceforge.subsonic.androidapp.service.parser.VersionParser;
+import net.sourceforge.subsonic.androidapp.service.ssl.SSLSocketFactory;
+import net.sourceforge.subsonic.androidapp.service.ssl.TrustSelfSignedStrategy;
+import net.sourceforge.subsonic.androidapp.util.CancellableTask;
+import net.sourceforge.subsonic.androidapp.util.Constants;
+import net.sourceforge.subsonic.androidapp.util.FileUtil;
+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
+import net.sourceforge.subsonic.androidapp.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class RESTMusicService implements MusicService {
+
+ private static final String TAG = RESTMusicService.class.getSimpleName();
+
+ private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000;
+ private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000;
+
+ // Allow 20 seconds extra timeout per MB offset.
+ private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0;
+
+ /**
+ * URL from which to fetch latest versions.
+ */
+ private static final String VERSION_URL = "http://subsonic.org/backend/version.view";
+
+ private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5;
+ private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L;
+
+ private final DefaultHttpClient httpClient;
+ private long redirectionLastChecked;
+ private int redirectionNetworkType = -1;
+ private String redirectFrom;
+ private String redirectTo;
+ private final ThreadSafeClientConnManager connManager;
+
+ public RESTMusicService() {
+
+ // Create and initialize default HTTP parameters
+ HttpParams params = new BasicHttpParams();
+ ConnManagerParams.setMaxTotalConnections(params, 20);
+ ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20));
+ HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT);
+
+ // Turn off stale checking. Our connections break all the time anyway,
+ // and it's not worth it to pay the penalty of checking every time.
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+
+ // Create and initialize scheme registry
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+ schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443));
+
+ // Create an HttpClient with the ThreadSafeClientConnManager.
+ // This connection manager must be used if more than one thread will
+ // be using the HttpClient.
+ connManager = new ThreadSafeClientConnManager(params, schemeRegistry);
+ httpClient = new DefaultHttpClient(connManager, params);
+ }
+
+ private SocketFactory createSSLSocketFactory() {
+ try {
+ return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x);
+ return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
+ }
+ }
+
+ @Override
+ public void ping(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "ping", null);
+ try {
+ new ErrorParser(context).parse(reader);
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
+ Reader reader = getReader(context, progressListener, "getLicense", null);
+ try {
+ ServerInfo serverInfo = new LicenseParser(context).parse(reader);
+ return serverInfo.isLicenseValid();
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ List cachedMusicFolders = readCachedMusicFolders(context);
+ if (cachedMusicFolders != null && !refresh) {
+ return cachedMusicFolders;
+ }
+
+ Reader reader = getReader(context, progressListener, "getMusicFolders", null);
+ try {
+ List musicFolders = new MusicFoldersParser(context).parse(reader, progressListener);
+ writeCachedMusicFolders(context, musicFolders);
+ return musicFolders;
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @Override
+ public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
+ Indexes cachedIndexes = readCachedIndexes(context, musicFolderId);
+ if (cachedIndexes != null && !refresh) {
+ return cachedIndexes;
+ }
+
+ long lastModified = cachedIndexes == null ? 0L : cachedIndexes.getLastModified();
+
+ List parameterNames = new ArrayList();
+ List
+ *
+ *
+ *
+ * Issue a certificate signing request (CSR)
+ *
keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore
+ *
+ *
+ *
+ *
+ * Send the certificate request to the trusted Certificate Authority for signature.
+ * One may choose to act as her own CA and sign the certificate request using a PKI
+ * tool, such as OpenSSL.
+ *
+ *
+ *
+ *
+ * Import the trusted CA root certificate
+ *
keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore
+ *
+ *
+ *
+ *
+ * Import the PKCS#7 file containg the complete certificate chain
+ *
keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore
+ *
+ *
+ *
+ *
+ * Verify the content the resultant keystore file
+ *
keytool -list -v -keystore my.keystore
+ *
+ *
+ *
+ *
+ * @since 4.0
+ */
+public class SSLSocketFactory implements LayeredSocketFactory {
+
+ public static final String TLS = "TLS";
+ public static final String SSL = "SSL";
+ public static final String SSLV2 = "SSLv2";
+
+ public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+ = new AllowAllHostnameVerifier();
+
+ public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+ = new BrowserCompatHostnameVerifier();
+
+ public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+ = new StrictHostnameVerifier();
+
+ /**
+ * The default factory using the default JVM settings for secure connections.
+ */
+ private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
+
+ /**
+ * Gets the default factory, which uses the default JVM settings for secure
+ * connections.
+ *
+ * @return the default factory
+ */
+ public static SSLSocketFactory getSocketFactory() {
+ return DEFAULT_FACTORY;
+ }
+
+ private final javax.net.ssl.SSLSocketFactory socketfactory;
+ private final HostNameResolver nameResolver;
+ // TODO: make final
+ private volatile X509HostnameVerifier hostnameVerifier;
+
+ private static SSLContext createSSLContext(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final TrustStrategy trustStrategy)
+ throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException {
+ if (algorithm == null) {
+ algorithm = TLS;
+ }
+ KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
+ kmfactory.init(keystore, keystorePassword != null ? keystorePassword.toCharArray(): null);
+ KeyManager[] keymanagers = kmfactory.getKeyManagers();
+ TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(keystore);
+ TrustManager[] trustmanagers = tmfactory.getTrustManagers();
+ if (trustmanagers != null && trustStrategy != null) {
+ for (int i = 0; i < trustmanagers.length; i++) {
+ TrustManager tm = trustmanagers[i];
+ if (tm instanceof X509TrustManager) {
+ trustmanagers[i] = new TrustManagerDecorator(
+ (X509TrustManager) tm, trustStrategy);
+ }
+ }
+ }
+
+ SSLContext sslcontext = SSLContext.getInstance(algorithm);
+ sslcontext.init(keymanagers, trustmanagers, random);
+ return sslcontext;
+ }
+
+ /**
+ * @deprecated Use {@link #SSLSocketFactory(String, KeyStore, String, KeyStore, SecureRandom, X509HostnameVerifier)}
+ */
+ @Deprecated
+ public SSLSocketFactory(
+ final String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final HostNameResolver nameResolver)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(createSSLContext(
+ algorithm, keystore, keystorePassword, truststore, random, null),
+ nameResolver);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(createSSLContext(
+ algorithm, keystore, keystorePassword, truststore, random, null),
+ hostnameVerifier);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final TrustStrategy trustStrategy,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(createSSLContext(
+ algorithm, keystore, keystorePassword, truststore, random, trustStrategy),
+ hostnameVerifier);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, keystore, keystorePassword, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{
+ this(TLS, keystore, keystorePassword, null, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, null, null, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final TrustStrategy trustStrategy,
+ final X509HostnameVerifier hostnameVerifier)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, null, null, null, null, trustStrategy, hostnameVerifier);
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final TrustStrategy trustStrategy)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ this(TLS, null, null, null, null, trustStrategy, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ public SSLSocketFactory(final SSLContext sslContext) {
+ this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+ }
+
+ /**
+ * @deprecated Use {@link #SSLSocketFactory(SSLContext)}
+ */
+ @Deprecated
+ public SSLSocketFactory(
+ final SSLContext sslContext, final HostNameResolver nameResolver) {
+ super();
+ this.socketfactory = sslContext.getSocketFactory();
+ this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+ this.nameResolver = nameResolver;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public SSLSocketFactory(
+ final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) {
+ super();
+ this.socketfactory = sslContext.getSocketFactory();
+ this.hostnameVerifier = hostnameVerifier;
+ this.nameResolver = null;
+ }
+
+ private SSLSocketFactory() {
+ super();
+ this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+ this.hostnameVerifier = null;
+ this.nameResolver = null;
+ }
+
+ /**
+ * @param params Optional parameters. Parameters passed to this method will have no effect.
+ * This method will create a unconnected instance of {@link Socket} class
+ * using {@link javax.net.ssl.SSLSocketFactory#createSocket()} method.
+ * @since 4.1
+ */
+ @SuppressWarnings("cast")
+ public Socket createSocket(final HttpParams params) throws IOException {
+ // the cast makes sure that the factory is working as expected
+ return (SSLSocket) this.socketfactory.createSocket();
+ }
+
+ @SuppressWarnings("cast")
+ public Socket createSocket() throws IOException {
+ // the cast makes sure that the factory is working as expected
+ return (SSLSocket) this.socketfactory.createSocket();
+ }
+
+ /**
+ * @since 4.1
+ */
+ public Socket connectSocket(
+ final Socket sock,
+ final InetSocketAddress remoteAddress,
+ final InetSocketAddress localAddress,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ if (remoteAddress == null) {
+ throw new IllegalArgumentException("Remote address may not be null");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ SSLSocket sslsock = (SSLSocket) (sock != null ? sock : createSocket());
+ if (localAddress != null) {
+// sslsock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params));
+ sslsock.bind(localAddress);
+ }
+
+ int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+ int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+ try {
+ sslsock.connect(remoteAddress, connTimeout);
+ } catch (SocketTimeoutException ex) {
+ throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/"
+ + remoteAddress.getAddress() + " timed out");
+ }
+ sslsock.setSoTimeout(soTimeout);
+ if (this.hostnameVerifier != null) {
+ try {
+ this.hostnameVerifier.verify(remoteAddress.getHostName(), sslsock);
+ // verifyHostName() didn't blowup - good!
+ } catch (IOException iox) {
+ // close the socket before re-throwing the exception
+ try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
+ throw iox;
+ }
+ }
+ return sslsock;
+ }
+
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates TLS/SSL socket connections
+ * which, by default, are considered secure.
+ *
+ * Derived classes may override this method to perform
+ * runtime checks, for example based on the cypher suite.
+ *
+ * @param sock the connected socket
+ *
+ * @return true
+ *
+ * @throws IllegalArgumentException if the argument is invalid
+ */
+ public boolean isSecure(final Socket sock) throws IllegalArgumentException {
+ if (sock == null) {
+ throw new IllegalArgumentException("Socket may not be null");
+ }
+ // This instanceof check is in line with createSocket() above.
+ if (!(sock instanceof SSLSocket)) {
+ throw new IllegalArgumentException("Socket not created by this factory");
+ }
+ // This check is performed last since it calls the argument object.
+ if (sock.isClosed()) {
+ throw new IllegalArgumentException("Socket is closed");
+ }
+ return true;
+ }
+
+ /**
+ * @since 4.1
+ */
+ public Socket createLayeredSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final boolean autoClose) throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
+ socket,
+ host,
+ port,
+ autoClose
+ );
+ if (this.hostnameVerifier != null) {
+ this.hostnameVerifier.verify(host, sslSocket);
+ }
+ // verifyHostName() didn't blowup - good!
+ return sslSocket;
+ }
+
+ @Deprecated
+ public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
+ if ( hostnameVerifier == null ) {
+ throw new IllegalArgumentException("Hostname verifier may not be null");
+ }
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ public X509HostnameVerifier getHostnameVerifier() {
+ return this.hostnameVerifier;
+ }
+
+ /**
+ * @deprecated Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)}
+ */
+ @Deprecated
+ public Socket connectSocket(
+ final Socket socket,
+ final String host, int port,
+ final InetAddress localAddress, int localPort,
+ final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
+ InetSocketAddress local = null;
+ if (localAddress != null || localPort > 0) {
+ // we need to bind explicitly
+ if (localPort < 0) {
+ localPort = 0; // indicates "any"
+ }
+ local = new InetSocketAddress(localAddress, localPort);
+ }
+ InetAddress remoteAddress;
+ if (this.nameResolver != null) {
+ remoteAddress = this.nameResolver.resolve(host);
+ } else {
+ remoteAddress = InetAddress.getByName(host);
+ }
+ InetSocketAddress remote = new InetSocketAddress(remoteAddress, port);
+ return connectSocket(socket, remote, local, params);
+ }
+
+ /**
+ * @deprecated Use {@link #createLayeredSocket(Socket, String, int, boolean)}
+ */
+ @Deprecated
+ public Socket createSocket(
+ final Socket socket,
+ final String host, int port,
+ boolean autoClose) throws IOException, UnknownHostException {
+ return createLayeredSocket(socket, host, port, autoClose);
+ }
+
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustManagerDecorator.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustManagerDecorator.java
new file mode 100644
index 00000000..41d98249
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustManagerDecorator.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package net.sourceforge.subsonic.androidapp.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * @since 4.1
+ */
+class TrustManagerDecorator implements X509TrustManager {
+
+ private final X509TrustManager trustManager;
+ private final TrustStrategy trustStrategy;
+
+ TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) {
+ super();
+ this.trustManager = trustManager;
+ this.trustStrategy = trustStrategy;
+ }
+
+ public void checkClientTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ this.trustManager.checkClientTrusted(chain, authType);
+ }
+
+ public void checkServerTrusted(
+ final X509Certificate[] chain, final String authType) throws CertificateException {
+ if (!this.trustStrategy.isTrusted(chain, authType)) {
+ this.trustManager.checkServerTrusted(chain, authType);
+ }
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return this.trustManager.getAcceptedIssuers();
+ }
+
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustSelfSignedStrategy.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustSelfSignedStrategy.java
new file mode 100644
index 00000000..4fdaaba2
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustSelfSignedStrategy.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package net.sourceforge.subsonic.androidapp.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A trust strategy that accepts self-signed certificates as trusted. Verification of all other
+ * certificates is done by the trust manager configured in the SSL context.
+ *
+ * @since 4.1
+ */
+public class TrustSelfSignedStrategy implements TrustStrategy {
+
+ public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ return true;
+ }
+
+}
diff --git a/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustStrategy.java b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustStrategy.java
new file mode 100644
index 00000000..3cf75b68
--- /dev/null
+++ b/subsonic-android/src/net/sourceforge/subsonic/androidapp/service/ssl/TrustStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package net.sourceforge.subsonic.androidapp.service.ssl;
+
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+/**
+ * A strategy to establish trustworthiness of certificates without consulting the trust manager
+ * configured in the actual SSL context. This interface can be used to override the standard
+ * JSSE certificate verification process.
+ *
+ * @since 4.1
+ */
+public interface TrustStrategy {
+
+ /**
+ * Determines whether the certificate chain can be trusted without consulting the trust manager
+ * configured in the actual SSL context. This method can be used to override the standard JSSE
+ * certificate verification process.
+ *
+ * Please note that, if this method returns false, the trust manager configured
+ * in the actual SSL context can still clear the certificate as trusted.
+ *
+ * @param chain the peer certificate chain
+ * @param authType the authentication type based on the client certificate
+ * @return true if the certificate can be trusted without verification by
+ * the trust manager, false otherwise.
+ * @throws CertificateException thrown if the certificate is not trusted or invalid.
+ */
+ boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException;
+
+}
--
cgit v1.2.3