Clover Coverage Report - Subsonic-Android Coverage Report
Coverage timestamp: ven dic 19 2014 17:57:13 EST
../../../../../img/srcFileCovDistChart8.png 45% of files have more coverage
285   742   83   6,33
60   585   0,29   45
45     1,84  
1    
This report was generated with an evaluation server license. Purchase Clover or configure your license.
 
  RESTMusicService       Line # 106 285 83 77,2% 0.77179486
 
No Tests
 
1    /*
2    This file is part of Subsonic.
3   
4    Subsonic is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8   
9    Subsonic is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12    GNU General Public License for more details.
13   
14    You should have received a copy of the GNU General Public License
15    along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
16   
17    Copyright 2009 (C) Sindre Mehus
18    */
19    package net.sourceforge.subsonic.androidapp.service;
20   
21    import java.io.FileOutputStream;
22    import java.io.IOException;
23    import java.io.InputStream;
24    import java.io.InputStreamReader;
25    import java.io.OutputStream;
26    import java.io.Reader;
27    import java.net.URLEncoder;
28    import java.util.ArrayList;
29    import java.util.Arrays;
30    import java.util.Collections;
31    import java.util.LinkedList;
32    import java.util.List;
33    import java.util.concurrent.atomic.AtomicReference;
34   
35    import org.apache.http.Header;
36    import org.apache.http.HttpEntity;
37    import org.apache.http.HttpHost;
38    import org.apache.http.HttpResponse;
39    import org.apache.http.NameValuePair;
40    import org.apache.http.auth.AuthScope;
41    import org.apache.http.auth.UsernamePasswordCredentials;
42    import org.apache.http.client.entity.UrlEncodedFormEntity;
43    import org.apache.http.client.methods.HttpPost;
44    import org.apache.http.client.methods.HttpUriRequest;
45    import org.apache.http.conn.params.ConnManagerParams;
46    import org.apache.http.conn.params.ConnPerRouteBean;
47    import org.apache.http.conn.scheme.PlainSocketFactory;
48    import org.apache.http.conn.scheme.Scheme;
49    import org.apache.http.conn.scheme.SchemeRegistry;
50    import org.apache.http.conn.scheme.SocketFactory;
51    import org.apache.http.impl.client.DefaultHttpClient;
52    import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
53    import org.apache.http.message.BasicHeader;
54    import org.apache.http.message.BasicNameValuePair;
55    import org.apache.http.params.BasicHttpParams;
56    import org.apache.http.params.HttpConnectionParams;
57    import org.apache.http.params.HttpParams;
58    import org.apache.http.protocol.BasicHttpContext;
59    import org.apache.http.protocol.ExecutionContext;
60    import org.apache.http.protocol.HttpContext;
61   
62    import android.content.Context;
63    import android.content.SharedPreferences;
64    import android.content.pm.PackageInfo;
65    import android.graphics.Bitmap;
66    import android.graphics.BitmapFactory;
67    import android.net.ConnectivityManager;
68    import android.net.NetworkInfo;
69    import android.util.Log;
70    import net.sourceforge.subsonic.androidapp.R;
71    import net.sourceforge.subsonic.androidapp.domain.Indexes;
72    import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
73    import net.sourceforge.subsonic.androidapp.domain.Lyrics;
74    import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
75    import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
76    import net.sourceforge.subsonic.androidapp.domain.Playlist;
77    import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
78    import net.sourceforge.subsonic.androidapp.domain.SearchResult;
79    import net.sourceforge.subsonic.androidapp.domain.ServerInfo;
80    import net.sourceforge.subsonic.androidapp.domain.Version;
81    import net.sourceforge.subsonic.androidapp.service.parser.AlbumListParser;
82    import net.sourceforge.subsonic.androidapp.service.parser.ErrorParser;
83    import net.sourceforge.subsonic.androidapp.service.parser.IndexesParser;
84    import net.sourceforge.subsonic.androidapp.service.parser.JukeboxStatusParser;
85    import net.sourceforge.subsonic.androidapp.service.parser.LicenseParser;
86    import net.sourceforge.subsonic.androidapp.service.parser.LyricsParser;
87    import net.sourceforge.subsonic.androidapp.service.parser.MusicDirectoryParser;
88    import net.sourceforge.subsonic.androidapp.service.parser.MusicFoldersParser;
89    import net.sourceforge.subsonic.androidapp.service.parser.PlaylistParser;
90    import net.sourceforge.subsonic.androidapp.service.parser.PlaylistsParser;
91    import net.sourceforge.subsonic.androidapp.service.parser.RandomSongsParser;
92    import net.sourceforge.subsonic.androidapp.service.parser.SearchResult2Parser;
93    import net.sourceforge.subsonic.androidapp.service.parser.SearchResultParser;
94    import net.sourceforge.subsonic.androidapp.service.parser.VersionParser;
95    import net.sourceforge.subsonic.androidapp.service.ssl.SSLSocketFactory;
96    import net.sourceforge.subsonic.androidapp.service.ssl.TrustSelfSignedStrategy;
97    import net.sourceforge.subsonic.androidapp.util.CancellableTask;
98    import net.sourceforge.subsonic.androidapp.util.Constants;
99    import net.sourceforge.subsonic.androidapp.util.FileUtil;
100    import net.sourceforge.subsonic.androidapp.util.ProgressListener;
101    import net.sourceforge.subsonic.androidapp.util.Util;
102   
103    /**
104    * @author Sindre Mehus
105    */
 
106    public class RESTMusicService implements MusicService {
107   
108    private static final String TAG = RESTMusicService.class.getSimpleName();
109   
110    private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000;
111    private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000;
112    private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000;
113    private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000;
114    private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000;
115   
116    // Allow 20 seconds extra timeout per MB offset.
117    private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0;
118   
119    /**
120    * URL from which to fetch latest versions.
121    */
122    private static final String VERSION_URL = "http://subsonic.org/backend/version.view";
123   
124    private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5;
125    private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L;
126   
127    private final DefaultHttpClient httpClient;
128    private long redirectionLastChecked;
129    private int redirectionNetworkType = -1;
130    private String redirectFrom;
131    private String redirectTo;
132    private final ThreadSafeClientConnManager connManager;
133   
 
134  2 toggle public RESTMusicService() {
135   
136    // Create and initialize default HTTP parameters
137  2 HttpParams params = new BasicHttpParams();
138  2 ConnManagerParams.setMaxTotalConnections(params, 20);
139  2 ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20));
140  2 HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT);
141  2 HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT);
142   
143    // Turn off stale checking. Our connections break all the time anyway,
144    // and it's not worth it to pay the penalty of checking every time.
145  2 HttpConnectionParams.setStaleCheckingEnabled(params, false);
146   
147    // Create and initialize scheme registry
148  2 SchemeRegistry schemeRegistry = new SchemeRegistry();
149  2 schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
150  2 schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443));
151   
152    // Create an HttpClient with the ThreadSafeClientConnManager.
153    // This connection manager must be used if more than one thread will
154    // be using the HttpClient.
155  2 connManager = new ThreadSafeClientConnManager(params, schemeRegistry);
156  2 httpClient = new DefaultHttpClient(connManager, params);
157    }
158   
 
159  2 toggle private SocketFactory createSSLSocketFactory() {
160  2 try {
161  2 return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
162    } catch (Throwable x) {
163  0 Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x);
164  0 return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
165    }
166    }
167   
 
168  2 toggle @Override
169    public void ping(Context context, ProgressListener progressListener) throws Exception {
170  2 Reader reader = getReader(context, progressListener, "ping", null);
171  2 try {
172  2 new ErrorParser(context).parse(reader);
173    } finally {
174  2 Util.close(reader);
175    }
176    }
177   
 
178  1 toggle @Override
179    public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
180  1 Reader reader = getReader(context, progressListener, "getLicense", null);
181  1 try {
182  1 ServerInfo serverInfo = new LicenseParser(context).parse(reader);
183  1 return serverInfo.isLicenseValid();
184    } finally {
185  1 Util.close(reader);
186    }
187    }
188   
 
189  2 toggle public List<MusicFolder> getMusicFolders(Context context, ProgressListener progressListener) throws Exception {
190  2 Reader reader = getReader(context, progressListener, "getMusicFolders", null);
191  2 try {
192  2 return new MusicFoldersParser(context).parse(reader, progressListener);
193    } finally {
194  2 Util.close(reader);
195    }
196    }
197   
 
198  3 toggle @Override
199    public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
200  3 Indexes cachedIndexes = readCachedIndexes(context, musicFolderId);
201  3 long lastModified = cachedIndexes == null ? 0L : cachedIndexes.getLastModified();
202   
203  3 List<String> parameterNames = new ArrayList<String>();
204  3 List<Object> parameterValues = new ArrayList<Object>();
205   
206  3 parameterNames.add("ifModifiedSince");
207  3 parameterValues.add(lastModified);
208   
209  3 if (musicFolderId != null) {
210  0 parameterNames.add("musicFolderId");
211  0 parameterValues.add(musicFolderId);
212    }
213   
214  3 Reader reader = getReader(context, progressListener, "getIndexes", null, parameterNames, parameterValues);
215  3 try {
216  3 Indexes indexes = new IndexesParser(context).parse(reader, progressListener);
217  3 if (indexes != null) {
218  1 writeCachedIndexes(context, indexes, musicFolderId);
219  1 return indexes;
220    }
221  2 return cachedIndexes;
222    } finally {
223  3 Util.close(reader);
224    }
225    }
226   
 
227  3 toggle private Indexes readCachedIndexes(Context context, String musicFolderId) {
228  3 String filename = getCachedIndexesFilename(context, musicFolderId);
229  3 return FileUtil.deserialize(context, filename);
230    }
231   
 
232  1 toggle private void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) {
233  1 String filename = getCachedIndexesFilename(context, musicFolderId);
234  1 FileUtil.serialize(context, indexes, filename);
235    }
236   
 
237  4 toggle private String getCachedIndexesFilename(Context context, String musicFolderId) {
238  4 String s = Util.getRestUrl(context, null) + musicFolderId;
239  4 return "indexes-" + Math.abs(s.hashCode()) + ".ser";
240    }
241   
 
242  5 toggle @Override
243    public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
244  5 Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id);
245  5 try {
246  5 return new MusicDirectoryParser(context).parse(reader, progressListener);
247    } finally {
248  5 Util.close(reader);
249    }
250    }
251   
 
252  1 toggle @Override
253    public SearchResult search(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
254  1 try {
255  1 return searchNew(critera, context, progressListener);
256    } catch (ServerTooOldException x) {
257    // Ensure backward compatibility with REST 1.3.
258  0 return searchOld(critera, context, progressListener);
259    }
260    }
261   
262    /**
263    * Search using the "search" REST method.
264    */
 
265  0 toggle private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
266  0 List<String> parameterNames = Arrays.asList("any", "songCount");
267  0 List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount());
268  0 Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues);
269  0 try {
270  0 return new SearchResultParser(context).parse(reader, progressListener);
271    } finally {
272  0 Util.close(reader);
273    }
274    }
275   
276    /**
277    * Search using the "search2" REST method, available in 1.4.0 and later.
278    */
 
279  1 toggle private SearchResult searchNew(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception {
280  1 checkServerVersion(context, "1.4", null);
281   
282  1 List<String> parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount");
283  1 List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getArtistCount(),
284    critera.getAlbumCount(), critera.getSongCount());
285  1 Reader reader = getReader(context, progressListener, "search2", null, parameterNames, parameterValues);
286  1 try {
287  1 return new SearchResult2Parser(context).parse(reader, progressListener);
288    } finally {
289  1 Util.close(reader);
290    }
291    }
292   
 
293  2 toggle @Override
294    public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
295  2 HttpParams params = new BasicHttpParams();
296  2 HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST);
297   
298  2 Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id);
299  2 try {
300  2 return new PlaylistParser(context).parse(reader, progressListener);
301    } finally {
302  2 Util.close(reader);
303    }
304    }
305   
 
306  2 toggle @Override
307    public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
308  2 Reader reader = getReader(context, progressListener, "getPlaylists", null);
309  2 try {
310  2 return new PlaylistsParser(context).parse(reader, progressListener);
311    } finally {
312  2 Util.close(reader);
313    }
314    }
315   
 
316  1 toggle @Override
317    public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
318  1 List<String> parameterNames = new LinkedList<String>();
319  1 List<Object> parameterValues = new LinkedList<Object>();
320   
321  1 if (id != null) {
322  0 parameterNames.add("playlistId");
323  0 parameterValues.add(id);
324    }
325  1 if (name != null) {
326  1 parameterNames.add("name");
327  1 parameterValues.add(name);
328    }
329  1 for (MusicDirectory.Entry entry : entries) {
330  8 parameterNames.add("songId");
331  8 parameterValues.add(entry.getId());
332    }
333   
334  1 Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues);
335  1 try {
336  1 new ErrorParser(context).parse(reader);
337    } finally {
338  1 Util.close(reader);
339    }
340    }
341   
 
342  1 toggle @Override
343    public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
344  1 Reader reader = getReader(context, progressListener, "getLyrics", null, Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title));
345  1 try {
346  1 return new LyricsParser(context).parse(reader, progressListener);
347    } finally {
348  1 Util.close(reader);
349    }
350    }
351   
 
352  4 toggle @Override
353    public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
354  4 checkServerVersion(context, "1.5", "Scrobbling not supported.");
355  4 Reader reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission));
356  4 try {
357  4 new ErrorParser(context).parse(reader);
358    } finally {
359  4 Util.close(reader);
360    }
361    }
362   
 
363  3 toggle @Override
364    public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
365  3 Reader reader = getReader(context, progressListener, "getAlbumList",
366    null, Arrays.asList("type", "size", "offset"), Arrays.<Object>asList(type, size, offset));
367  3 try {
368  3 return new AlbumListParser(context).parse(reader, progressListener);
369    } finally {
370  3 Util.close(reader);
371    }
372    }
373   
 
374  5 toggle @Override
375    public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception {
376  5 HttpParams params = new BasicHttpParams();
377  5 HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
378   
379  5 Reader reader = getReader(context, progressListener, "getRandomSongs", params, "size", size);
380  5 try {
381  5 return new RandomSongsParser(context).parse(reader, progressListener);
382    } finally {
383  5 Util.close(reader);
384    }
385    }
386   
 
387  0 toggle @Override
388    public Version getLocalVersion(Context context) throws Exception {
389  0 PackageInfo packageInfo = context.getPackageManager().getPackageInfo("net.sourceforge.subsonic.androidapp", 0);
390  0 return new Version(packageInfo.versionName);
391    }
392   
 
393  0 toggle @Override
394    public Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception {
395  0 Reader reader = getReaderForURL(context, VERSION_URL, null, null, null, progressListener);
396  0 try {
397  0 return new VersionParser().parse(reader);
398    } finally {
399  0 Util.close(reader);
400    }
401    }
402   
 
403  9 toggle private void checkServerVersion(Context context, String version, String text) throws ServerTooOldException {
404  9 Version serverVersion = Util.getServerRestVersion(context);
405  9 Version requiredVersion = new Version(version);
406  9 boolean ok = serverVersion == null || serverVersion.compareTo(requiredVersion) >= 0;
407   
408  9 if (!ok) {
409  0 throw new ServerTooOldException(text, serverVersion, requiredVersion);
410    }
411    }
412   
 
413  38 toggle @Override
414    public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception {
415   
416    // Synchronize on the entry so that we don't download concurrently for the same song.
417  38 synchronized (entry) {
418   
419    // Use cached file, if existing.
420  38 Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
421  38 if (bitmap != null) {
422  8 return bitmap;
423    }
424   
425  30 String url = Util.getRestUrl(context, "getCoverArt");
426   
427  30 InputStream in = null;
428  30 try {
429  30 List<String> parameterNames = Arrays.asList("id", "size");
430  30 List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt(), size);
431  30 HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener);
432  29 in = entity.getContent();
433   
434    // If content type is XML, an error occured. Get it.
435  30 String contentType = Util.getContentType(entity);
436  30 if (contentType != null && contentType.startsWith("text/xml")) {
437  0 new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8));
438  0 return null; // Never reached.
439    }
440   
441  30 byte[] bytes = Util.toByteArray(in);
442   
443  30 if (saveToFile) {
444  8 OutputStream out = null;
445  8 try {
446  8 out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry));
447  8 out.write(bytes);
448    } finally {
449  8 Util.close(out);
450    }
451    }
452   
453  30 return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
454   
455    } finally {
456  30 Util.close(in);
457    }
458    }
459    }
460   
 
461  21 toggle @Override
462    public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception {
463   
464  21 String url = Util.getRestUrl(context, "stream");
465   
466    // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is
467    // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server.
468    // In that case, the server uses a long time before sending any data, causing the client to time out.
469  21 HttpParams params = new BasicHttpParams();
470  21 int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE);
471  21 HttpConnectionParams.setSoTimeout(params, timeout);
472   
473    // Add "Range" header if offset is given.
474  21 List<Header> headers = new ArrayList<Header>();
475  21 if (offset > 0) {
476  3 headers.add(new BasicHeader("Range", "bytes=" + offset + "-"));
477    }
478  21 List<String> parameterNames = Arrays.asList("id", "maxBitRate");
479  21 List<Object> parameterValues = Arrays.<Object>asList(song.getId(), maxBitrate);
480  21 HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task);
481   
482    // If content type is XML, an error occurred. Get it.
483  21 String contentType = Util.getContentType(response.getEntity());
484  21 if (contentType != null && contentType.startsWith("text/xml")) {
485  0 InputStream in = response.getEntity().getContent();
486  0 try {
487  0 new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8));
488    } finally {
489  0 Util.close(in);
490    }
491    }
492   
493  21 return response;
494    }
495   
 
496  0 toggle @Override
497    public String getVideoUrl(Context context, String id) {
498  0 StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "videoPlayer"));
499  0 builder.append("&id=").append(id);
500  0 builder.append("&maxBitRate=500");
501  0 builder.append("&autoplay=true");
502   
503  0 String url = rewriteUrlWithRedirect(context, builder.toString());
504  0 Log.i(TAG, "Using video URL: " + url);
505  0 return url;
506    }
507   
 
508  2 toggle @Override
509    public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
510  2 int n = ids.size();
511  2 List<String> parameterNames = new ArrayList<String>(n + 1);
512  2 parameterNames.add("action");
513  42 for (int i = 0; i < n; i++) {
514  40 parameterNames.add("id");
515    }
516  2 List<Object> parameterValues = new ArrayList<Object>();
517  2 parameterValues.add("set");
518  2 parameterValues.addAll(ids);
519   
520  2 return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
521    }
522   
 
523  0 toggle @Override
524    public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
525  0 List<String> parameterNames = Arrays.asList("action", "index", "offset");
526  0 List<Object> parameterValues = Arrays.<Object>asList("skip", index, offsetSeconds);
527  0 return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
528    }
529   
 
530  2 toggle @Override
531    public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
532  2 return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("stop"));
533    }
534   
 
535  0 toggle @Override
536    public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
537  0 return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("start"));
538    }
539   
 
540  0 toggle @Override
541    public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
542  0 return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("status"));
543    }
544   
 
545  0 toggle @Override
546    public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
547  0 List<String> parameterNames = Arrays.asList("action", "gain");
548  0 List<Object> parameterValues = Arrays.<Object>asList("setGain", gain);
549  0 return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
550   
551    }
552   
 
553  4 toggle private JukeboxStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception {
554  4 checkServerVersion(context, "1.7", "Jukebox not supported.");
555  4 Reader reader = getReader(context, progressListener, "jukeboxControl", null, parameterNames, parameterValues);
556  4 try {
557  4 return new JukeboxStatusParser(context).parse(reader);
558    } finally {
559  4 Util.close(reader);
560    }
561    }
562   
 
563  7 toggle private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception {
564  7 return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList());
565    }
566   
 
567  12 toggle private Reader getReader(Context context, ProgressListener progressListener, String method,
568    HttpParams requestParams, String parameterName, Object parameterValue) throws Exception {
569  12 return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.<Object>asList(parameterValue));
570    }
571   
 
572  36 toggle private Reader getReader(Context context, ProgressListener progressListener, String method,
573    HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues) throws Exception {
574   
575  36 if (progressListener != null) {
576  22 progressListener.updateProgress(R.string.service_connecting);
577    }
578   
579  36 String url = Util.getRestUrl(context, method);
580  36 return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
581    }
582   
 
583  36 toggle private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
584    List<Object> parameterValues, ProgressListener progressListener) throws Exception {
585  36 HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
586  36 if (entity == null) {
587  0 throw new RuntimeException("No entity received for URL " + url);
588    }
589   
590  36 InputStream in = entity.getContent();
591  36 return new InputStreamReader(in, Constants.UTF_8);
592    }
593   
 
594  66 toggle private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames,
595    List<Object> parameterValues, ProgressListener progressListener) throws Exception {
596  66 return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, null).getEntity();
597    }
598   
 
599  87 toggle private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams,
600    List<String> parameterNames, List<Object> parameterValues,
601    List<Header> headers, ProgressListener progressListener, CancellableTask task) throws Exception {
602  87 Log.d(TAG, "Connections in pool: " + connManager.getConnectionsInPool());
603   
604    // If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being
605    // received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus
606    // loosing its entity.
607  87 if (parameterNames != null && parameterNames.size() < 10) {
608  85 StringBuilder builder = new StringBuilder(url);
609  236 for (int i = 0; i < parameterNames.size(); i++) {
610  151 builder.append("&").append(parameterNames.get(i)).append("=");
611  151 builder.append(URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"));
612    }
613  85 url = builder.toString();
614  85 parameterNames = null;
615  85 parameterValues = null;
616    }
617   
618  87 String rewrittenUrl = rewriteUrlWithRedirect(context, url);
619  87 return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task);
620    }
621   
 
622  87 toggle private HttpResponse executeWithRetry(Context context, String url, String originalUrl, HttpParams requestParams,
623    List<String> parameterNames, List<Object> parameterValues,
624    List<Header> headers, ProgressListener progressListener, CancellableTask task) throws IOException {
625  87 Log.i(TAG, "Using URL " + url);
626   
627  87 final AtomicReference<Boolean> cancelled = new AtomicReference<Boolean>(false);
628  87 int attempts = 0;
629  87 while (true) {
630  87 attempts++;
631  87 HttpContext httpContext = new BasicHttpContext();
632  87 final HttpPost request = new HttpPost(url);
633   
634  87 if (task != null) {
635    // Attempt to abort the HTTP request if the task is cancelled.
636  21 task.setOnCancelListener(new CancellableTask.OnCancelListener() {
 
637  20 toggle @Override
638    public void onCancel() {
639  20 cancelled.set(true);
640  20 request.abort();
641    }
642    });
643    }
644   
645  87 if (parameterNames != null) {
646  2 List<NameValuePair> params = new ArrayList<NameValuePair>();
647  44 for (int i = 0; i < parameterNames.size(); i++) {
648  42 params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i))));
649    }
650  2 request.setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8));
651    }
652   
653  87 if (requestParams != null) {
654  28 request.setParams(requestParams);
655  28 Log.d(TAG, "Socket read timeout: " + HttpConnectionParams.getSoTimeout(requestParams) + " ms.");
656    }
657   
658  87 if (headers != null) {
659  21 for (Header header : headers) {
660  3 request.addHeader(header);
661    }
662    }
663   
664    // Set credentials to get through apache proxies that require authentication.
665  87 SharedPreferences prefs = Util.getPreferences(context);
666  87 int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
667  87 String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
668  87 String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
669  87 httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
670    new UsernamePasswordCredentials(username, password));
671   
672  87 try {
673  87 HttpResponse response = httpClient.execute(request, httpContext);
674  87 detectRedirect(originalUrl, context, httpContext);
675  86 return response;
676    } catch (IOException x) {
677  0 request.abort();
678  0 if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || cancelled.get()) {
679  0 throw x;
680    }
681  0 if (progressListener != null) {
682  0 String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1);
683  0 progressListener.updateProgress(msg);
684    }
685  0 Log.w(TAG, "Got IOException (" + attempts + "), will retry", x);
686  0 increaseTimeouts(requestParams);
687  0 Util.sleepQuietly(2000L);
688    }
689    }
690    }
691   
 
692  0 toggle private void increaseTimeouts(HttpParams requestParams) {
693  0 if (requestParams != null) {
694  0 int connectTimeout = HttpConnectionParams.getConnectionTimeout(requestParams);
695  0 if (connectTimeout != 0) {
696  0 HttpConnectionParams.setConnectionTimeout(requestParams, (int) (connectTimeout * 1.3F));
697    }
698  0 int readTimeout = HttpConnectionParams.getSoTimeout(requestParams);
699  0 if (readTimeout != 0) {
700  0 HttpConnectionParams.setSoTimeout(requestParams, (int) (readTimeout * 1.5F));
701    }
702    }
703    }
704   
 
705  87 toggle private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) {
706  87 HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST);
707  87 HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
708  87 String redirectedUrl = host.toURI() + request.getURI();
709   
710  87 redirectFrom = originalUrl.substring(0, originalUrl.indexOf("/rest/"));
711  87 redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/"));
712   
713  87 Log.i(TAG, redirectFrom + " redirects to " + redirectTo);
714  87 redirectionLastChecked = System.currentTimeMillis();
715  87 redirectionNetworkType = getCurrentNetworkType(context);
716    }
717   
 
718  87 toggle private String rewriteUrlWithRedirect(Context context, String url) {
719   
720    // Only cache for a certain time.
721  87 if (System.currentTimeMillis() - redirectionLastChecked > REDIRECTION_CHECK_INTERVAL_MILLIS) {
722  1 return url;
723    }
724   
725    // Ignore cache if network type has changed.
726  86 if (redirectionNetworkType != getCurrentNetworkType(context)) {
727  0 return url;
728    }
729   
730  86 if (redirectFrom == null || redirectTo == null) {
731  0 return url;
732    }
733   
734  86 return url.replace(redirectFrom, redirectTo);
735    }
736   
 
737  173 toggle private int getCurrentNetworkType(Context context) {
738  173 ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
739  173 NetworkInfo networkInfo = manager.getActiveNetworkInfo();
740  173 return networkInfo == null ? -1 : networkInfo.getType();
741    }
742    }