- With Subsonic you can easily stream or download music from your home computer to your Android phone
+ With Subphonic you can easily stream or download music from your home computer to your Android phone
(and do lots of other cool stuff too).
diff --git a/subsonic-android/build.properties b/subsonic-android/build.properties
new file mode 100644
index 00000000..e69de29b
diff --git a/subsonic-android/default.properties b/subsonic-android/default.properties
new file mode 100644
index 00000000..e69de29b
diff --git a/subsonic-android/patches/jukebox-patch.txt b/subsonic-android/patches/jukebox-patch.txt
index 5bc92000..3840eb0a 100644
--- a/subsonic-android/patches/jukebox-patch.txt
+++ b/subsonic-android/patches/jukebox-patch.txt
@@ -3,12 +3,12 @@ Index: AndroidManifest.xml
--- AndroidManifest.xml (revision 2441)
+++ AndroidManifest.xml (working copy)
@@ -114,7 +114,8 @@
- a:authorities="net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider"/>
+ a:authorities="github.daneren2005.subphonic.provider.SearchSuggestionProvider"/>
-+ a:value="net.sourceforge.subsonic.androidapp.activity.QueryReceiverActivity"/>
-+
+- a:value="github.daneren2005.subphonic.activity.QueryReceiverActivity"/>
++ a:value="github.daneren2005.subphonic.activity.QueryReceiverActivity"/>
++
@@ -224,21 +224,21 @@ Index: src/net/sourceforge/subsonic/androidapp/activity/JukeboxActivity.java
+ Copyright 2009 (C) Sindre Mehus
+ */
+
-+package net.sourceforge.subsonic.androidapp.activity;
++package github.daneren2005.subphonic.activity;
+
+import java.util.List;
+
-+import net.sourceforge.subsonic.androidapp.R;
-+import net.sourceforge.subsonic.androidapp.domain.Jukebox;
-+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
-+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory.Entry;
-+import net.sourceforge.subsonic.androidapp.service.MusicService;
-+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
-+import net.sourceforge.subsonic.androidapp.util.BackgroundTask;
-+import net.sourceforge.subsonic.androidapp.util.JukeboxSongView;
-+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
-+import net.sourceforge.subsonic.androidapp.util.TabActivityBackgroundTask;
-+import net.sourceforge.subsonic.androidapp.util.Util;
++import github.daneren2005.subphonic.R;
++import github.daneren2005.subphonic.domain.Jukebox;
++import github.daneren2005.subphonic.domain.MusicDirectory;
++import github.daneren2005.subphonic.domain.MusicDirectory.Entry;
++import github.daneren2005.subphonic.service.MusicService;
++import github.daneren2005.subphonic.service.MusicServiceFactory;
++import github.daneren2005.subphonic.util.BackgroundTask;
++import github.daneren2005.subphonic.util.JukeboxSongView;
++import github.daneren2005.subphonic.util.ProgressListener;
++import github.daneren2005.subphonic.util.TabActivityBackgroundTask;
++import github.daneren2005.subphonic.util.Util;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
@@ -485,16 +485,16 @@ Index: src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java
import java.util.LinkedList;
import java.util.List;
-+import net.sourceforge.subsonic.androidapp.R;
-+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
-+import net.sourceforge.subsonic.androidapp.service.DownloadService;
-+import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
-+import net.sourceforge.subsonic.androidapp.service.MusicService;
-+import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
-+import net.sourceforge.subsonic.androidapp.util.Constants;
-+import net.sourceforge.subsonic.androidapp.util.ImageLoader;
-+import net.sourceforge.subsonic.androidapp.util.ModalBackgroundTask;
-+import net.sourceforge.subsonic.androidapp.util.Util;
++import github.daneren2005.subphonic.R;
++import github.daneren2005.subphonic.domain.MusicDirectory;
++import github.daneren2005.subphonic.service.DownloadService;
++import github.daneren2005.subphonic.service.DownloadServiceImpl;
++import github.daneren2005.subphonic.service.MusicService;
++import github.daneren2005.subphonic.service.MusicServiceFactory;
++import github.daneren2005.subphonic.util.Constants;
++import github.daneren2005.subphonic.util.ImageLoader;
++import github.daneren2005.subphonic.util.ModalBackgroundTask;
++import github.daneren2005.subphonic.util.Util;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -502,16 +502,16 @@ Index: src/net/sourceforge/subsonic/androidapp/activity/SubsonicTabActivity.java
import android.view.View;
import android.view.Window;
import android.widget.TextView;
--import net.sourceforge.subsonic.androidapp.R;
--import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
--import net.sourceforge.subsonic.androidapp.service.DownloadService;
--import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
--import net.sourceforge.subsonic.androidapp.service.MusicService;
--import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
--import net.sourceforge.subsonic.androidapp.util.Constants;
--import net.sourceforge.subsonic.androidapp.util.ImageLoader;
--import net.sourceforge.subsonic.androidapp.util.ModalBackgroundTask;
--import net.sourceforge.subsonic.androidapp.util.Util;
+-import github.daneren2005.subphonic.R;
+-import github.daneren2005.subphonic.domain.MusicDirectory;
+-import github.daneren2005.subphonic.service.DownloadService;
+-import github.daneren2005.subphonic.service.DownloadServiceImpl;
+-import github.daneren2005.subphonic.service.MusicService;
+-import github.daneren2005.subphonic.service.MusicServiceFactory;
+-import github.daneren2005.subphonic.util.Constants;
+-import github.daneren2005.subphonic.util.ImageLoader;
+-import github.daneren2005.subphonic.util.ModalBackgroundTask;
+-import github.daneren2005.subphonic.util.Util;
/**
* @author Sindre Mehus
@@ -653,7 +653,7 @@ Index: src/net/sourceforge/subsonic/androidapp/domain/Jukebox.java
+ Copyright 2009 (C) Sindre Mehus
+ */
+
-+package net.sourceforge.subsonic.androidapp.domain;
++package github.daneren2005.subphonic.domain;
+
+import java.io.Serializable;
+import java.util.List;
@@ -789,7 +789,7 @@ Index: src/net/sourceforge/subsonic/androidapp/domain/User.java
+ Copyright 2009 (C) Sindre Mehus
+ */
+
-+package net.sourceforge.subsonic.androidapp.domain;
++package github.daneren2005.subphonic.domain;
+
+import java.io.Serializable;
+
@@ -820,15 +820,15 @@ Index: src/net/sourceforge/subsonic/androidapp/service/CachedMusicService.java
@@ -21,9 +21,11 @@
import android.content.Context;
import android.graphics.Bitmap;
- import net.sourceforge.subsonic.androidapp.domain.Indexes;
-+import net.sourceforge.subsonic.androidapp.domain.Jukebox;
- 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.User;
- import net.sourceforge.subsonic.androidapp.domain.Version;
- import net.sourceforge.subsonic.androidapp.domain.SearchResult;
- import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+ import github.daneren2005.subphonic.domain.Indexes;
++import github.daneren2005.subphonic.domain.Jukebox;
+ import github.daneren2005.subphonic.domain.MusicDirectory;
+ import github.daneren2005.subphonic.domain.MusicFolder;
+ import github.daneren2005.subphonic.domain.Playlist;
++import github.daneren2005.subphonic.domain.User;
+ import github.daneren2005.subphonic.domain.Version;
+ import github.daneren2005.subphonic.domain.SearchResult;
+ import github.daneren2005.subphonic.domain.SearchCritera;
@@ -199,4 +201,17 @@
restUrl = newUrl;
}
@@ -854,15 +854,15 @@ Index: src/net/sourceforge/subsonic/androidapp/service/MusicService.java
@@ -21,9 +21,11 @@
import android.content.Context;
import android.graphics.Bitmap;
- import net.sourceforge.subsonic.androidapp.domain.Indexes;
-+import net.sourceforge.subsonic.androidapp.domain.Jukebox;
- 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.User;
- import net.sourceforge.subsonic.androidapp.domain.Version;
- import net.sourceforge.subsonic.androidapp.domain.SearchResult;
- import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
+ import github.daneren2005.subphonic.domain.Indexes;
++import github.daneren2005.subphonic.domain.Jukebox;
+ import github.daneren2005.subphonic.domain.MusicDirectory;
+ import github.daneren2005.subphonic.domain.MusicFolder;
+ import github.daneren2005.subphonic.domain.Playlist;
++import github.daneren2005.subphonic.domain.User;
+ import github.daneren2005.subphonic.domain.Version;
+ import github.daneren2005.subphonic.domain.SearchResult;
+ import github.daneren2005.subphonic.domain.SearchCritera;
@@ -74,4 +76,8 @@
Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception;
@@ -879,33 +879,33 @@ Index: src/net/sourceforge/subsonic/androidapp/service/RESTMusicService.java
+++ src/net/sourceforge/subsonic/androidapp/service/RESTMusicService.java (working copy)
@@ -28,6 +28,7 @@
import android.util.Log;
- import net.sourceforge.subsonic.androidapp.R;
- import net.sourceforge.subsonic.androidapp.domain.Indexes;
-+import net.sourceforge.subsonic.androidapp.domain.Jukebox;
- import net.sourceforge.subsonic.androidapp.domain.Lyrics;
- import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
- import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
+ import github.daneren2005.subphonic.R;
+ import github.daneren2005.subphonic.domain.Indexes;
++import github.daneren2005.subphonic.domain.Jukebox;
+ import github.daneren2005.subphonic.domain.Lyrics;
+ import github.daneren2005.subphonic.domain.MusicDirectory;
+ import github.daneren2005.subphonic.domain.MusicFolder;
@@ -35,10 +36,12 @@
- 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.User;
- 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.JukeboxParser;
- 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 github.daneren2005.subphonic.domain.SearchCritera;
+ import github.daneren2005.subphonic.domain.SearchResult;
+ import github.daneren2005.subphonic.domain.ServerInfo;
++import github.daneren2005.subphonic.domain.User;
+ import github.daneren2005.subphonic.domain.Version;
+ import github.daneren2005.subphonic.service.parser.AlbumListParser;
+ import github.daneren2005.subphonic.service.parser.ErrorParser;
+ import github.daneren2005.subphonic.service.parser.IndexesParser;
++import github.daneren2005.subphonic.service.parser.JukeboxParser;
+ import github.daneren2005.subphonic.service.parser.LicenseParser;
+ import github.daneren2005.subphonic.service.parser.LyricsParser;
+ import github.daneren2005.subphonic.service.parser.MusicDirectoryParser;
@@ -48,6 +51,7 @@
- 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.UserParser;
- 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 github.daneren2005.subphonic.service.parser.RandomSongsParser;
+ import github.daneren2005.subphonic.service.parser.SearchResult2Parser;
+ import github.daneren2005.subphonic.service.parser.SearchResultParser;
++import github.daneren2005.subphonic.service.parser.UserParser;
+ import github.daneren2005.subphonic.service.parser.VersionParser;
+ import github.daneren2005.subphonic.service.ssl.SSLSocketFactory;
+ import github.daneren2005.subphonic.service.ssl.TrustSelfSignedStrategy;
@@ -195,6 +199,53 @@
Util.close(reader);
}
@@ -1000,14 +1000,14 @@ Index: src/net/sourceforge/subsonic/androidapp/service/parser/JukeboxParser.java
+ Copyright 2009 (C) Sindre Mehus
+ */
+
-+package net.sourceforge.subsonic.androidapp.service.parser;
++package github.daneren2005.subphonic.service.parser;
+
+import java.io.Reader;
+
-+import net.sourceforge.subsonic.androidapp.R;
-+import net.sourceforge.subsonic.androidapp.domain.Jukebox;
-+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
-+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
++import github.daneren2005.subphonic.R;
++import github.daneren2005.subphonic.domain.Jukebox;
++import github.daneren2005.subphonic.domain.MusicDirectory;
++import github.daneren2005.subphonic.util.ProgressListener;
+
+import org.xmlpull.v1.XmlPullParser;
+
@@ -1091,13 +1091,13 @@ Index: src/net/sourceforge/subsonic/androidapp/service/parser/UserParser.java
+ Copyright 2009 (C) Sindre Mehus
+ */
+
-+package net.sourceforge.subsonic.androidapp.service.parser;
++package github.daneren2005.subphonic.service.parser;
+
+import java.io.Reader;
+
-+import net.sourceforge.subsonic.androidapp.R;
-+import net.sourceforge.subsonic.androidapp.domain.User;
-+import net.sourceforge.subsonic.androidapp.util.ProgressListener;
++import github.daneren2005.subphonic.R;
++import github.daneren2005.subphonic.domain.User;
++import github.daneren2005.subphonic.util.ProgressListener;
+
+import org.xmlpull.v1.XmlPullParser;
+
@@ -1163,10 +1163,10 @@ Index: src/net/sourceforge/subsonic/androidapp/util/JukeboxSongView.java
+ Copyright 2009 (C) Sindre Mehus
+ */
+
-+package net.sourceforge.subsonic.androidapp.util;
++package github.daneren2005.subphonic.util;
+
-+import net.sourceforge.subsonic.androidapp.R;
-+import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
++import github.daneren2005.subphonic.R;
++import github.daneren2005.subphonic.domain.MusicDirectory;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
diff --git a/subsonic-android/private/cache/retriever/catalog.xml b/subsonic-android/private/cache/retriever/catalog.xml
new file mode 100644
index 00000000..9a13de23
--- /dev/null
+++ b/subsonic-android/private/cache/retriever/catalog.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/subsonic-android/proguard-project.txt b/subsonic-android/proguard-project.txt
new file mode 100644
index 00000000..b60ae7ea
--- /dev/null
+++ b/subsonic-android/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/subsonic-android/res/layout-land/download.xml b/subsonic-android/res/layout-land/download.xml
index 79dcc650..a63e7d9e 100644
--- a/subsonic-android/res/layout-land/download.xml
+++ b/subsonic-android/res/layout-land/download.xml
@@ -165,7 +165,7 @@
-
-
+
diff --git a/subsonic-android/res/layout-port/download.xml b/subsonic-android/res/layout-port/download.xml
index 73e94eb9..c2ca0a4a 100644
--- a/subsonic-android/res/layout-port/download.xml
+++ b/subsonic-android/res/layout-port/download.xml
@@ -4,7 +4,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent">
-
-
+
-
-
+
diff --git a/subsonic-android/res/layout/download_slider.xml b/subsonic-android/res/layout/download_slider.xml
index a4c62ca2..98b54df3 100644
--- a/subsonic-android/res/layout/download_slider.xml
+++ b/subsonic-android/res/layout/download_slider.xml
@@ -1,5 +1,5 @@
-
- Subsonic
+ SubphonicOKSaveCancel
@@ -12,14 +12,14 @@
UnpinDelete
- Subsonic home
+ Subphonic homeMedia librarySearchPlaylistsNow playingWelcome!
- Welcome to Subsonic! The app is currently configured to use the Subsonic demo server. After you\'ve
+ Welcome to Subphonic! The app is currently configured to use the Subsonic demo server. After you\'ve
set up your personal server (available from subsonic.org), please go to Settings and change the configuration to connect to it.Select serverShuffle play
@@ -39,7 +39,7 @@
PlaylistsHelp
- Welcome to Subsonic!
+ Welcome to Subphonic!BackClosefile:///android_asset/html/en/index.html
@@ -116,7 +116,7 @@
Error
- Subsonic settings
+ Subphonic settingsTest connectionServersUnused 1
@@ -176,7 +176,7 @@
Search history clearedOther settingsScrobble to Last.fm
- Remember to set up your Last.fm user and password on the Subsonic server
+ Remember to set up your Last.fm user and password on the Subphonic serverHide from otherHide music files from other apps.Takes effect next time Android scans your phone for music.
@@ -198,7 +198,7 @@
Reading from server.Reading from server. Done!
- Incompatible versions. Please upgrade Subsonic Android app.
+ Incompatible versions. Please upgrade Subphonic Android app.Incompatible versions. Please upgrade Subsonic server.Wrong username or password.Not authorized. Check user permissions in Subsonic server.
diff --git a/subsonic-android/res/xml/searchable.xml b/subsonic-android/res/xml/searchable.xml
index 7822e98c..b8040e24 100644
--- a/subsonic-android/res/xml/searchable.xml
+++ b/subsonic-android/res/xml/searchable.xml
@@ -4,6 +4,6 @@
android:hint="@string/search.title"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
android:voiceLanguageModel="web_search"
- android:searchSuggestAuthority="net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider"
+ android:searchSuggestAuthority="github.daneren2005.subphonic.provider.SearchSuggestionProvider"
android:searchSuggestSelection=" ?" >
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/DownloadActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/DownloadActivity.java
new file mode 100644
index 00000000..3c2d7b04
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/DownloadActivity.java
@@ -0,0 +1,874 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.ContextMenu;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.ViewFlipper;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.PlayerState;
+import github.daneren2005.subphonic.domain.RepeatMode;
+import github.daneren2005.subphonic.service.DownloadFile;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.HorizontalSlider;
+import github.daneren2005.subphonic.util.SilentBackgroundTask;
+import github.daneren2005.subphonic.util.SongView;
+import github.daneren2005.subphonic.util.Util;
+import github.daneren2005.subphonic.view.VisualizerView;
+
+import static github.daneren2005.subphonic.domain.PlayerState.*;
+
+public class DownloadActivity extends SubsonicTabActivity implements OnGestureListener {
+
+ private static final int DIALOG_SAVE_PLAYLIST = 100;
+ private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5;
+ private static final int COLOR_BUTTON_ENABLED = Color.rgb(129, 201, 54);
+ private static final int COLOR_BUTTON_DISABLED = Color.rgb(164, 166, 158);
+
+ private ViewFlipper playlistFlipper;
+ private ViewFlipper buttonBarFlipper;
+ private TextView emptyTextView;
+ private TextView songTitleTextView;
+ private TextView albumTextView;
+ private TextView artistTextView;
+ private ImageView albumArtImageView;
+ private ListView playlistView;
+ private TextView positionTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private HorizontalSlider progressBar;
+ private View previousButton;
+ private View nextButton;
+ private View pauseButton;
+ private View stopButton;
+ private View startButton;
+ private View shuffleButton;
+ private ImageButton repeatButton;
+ private Button equalizerButton;
+ private Button visualizerButton;
+ private Button jukeboxButton;
+ private View toggleListButton;
+ private ScheduledExecutorService executorService;
+ private DownloadFile currentPlaying;
+ private long currentRevision;
+ private EditText playlistNameView;
+ private GestureDetector gestureScanner;
+ private int swipeDistance;
+ private int swipeVelocity;
+ private VisualizerView visualizerView;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.download);
+
+ WindowManager w = getWindowManager();
+ Display d = w.getDefaultDisplay();
+ swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ gestureScanner = new GestureDetector(this);
+
+ playlistFlipper = (ViewFlipper) findViewById(R.id.download_playlist_flipper);
+ buttonBarFlipper = (ViewFlipper) findViewById(R.id.download_button_bar_flipper);
+ emptyTextView = (TextView) findViewById(R.id.download_empty);
+ songTitleTextView = (TextView) findViewById(R.id.download_song_title);
+ albumTextView = (TextView) findViewById(R.id.download_album);
+ artistTextView = (TextView) findViewById(R.id.download_artist);
+ albumArtImageView = (ImageView) findViewById(R.id.download_album_art_image);
+ positionTextView = (TextView) findViewById(R.id.download_position);
+ durationTextView = (TextView) findViewById(R.id.download_duration);
+ statusTextView = (TextView) findViewById(R.id.download_status);
+ progressBar = (HorizontalSlider) findViewById(R.id.download_progress_bar);
+ playlistView = (ListView) findViewById(R.id.download_list);
+ previousButton = findViewById(R.id.download_previous);
+ nextButton = findViewById(R.id.download_next);
+ pauseButton = findViewById(R.id.download_pause);
+ stopButton = findViewById(R.id.download_stop);
+ startButton = findViewById(R.id.download_start);
+ shuffleButton = findViewById(R.id.download_shuffle);
+ repeatButton = (ImageButton) findViewById(R.id.download_repeat);
+ equalizerButton = (Button) findViewById(R.id.download_equalizer);
+ visualizerButton = (Button) findViewById(R.id.download_visualizer);
+ jukeboxButton = (Button) findViewById(R.id.download_jukebox);
+ LinearLayout visualizerViewLayout = (LinearLayout) findViewById(R.id.download_visualizer_view_layout);
+
+ toggleListButton = findViewById(R.id.download_toggle_list);
+
+ View.OnTouchListener touchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent me) {
+ return gestureScanner.onTouchEvent(me);
+ }
+ };
+ previousButton.setOnTouchListener(touchListener);
+ nextButton.setOnTouchListener(touchListener);
+ pauseButton.setOnTouchListener(touchListener);
+ stopButton.setOnTouchListener(touchListener);
+ startButton.setOnTouchListener(touchListener);
+ equalizerButton.setOnTouchListener(touchListener);
+ visualizerButton.setOnTouchListener(touchListener);
+ jukeboxButton.setOnTouchListener(touchListener);
+ buttonBarFlipper.setOnTouchListener(touchListener);
+ emptyTextView.setOnTouchListener(touchListener);
+ albumArtImageView.setOnTouchListener(touchListener);
+
+ albumArtImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleFullscreenAlbumArt();
+ }
+ });
+
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().previous();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ });
+
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) {
+ getDownloadService().next();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }
+ });
+
+ pauseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ getDownloadService().pause();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ });
+
+ stopButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ getDownloadService().reset();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ });
+
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ start();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ });
+
+ shuffleButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ getDownloadService().shuffle();
+ Util.toast(DownloadActivity.this, R.string.download_menu_shuffle_notification);
+ }
+ });
+
+ repeatButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ RepeatMode repeatMode = getDownloadService().getRepeatMode().next();
+ getDownloadService().setRepeatMode(repeatMode);
+ onDownloadListChanged();
+ switch (repeatMode) {
+ case OFF:
+ Util.toast(DownloadActivity.this, R.string.download_repeat_off);
+ break;
+ case ALL:
+ Util.toast(DownloadActivity.this, R.string.download_repeat_all);
+ break;
+ case SINGLE:
+ Util.toast(DownloadActivity.this, R.string.download_repeat_single);
+ break;
+ default:
+ break;
+ }
+ }
+ });
+
+ equalizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startActivity(new Intent(DownloadActivity.this, EqualizerActivity.class));
+ }
+ });
+
+ visualizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean active = !visualizerView.isActive();
+ visualizerView.setActive(active);
+ getDownloadService().setShowVisualization(visualizerView.isActive());
+ updateButtons();
+ Util.toast(DownloadActivity.this, active ? R.string.download_visualizer_on : R.string.download_visualizer_off);
+ }
+ });
+
+ jukeboxButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled();
+ getDownloadService().setJukeboxEnabled(jukeboxEnabled);
+ updateButtons();
+ Util.toast(DownloadActivity.this, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
+ }
+ });
+
+ toggleListButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleFullscreenAlbumArt();
+ }
+ });
+
+ progressBar.setOnSliderChangeListener(new HorizontalSlider.OnSliderChangeListener() {
+ @Override
+ public void onSliderChanged(View view, int position, boolean inProgress) {
+ Util.toast(DownloadActivity.this, Util.formatDuration(position / 1000), true);
+ if (!inProgress) {
+ getDownloadService().seekTo(position);
+ onProgressChanged();
+ }
+ }
+ });
+ playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().play(position);
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ });
+
+ registerForContextMenu(playlistView);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null && getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.setShufflePlayEnabled(true);
+ }
+
+ boolean visualizerAvailable = downloadService != null && downloadService.getVisualizerController() != null;
+ boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerController() != null;
+
+ if (!equalizerAvailable) {
+ equalizerButton.setVisibility(View.GONE);
+ }
+ if (!visualizerAvailable) {
+ visualizerButton.setVisibility(View.GONE);
+ } else {
+ visualizerView = new VisualizerView(this);
+ visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT));
+
+ visualizerView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ visualizerView.setActive(!visualizerView.isActive());
+ getDownloadService().setShowVisualization(visualizerView.isActive());
+ updateButtons();
+ return true;
+ }
+ });
+ }
+
+ // TODO: Extract to utility method and cache.
+ Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/Storopia.ttf");
+ equalizerButton.setTypeface(typeface);
+ visualizerButton.setTypeface(typeface);
+ jukeboxButton.setTypeface(typeface);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null || downloadService.getCurrentPlaying() == null) {
+ playlistFlipper.setDisplayedChild(1);
+ buttonBarFlipper.setDisplayedChild(1);
+ }
+
+ onDownloadListChanged();
+ onCurrentChanged();
+ onProgressChanged();
+ scrollToCurrent();
+ if (downloadService != null && downloadService.getKeepScreenOn()) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ if (visualizerView != null) {
+ visualizerView.setActive(downloadService != null && downloadService.getShowVisualization());
+ }
+
+ updateButtons();
+ }
+
+ private void updateButtons() {
+ boolean eqEnabled = getDownloadService() != null && getDownloadService().getEqualizerController() != null &&
+ getDownloadService().getEqualizerController().isEnabled();
+ equalizerButton.setTextColor(eqEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+
+ if (visualizerView != null) {
+ visualizerButton.setTextColor(visualizerView.isActive() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ boolean jukeboxEnabled = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+ jukeboxButton.setTextColor(jukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ // Scroll to current playing/downloading.
+ private void scrollToCurrent() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ for (int i = 0; i < playlistView.getAdapter().getCount(); i++) {
+ if (currentPlaying == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ DownloadFile currentDownloading = getDownloadService().getCurrentDownloading();
+ for (int i = 0; i < playlistView.getAdapter().getCount(); i++) {
+ if (currentDownloading == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ if (visualizerView != null) {
+ visualizerView.setActive(false);
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ if (id == DIALOG_SAVE_PLAYLIST) {
+ AlertDialog.Builder builder;
+
+ LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
+ final View layout = inflater.inflate(R.layout.save_playlist, (ViewGroup) findViewById(R.id.save_playlist_root));
+ playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name);
+
+ builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.download_playlist_title);
+ builder.setMessage(R.string.download_playlist_name);
+ builder.setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ savePlaylistInBackground(String.valueOf(playlistNameView.getText()));
+ }
+ });
+ builder.setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ builder.setView(layout);
+ builder.setCancelable(true);
+
+ return builder.create();
+ } else {
+ return super.onCreateDialog(id);
+ }
+ }
+
+ @Override
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ if (id == DIALOG_SAVE_PLAYLIST) {
+ String playlistName = getDownloadService().getSuggestedPlaylistName();
+ if (playlistName != null) {
+ playlistNameView.setText(playlistName);
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.nowplaying, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem savePlaylist = menu.findItem(R.id.menu_save_playlist);
+ boolean savePlaylistEnabled = !Util.isOffline(this);
+ savePlaylist.setEnabled(savePlaylistEnabled);
+ savePlaylist.setVisible(savePlaylistEnabled);
+ MenuItem screenOption = menu.findItem(R.id.menu_screen_on_off);
+ if (getDownloadService().getKeepScreenOn()) {
+ screenOption.setTitle(R.string.download_menu_screen_off);
+ } else {
+ screenOption.setTitle(R.string.download_menu_screen_on);
+ }
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if (view == playlistView) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.nowplaying_context, menu);
+
+ if (downloadFile.getSong().getParent() == null) {
+ menu.findItem(R.id.menu_show_album).setVisible(false);
+ }
+ if (Util.isOffline(this)) {
+ menu.findItem(R.id.menu_lyrics).setVisible(false);
+ menu.findItem(R.id.menu_save_playlist).setVisible(false);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+ return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ return menuItemSelected(menuItem.getItemId(), null) || super.onOptionsItemSelected(menuItem);
+ }
+
+ private boolean menuItemSelected(int menuItemId, DownloadFile song) {
+ switch (menuItemId) {
+ case R.id.menu_show_album:
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, song.getSong().getParent());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, song.getSong().getAlbum());
+ Util.startActivityWithoutTransition(this, intent);
+ return true;
+ case R.id.menu_lyrics:
+ intent = new Intent(this, LyricsActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, song.getSong().getArtist());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_TITLE, song.getSong().getTitle());
+ Util.startActivityWithoutTransition(this, intent);
+ return true;
+ case R.id.menu_remove:
+ getDownloadService().remove(song);
+ onDownloadListChanged();
+ return true;
+ case R.id.menu_remove_all:
+ getDownloadService().setShufflePlayEnabled(false);
+ getDownloadService().clear();
+ onDownloadListChanged();
+ return true;
+ case R.id.menu_screen_on_off:
+ if (getDownloadService().getKeepScreenOn()) {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(false);
+ } else {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(true);
+ }
+ return true;
+ case R.id.menu_shuffle:
+ getDownloadService().shuffle();
+ Util.toast(this, R.string.download_menu_shuffle_notification);
+ return true;
+ case R.id.menu_save_playlist:
+ showDialog(DIALOG_SAVE_PLAYLIST);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void update() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) {
+ onDownloadListChanged();
+ }
+
+ if (currentPlaying != getDownloadService().getCurrentPlaying()) {
+ onCurrentChanged();
+ }
+
+ onProgressChanged();
+ }
+
+ private void savePlaylistInBackground(final String playlistName) {
+ Util.toast(DownloadActivity.this, getResources().getString(R.string.download_playlist_saving, playlistName));
+ getDownloadService().setSuggestedPlaylistName(playlistName);
+ new SilentBackgroundTask(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ List entries = new LinkedList();
+ for (DownloadFile downloadFile : getDownloadService().getDownloads()) {
+ entries.add(downloadFile.getSong());
+ }
+ MusicService musicService = MusicServiceFactory.getMusicService(DownloadActivity.this);
+ musicService.createPlaylist(null, playlistName, entries, DownloadActivity.this, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(DownloadActivity.this, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg = getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ Util.toast(DownloadActivity.this, msg);
+ }
+ }.execute();
+ }
+
+ private void toggleFullscreenAlbumArt() {
+ scrollToCurrent();
+ if (playlistFlipper.getDisplayedChild() == 1) {
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out));
+ playlistFlipper.setDisplayedChild(0);
+ buttonBarFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in));
+ buttonBarFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out));
+ buttonBarFlipper.setDisplayedChild(0);
+
+
+ } else {
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out));
+ playlistFlipper.setDisplayedChild(1);
+ buttonBarFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in));
+ buttonBarFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out));
+ buttonBarFlipper.setDisplayedChild(1);
+ }
+ }
+
+ private void start() {
+ DownloadService service = getDownloadService();
+ PlayerState state = service.getPlayerState();
+ if (state == PAUSED || state == COMPLETED) {
+ service.start();
+ } else if (state == STOPPED || state == IDLE) {
+ warnIfNetworkOrStorageUnavailable();
+ int current = service.getCurrentPlayingIndex();
+ // TODO: Use play() method.
+ if (current == -1) {
+ service.play(0);
+ } else {
+ service.play(current);
+ }
+ }
+ }
+
+ private void onDownloadListChanged() {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ List list = downloadService.getDownloads();
+
+ playlistView.setAdapter(new SongListAdapter(list));
+ emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
+ currentRevision = downloadService.getDownloadListUpdateRevision();
+
+ switch (downloadService.getRepeatMode()) {
+ case OFF:
+ repeatButton.setImageResource(R.drawable.media_repeat_off);
+ break;
+ case ALL:
+ repeatButton.setImageResource(R.drawable.media_repeat_all);
+ break;
+ case SINGLE:
+ repeatButton.setImageResource(R.drawable.media_repeat_single);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void onCurrentChanged() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ currentPlaying = getDownloadService().getCurrentPlaying();
+ if (currentPlaying != null) {
+ MusicDirectory.Entry song = currentPlaying.getSong();
+ songTitleTextView.setText(song.getTitle());
+ albumTextView.setText(song.getAlbum());
+ artistTextView.setText(song.getArtist());
+ getImageLoader().loadImage(albumArtImageView, song, true, true);
+ } else {
+ songTitleTextView.setText(null);
+ albumTextView.setText(null);
+ artistTextView.setText(null);
+ getImageLoader().loadImage(albumArtImageView, null, true, false);
+ }
+ }
+
+ private void onProgressChanged() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ if (currentPlaying != null) {
+
+ int millisPlayed = Math.max(0, getDownloadService().getPlayerPosition());
+ Integer duration = getDownloadService().getPlayerDuration();
+ int millisTotal = duration == null ? 0 : duration;
+
+ positionTextView.setText(Util.formatDuration(millisPlayed / 1000));
+ durationTextView.setText(Util.formatDuration(millisTotal / 1000));
+ progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug.
+ progressBar.setProgress(millisPlayed);
+ progressBar.setSlidingEnabled(currentPlaying.isCompleteFileAvailable() || getDownloadService().isJukeboxEnabled());
+ } else {
+ positionTextView.setText("0:00");
+ durationTextView.setText("-:--");
+ progressBar.setProgress(0);
+ progressBar.setSlidingEnabled(false);
+ }
+
+ PlayerState playerState = getDownloadService().getPlayerState();
+
+ switch (playerState) {
+ case DOWNLOADING:
+ long bytes = currentPlaying.getPartialFile().length();
+ statusTextView.setText(getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, this)));
+ break;
+ case PREPARING:
+ statusTextView.setText(R.string.download_playerstate_buffering);
+ break;
+ case STARTED:
+ if (getDownloadService().isShufflePlayEnabled()) {
+ statusTextView.setText(R.string.download_playerstate_playing_shuffle);
+ } else {
+ statusTextView.setText(null);
+ }
+ break;
+ default:
+ statusTextView.setText(null);
+ break;
+ }
+
+ switch (playerState) {
+ case STARTED:
+ pauseButton.setVisibility(View.VISIBLE);
+ stopButton.setVisibility(View.GONE);
+ startButton.setVisibility(View.GONE);
+ break;
+ case DOWNLOADING:
+ case PREPARING:
+ pauseButton.setVisibility(View.GONE);
+ stopButton.setVisibility(View.VISIBLE);
+ startButton.setVisibility(View.GONE);
+ break;
+ default:
+ pauseButton.setVisibility(View.GONE);
+ stopButton.setVisibility(View.GONE);
+ startButton.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ jukeboxButton.setTextColor(getDownloadService().isJukeboxEnabled() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ private class SongListAdapter extends ArrayAdapter {
+ public SongListAdapter(List entries) {
+ super(DownloadActivity.this, android.R.layout.simple_list_item_1, entries);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(DownloadActivity.this);
+ }
+ DownloadFile downloadFile = getItem(position);
+ view.setSong(downloadFile.getSong(), false);
+ return view;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ return gestureScanner.onTouchEvent(me);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent me) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return false;
+ }
+
+ // Right to Left swipe
+ if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) {
+ downloadService.next();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ return true;
+ }
+
+ // Left to Right swipe
+ if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.previous();
+ onCurrentChanged();
+ onProgressChanged();
+ return true;
+ }
+
+ // Top to Bottom swipe
+ if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.seekTo(downloadService.getPlayerPosition() + 30000);
+ onProgressChanged();
+ return true;
+ }
+
+ // Bottom to Top swipe
+ if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.seekTo(downloadService.getPlayerPosition() - 8000);
+ onProgressChanged();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/EqualizerActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/EqualizerActivity.java
new file mode 100644
index 00000000..4b706984
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/EqualizerActivity.java
@@ -0,0 +1,181 @@
+/*
+ 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 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.activity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.app.Activity;
+import android.media.audiofx.Equalizer;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.audiofx.EqualizerController;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+
+/**
+ * Equalizer controls.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class EqualizerActivity extends Activity {
+
+ private static final int MENU_GROUP_PRESET = 100;
+
+ private final Map bars = new HashMap();
+ private EqualizerController equalizerController;
+ private Equalizer equalizer;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.equalizer);
+ equalizerController = DownloadServiceImpl.getInstance().getEqualizerController();
+ equalizer = equalizerController.getEqualizer();
+
+ initEqualizer();
+
+ final View presetButton = findViewById(R.id.equalizer_preset);
+ registerForContextMenu(presetButton);
+ presetButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ presetButton.showContextMenu();
+ }
+ });
+
+ CheckBox enabledCheckBox = (CheckBox) findViewById(R.id.equalizer_enabled);
+ enabledCheckBox.setChecked(equalizer.getEnabled());
+ enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ setEqualizerEnabled(b);
+ }
+ });
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ equalizerController.saveSettings();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ short currentPreset;
+ try {
+ currentPreset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ currentPreset = -1;
+ }
+
+ for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++) {
+ MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
+ if (preset == currentPreset) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ short preset = (short) menuItem.getItemId();
+ equalizer.usePreset(preset);
+ updateBars();
+ return true;
+ }
+
+ private void setEqualizerEnabled(boolean enabled) {
+ equalizer.setEnabled(enabled);
+ updateBars();
+ }
+
+ private void updateBars() {
+
+ for (Map.Entry entry : bars.entrySet()) {
+ short band = entry.getKey();
+ SeekBar bar = entry.getValue();
+ bar.setEnabled(equalizer.getEnabled());
+ short minEQLevel = equalizer.getBandLevelRange()[0];
+ bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
+ }
+ }
+
+ private void initEqualizer() {
+ LinearLayout layout = (LinearLayout) findViewById(R.id.equalizer_layout);
+
+ final short minEQLevel = equalizer.getBandLevelRange()[0];
+ final short maxEQLevel = equalizer.getBandLevelRange()[1];
+
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ final short band = i;
+
+ View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null);
+ TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
+ final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
+ SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
+
+ freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz");
+
+ bars.put(band, bar);
+ bar.setMax(maxEQLevel - minEQLevel);
+ short level = equalizer.getBandLevel(band);
+ bar.setProgress(level - minEQLevel);
+ bar.setEnabled(equalizer.getEnabled());
+ updateLevelText(levelTextView, level);
+
+ bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ short level = (short) (progress + minEQLevel);
+ if (fromUser) {
+ equalizer.setBandLevel(band, level);
+ }
+ updateLevelText(levelTextView, level);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ layout.addView(bandBar);
+ }
+ }
+
+ private void updateLevelText(TextView levelTextView, short level) {
+ levelTextView.setText((level > 0 ? "+" : "") + level / 100 + " dB");
+ }
+
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/HelpActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/HelpActivity.java
new file mode 100644
index 00000000..ea382887
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/HelpActivity.java
@@ -0,0 +1,117 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.Button;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.util.Util;
+
+/**
+ * An HTML-based help screen with Back and Done buttons at the bottom.
+ *
+ * @author Sindre Mehus
+ */
+public final class HelpActivity extends Activity {
+
+ private WebView webView;
+ private Button backButton;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ setContentView(R.layout.help);
+
+ webView = (WebView) findViewById(R.id.help_contents);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.setWebViewClient(new HelpClient());
+ if (bundle != null) {
+ webView.restoreState(bundle);
+ } else {
+ webView.loadUrl(getResources().getString(R.string.help_url));
+ }
+
+ backButton = (Button) findViewById(R.id.help_back);
+ backButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ webView.goBack();
+ }
+ });
+
+ Button doneButton = (Button) findViewById(R.id.help_close);
+ doneButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ webView.saveState(state);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (webView.canGoBack()) {
+ webView.goBack();
+ return true;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private final class HelpClient extends WebViewClient {
+ @Override
+ public void onLoadResource(WebView webView, String url) {
+ setProgressBarIndeterminateVisibility(true);
+ setTitle(getResources().getString(R.string.help_loading));
+ super.onLoadResource(webView, url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ setProgressBarIndeterminateVisibility(false);
+ setTitle(view.getTitle());
+ backButton.setEnabled(view.canGoBack());
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Util.toast(HelpActivity.this, description);
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/LyricsActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/LyricsActivity.java
new file mode 100644
index 00000000..10c5ee59
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/LyricsActivity.java
@@ -0,0 +1,72 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.os.Bundle;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.Lyrics;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.BackgroundTask;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.TabActivityBackgroundTask;
+
+/**
+ * Displays song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public final class LyricsActivity extends SubsonicTabActivity {
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.lyrics);
+ load();
+ }
+
+ private void load() {
+ BackgroundTask task = new TabActivityBackgroundTask(this) {
+ @Override
+ protected Lyrics doInBackground() throws Throwable {
+ String artist = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ARTIST);
+ String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_TITLE);
+ MusicService musicService = MusicServiceFactory.getMusicService(LyricsActivity.this);
+ return musicService.getLyrics(artist, title, LyricsActivity.this, this);
+ }
+
+ @Override
+ protected void done(Lyrics result) {
+ TextView artistView = (TextView) findViewById(R.id.lyrics_artist);
+ TextView titleView = (TextView) findViewById(R.id.lyrics_title);
+ TextView textView = (TextView) findViewById(R.id.lyrics_text);
+ if (result != null && result.getArtist() != null) {
+ artistView.setText(result.getArtist());
+ titleView.setText(result.getTitle());
+ textView.setText(result.getText());
+ } else {
+ artistView.setText(R.string.lyrics_nomatch);
+ }
+ }
+ };
+ task.execute();
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/MainActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/MainActivity.java
new file mode 100644
index 00000000..7ad621ef
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/MainActivity.java
@@ -0,0 +1,258 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import java.util.Arrays;
+
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.MergeAdapter;
+import github.daneren2005.subphonic.util.Util;
+import github.daneren2005.subphonic.util.FileUtil;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class MainActivity extends SubsonicTabActivity {
+
+ private static final int MENU_GROUP_SERVER = 10;
+ private static final int MENU_ITEM_SERVER_1 = 101;
+ private static final int MENU_ITEM_SERVER_2 = 102;
+ private static final int MENU_ITEM_SERVER_3 = 103;
+ private static final int MENU_ITEM_OFFLINE = 104;
+
+ private String theme;
+
+ private static boolean infoDialogDisplayed;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) {
+ exit();
+ }
+ setContentView(R.layout.main);
+
+ loadSettings();
+
+ View buttons = LayoutInflater.from(this).inflate(R.layout.main_buttons, null);
+
+ final View serverButton = buttons.findViewById(R.id.main_select_server);
+ final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2);
+
+ final View albumsTitle = buttons.findViewById(R.id.main_albums);
+ final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
+ final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
+ final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
+ final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
+ final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
+
+ final View dummyView = findViewById(R.id.main_dummy);
+
+ int instance = Util.getActiveServer(this);
+ String name = Util.getServerName(this, instance);
+ serverTextView.setText(name);
+
+ ListView list = (ListView) findViewById(R.id.main_list);
+
+ MergeAdapter adapter = new MergeAdapter();
+ adapter.addViews(Arrays.asList(serverButton), true);
+ if (!Util.isOffline(this)) {
+ adapter.addView(albumsTitle, false);
+ adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton, albumsHighestButton, albumsRecentButton, albumsFrequentButton), true);
+ }
+ list.setAdapter(adapter);
+ registerForContextMenu(dummyView);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (view == serverButton) {
+ dummyView.showContextMenu();
+ } else if (view == albumsNewestButton) {
+ showAlbumList("newest");
+ } else if (view == albumsRandomButton) {
+ showAlbumList("random");
+ } else if (view == albumsHighestButton) {
+ showAlbumList("highest");
+ } else if (view == albumsRecentButton) {
+ showAlbumList("recent");
+ } else if (view == albumsFrequentButton) {
+ showAlbumList("frequent");
+ }
+ }
+ });
+
+ // Title: Subsonic
+ setTitle(R.string.common_appname);
+
+ // Button 1: shuffle
+ ImageButton actionShuffleButton = (ImageButton)findViewById(R.id.action_button_1);
+ actionShuffleButton.setImageResource(R.drawable.action_shuffle);
+ actionShuffleButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(MainActivity.this, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(MainActivity.this, intent);
+ }
+ });
+
+ // Button 2: search
+ ImageButton actionSearchButton = (ImageButton)findViewById(R.id.action_button_2);
+ actionSearchButton.setImageResource(R.drawable.action_search);
+ actionSearchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(MainActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
+ Util.startActivityWithoutTransition(MainActivity.this, intent);
+ }
+ });
+
+ // Remember the current theme.
+ theme = Util.getTheme(this);
+
+ showInfoDialog();
+ }
+
+ private void loadSettings() {
+ PreferenceManager.setDefaultValues(this, R.xml.settings, false);
+ SharedPreferences prefs = Util.getPreferences(this);
+ if (!prefs.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath());
+ editor.commit();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Restart activity if theme has changed.
+ if (theme != null && !theme.equals(Util.getTheme(this))) {
+ restart();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuItem menuItem1 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_1, MENU_ITEM_SERVER_1, Util.getServerName(this, 1));
+ MenuItem menuItem2 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_2, MENU_ITEM_SERVER_2, Util.getServerName(this, 2));
+ MenuItem menuItem3 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_3, MENU_ITEM_SERVER_3, Util.getServerName(this, 3));
+ MenuItem menuItem4 = menu.add(MENU_GROUP_SERVER, MENU_ITEM_OFFLINE, MENU_ITEM_OFFLINE, Util.getServerName(this, 0));
+ menu.setGroupCheckable(MENU_GROUP_SERVER, true, true);
+ menu.setHeaderTitle(R.string.main_select_server);
+
+ switch (Util.getActiveServer(this)) {
+ case 0:
+ menuItem4.setChecked(true);
+ break;
+ case 1:
+ menuItem1.setChecked(true);
+ break;
+ case 2:
+ menuItem2.setChecked(true);
+ break;
+ case 3:
+ menuItem3.setChecked(true);
+ break;
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case MENU_ITEM_OFFLINE:
+ setActiveServer(0);
+ break;
+ case MENU_ITEM_SERVER_1:
+ setActiveServer(1);
+ break;
+ case MENU_ITEM_SERVER_2:
+ setActiveServer(2);
+ break;
+ case MENU_ITEM_SERVER_3:
+ setActiveServer(3);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+
+ // Restart activity
+ restart();
+ return true;
+ }
+
+ private void setActiveServer(int instance) {
+ if (Util.getActiveServer(this) != instance) {
+ DownloadService service = getDownloadService();
+ if (service != null) {
+ service.clearIncomplete();
+ }
+ Util.setActiveServer(this, instance);
+ }
+ }
+
+ private void restart() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void exit() {
+ stopService(new Intent(this, DownloadServiceImpl.class));
+ finish();
+ }
+
+ private void showInfoDialog() {
+ if (!infoDialogDisplayed) {
+ infoDialogDisplayed = true;
+ if (Util.getRestUrl(this, null).contains("demo.subsonic.org")) {
+ Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text);
+ }
+ }
+ }
+
+ private void showAlbumList(String type) {
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/PlayVideoActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/PlayVideoActivity.java
new file mode 100644
index 00000000..a393d3a8
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/PlayVideoActivity.java
@@ -0,0 +1,147 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import java.lang.reflect.Method;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Window;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.Util;
+
+/**
+ * Plays videos in a web page.
+ *
+ * @author Sindre Mehus
+ */
+public final class PlayVideoActivity extends Activity {
+
+ private static final String TAG = PlayVideoActivity.class.getSimpleName();
+ private WebView webView;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ setContentView(R.layout.play_video);
+
+ webView = (WebView) findViewById(R.id.play_video_contents);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.getSettings().setPluginsEnabled(true);
+ webView.getSettings().setAllowFileAccess(true);
+ webView.getSettings().setSupportZoom(true);
+ webView.getSettings().setBuiltInZoomControls(true);
+
+ webView.setWebViewClient(new Client());
+ if (bundle != null) {
+ webView.restoreState(bundle);
+ } else {
+ webView.loadUrl(getVideoUrl());
+ }
+
+ // Show warning if Flash plugin is not installed.
+ if (isFlashPluginInstalled()) {
+ Util.toast(this, R.string.play_video_loading, false);
+ } else {
+ Util.toast(this, R.string.play_video_noplugin, false);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ callHiddenWebViewMethod("onPause");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ callHiddenWebViewMethod("onResume");
+ }
+
+ private String getVideoUrl() {
+ String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
+ return MusicServiceFactory.getMusicService(this).getVideoUrl(this, id);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ webView.saveState(state);
+ }
+
+ private void callHiddenWebViewMethod(String name){
+ if( webView != null ){
+ try {
+ Method method = WebView.class.getMethod(name);
+ method.invoke(webView);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to invoke " + name, x);
+ }
+ }
+ }
+
+ private boolean isFlashPluginInstalled() {
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo("com.adobe.flashplayer", 0);
+ return packageInfo != null;
+ } catch (PackageManager.NameNotFoundException x) {
+ return false;
+ }
+ }
+
+ private final class Client extends WebViewClient {
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Util.toast(PlayVideoActivity.this, description);
+ Log.e(TAG, "Error: " + description);
+ }
+
+ @Override
+ public void onLoadResource(WebView view, String url) {
+ super.onLoadResource(view, url);
+ Log.d(TAG, "onLoadResource: " + url);
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ Log.d(TAG, "onPageStarted: " + url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ Log.d(TAG, "onPageFinished: " + url);
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/QueryReceiverActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/QueryReceiverActivity.java
new file mode 100644
index 00000000..cabae1ce
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/QueryReceiverActivity.java
@@ -0,0 +1,56 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.Util;
+import github.daneren2005.subphonic.provider.SearchSuggestionProvider1;
+
+/**
+ * Receives search queries and forwards to the SelectAlbumActivity.
+ *
+ * @author Sindre Mehus
+ */
+public class QueryReceiverActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider1.AUTHORITY,
+ SearchSuggestionProvider1.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ Intent intent = new Intent(QueryReceiverActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ Util.startActivityWithoutTransition(QueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/SearchActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/SearchActivity.java
new file mode 100644
index 00000000..a9d64fda
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/SearchActivity.java
@@ -0,0 +1,368 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.MenuItem;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.net.Uri;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.Artist;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.SearchCritera;
+import github.daneren2005.subphonic.domain.SearchResult;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.util.ArtistAdapter;
+import github.daneren2005.subphonic.util.BackgroundTask;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.EntryAdapter;
+import github.daneren2005.subphonic.util.MergeAdapter;
+import github.daneren2005.subphonic.util.TabActivityBackgroundTask;
+import github.daneren2005.subphonic.util.Util;
+
+/**
+ * Performs searches and displays the matching artists, albums and songs.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchActivity extends SubsonicTabActivity {
+
+ private static final int DEFAULT_ARTISTS = 3;
+ private static final int DEFAULT_ALBUMS = 5;
+ private static final int DEFAULT_SONGS = 10;
+
+ private static final int MAX_ARTISTS = 10;
+ private static final int MAX_ALBUMS = 20;
+ private static final int MAX_SONGS = 25;
+ private ListView list;
+
+ private View artistsHeading;
+ private View albumsHeading;
+ private View songsHeading;
+ private TextView searchButton;
+ private View moreArtistsButton;
+ private View moreAlbumsButton;
+ private View moreSongsButton;
+ private SearchResult searchResult;
+ private MergeAdapter mergeAdapter;
+ private ArtistAdapter artistAdapter;
+ private ListAdapter moreArtistsAdapter;
+ private EntryAdapter albumAdapter;
+ private ListAdapter moreAlbumsAdapter;
+ private ListAdapter moreSongsAdapter;
+ private EntryAdapter songAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.search);
+
+ setTitle(R.string.search_title);
+
+ View buttons = LayoutInflater.from(this).inflate(R.layout.search_buttons, null);
+
+ artistsHeading = buttons.findViewById(R.id.search_artists);
+ albumsHeading = buttons.findViewById(R.id.search_albums);
+ songsHeading = buttons.findViewById(R.id.search_songs);
+
+ searchButton = (TextView) buttons.findViewById(R.id.search_search);
+ moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
+ moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
+ moreSongsButton = buttons.findViewById(R.id.search_more_songs);
+
+ list = (ListView) findViewById(R.id.search_list);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (view == searchButton) {
+ onSearchRequested();
+ } else if (view == moreArtistsButton) {
+ expandArtists();
+ } else if (view == moreAlbumsButton) {
+ expandAlbums();
+ } else if (view == moreSongsButton) {
+ expandSongs();
+ } else {
+ Object item = parent.getItemAtPosition(position);
+ if (item instanceof Artist) {
+ onArtistSelected((Artist) item);
+ } else if (item instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
+ if (entry.isDirectory()) {
+ onAlbumSelected(entry, false);
+ } else if (entry.isVideo()) {
+ onVideoSelected(entry);
+ } else {
+ onSongSelected(entry, false, true, true, false);
+ }
+
+ }
+ }
+ }
+ });
+ registerForContextMenu(list);
+
+ // Button 1: gone
+ findViewById(R.id.action_button_1).setVisibility(View.GONE);
+
+ // Button 2: search
+ final ImageButton actionSearchButton = (ImageButton)findViewById(R.id.action_button_2);
+ actionSearchButton.setImageResource(R.drawable.action_search);
+ actionSearchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onSearchRequested();
+ }
+ });
+
+ onNewIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY);
+ boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false);
+
+ if (query != null) {
+ mergeAdapter = new MergeAdapter();
+ list.setAdapter(mergeAdapter);
+ search(query, autoplay);
+ } else {
+ populateList();
+ if (requestsearch)
+ onSearchRequested();
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ boolean isArtist = selectedItem instanceof Artist;
+ boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
+ boolean isSong = selectedItem instanceof MusicDirectory.Entry && (!((MusicDirectory.Entry) selectedItem).isDirectory())
+ && (!((MusicDirectory.Entry) selectedItem).isVideo());
+
+ if (isArtist || isAlbum) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_album_context, menu);
+ } else if (isSong) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_song_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
+ MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
+ String id = artist != null ? artist.getId() : entry.getId();
+
+ switch (menuItem.getItemId()) {
+ case R.id.album_menu_play_now:
+ downloadRecursively(id, false, false, true);
+ break;
+ case R.id.album_menu_play_last:
+ downloadRecursively(id, false, true, false);
+ break;
+ case R.id.album_menu_pin:
+ downloadRecursively(id, true, true, false);
+ break;
+ case R.id.song_menu_play_now:
+ onSongSelected(entry, false, false, true, false);
+ break;
+ case R.id.song_menu_play_next:
+ onSongSelected(entry, false, true, false, true);
+ break;
+ case R.id.song_menu_play_last:
+ onSongSelected(entry, false, true, false, false);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+
+ return true;
+ }
+
+ private void search(final String query, final boolean autoplay) {
+ BackgroundTask task = new TabActivityBackgroundTask(this) {
+ @Override
+ protected SearchResult doInBackground() throws Throwable {
+ SearchCritera criteria = new SearchCritera(query, MAX_ARTISTS, MAX_ALBUMS, MAX_SONGS);
+ MusicService service = MusicServiceFactory.getMusicService(SearchActivity.this);
+ return service.search(criteria, SearchActivity.this, this);
+ }
+
+ @Override
+ protected void done(SearchResult result) {
+ searchResult = result;
+ populateList();
+ if (autoplay) {
+ autoplay();
+ }
+
+ }
+ };
+ task.execute();
+ }
+
+ private void populateList() {
+ mergeAdapter = new MergeAdapter();
+ mergeAdapter.addView(searchButton, true);
+
+ if (searchResult != null) {
+ List artists = searchResult.getArtists();
+ if (!artists.isEmpty()) {
+ mergeAdapter.addView(artistsHeading);
+ List displayedArtists = new ArrayList(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
+ artistAdapter = new ArtistAdapter(this, displayedArtists);
+ mergeAdapter.addAdapter(artistAdapter);
+ if (artists.size() > DEFAULT_ARTISTS) {
+ moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
+ }
+ }
+
+ List albums = searchResult.getAlbums();
+ if (!albums.isEmpty()) {
+ mergeAdapter.addView(albumsHeading);
+ List displayedAlbums = new ArrayList(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
+ albumAdapter = new EntryAdapter(this, getImageLoader(), displayedAlbums, false);
+ mergeAdapter.addAdapter(albumAdapter);
+ if (albums.size() > DEFAULT_ALBUMS) {
+ moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
+ }
+ }
+
+ List songs = searchResult.getSongs();
+ if (!songs.isEmpty()) {
+ mergeAdapter.addView(songsHeading);
+ List displayedSongs = new ArrayList(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
+ songAdapter = new EntryAdapter(this, getImageLoader(), displayedSongs, false);
+ mergeAdapter.addAdapter(songAdapter);
+ if (songs.size() > DEFAULT_SONGS) {
+ moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
+ }
+ }
+
+ boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
+ searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
+ }
+
+ list.setAdapter(mergeAdapter);
+ }
+
+ private void expandArtists() {
+ artistAdapter.clear();
+ for (Artist artist : searchResult.getArtists()) {
+ artistAdapter.add(artist);
+ }
+ artistAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreArtistsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandAlbums() {
+ albumAdapter.clear();
+ for (MusicDirectory.Entry album : searchResult.getAlbums()) {
+ albumAdapter.add(album);
+ }
+ albumAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreAlbumsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandSongs() {
+ songAdapter.clear();
+ for (MusicDirectory.Entry song : searchResult.getSongs()) {
+ songAdapter.add(song);
+ }
+ songAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreSongsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void onArtistSelected(Artist artist) {
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) {
+ Intent intent = new Intent(SearchActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, album.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
+ Util.startActivityWithoutTransition(SearchActivity.this, intent);
+ }
+
+ private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ downloadService.download(Arrays.asList(song), save, false, playNext);
+ if (autoplay) {
+ downloadService.play(downloadService.size() - 1);
+ }
+
+ Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
+ }
+ }
+
+ private void onVideoSelected(MusicDirectory.Entry entry) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(this, entry.getId())));
+ startActivity(intent);
+ }
+
+ private void autoplay() {
+ if (!searchResult.getSongs().isEmpty()) {
+ onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
+ } else if (!searchResult.getAlbums().isEmpty()) {
+ onAlbumSelected(searchResult.getAlbums().get(0), true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/SelectAlbumActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/SelectAlbumActivity.java
new file mode 100644
index 00000000..1fd3fa71
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/SelectAlbumActivity.java
@@ -0,0 +1,568 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.service.DownloadFile;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.EntryAdapter;
+import github.daneren2005.subphonic.util.Pair;
+import github.daneren2005.subphonic.util.TabActivityBackgroundTask;
+import github.daneren2005.subphonic.util.Util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectAlbumActivity extends SubsonicTabActivity {
+
+ private static final String TAG = SelectAlbumActivity.class.getSimpleName();
+
+ private ListView entryList;
+ private View footer;
+ private View emptyView;
+ private Button selectButton;
+ private Button playNowButton;
+ private Button playLastButton;
+ private Button pinButton;
+ private Button unpinButton;
+ private Button deleteButton;
+ private Button moreButton;
+ private ImageView coverArtView;
+ private boolean licenseValid;
+ private ImageButton playAllButton;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_album);
+
+ entryList = (ListView) findViewById(R.id.select_album_entries);
+
+ footer = LayoutInflater.from(this).inflate(R.layout.select_album_footer, entryList, false);
+ entryList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ entryList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (position >= 0) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
+ if (entry.isDirectory()) {
+ Intent intent = new Intent(SelectAlbumActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, entry.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle());
+ Util.startActivityWithoutTransition(SelectAlbumActivity.this, intent);
+ } else if (entry.isVideo()) {
+ playVideo(entry);
+ } else {
+ enableButtons();
+ }
+ }
+ }
+ });
+
+ coverArtView = (ImageView) findViewById(R.id.actionbar_home_icon);
+ selectButton = (Button) findViewById(R.id.select_album_select);
+ playNowButton = (Button) findViewById(R.id.select_album_play_now);
+ playLastButton = (Button) findViewById(R.id.select_album_play_last);
+ pinButton = (Button) footer.findViewById(R.id.select_album_pin);
+ unpinButton = (Button) footer.findViewById(R.id.select_album_unpin);
+ unpinButton = (Button) footer.findViewById(R.id.select_album_unpin);
+ deleteButton = (Button) footer.findViewById(R.id.select_album_delete);
+ moreButton = (Button) footer.findViewById(R.id.select_album_more);
+ emptyView = findViewById(R.id.select_album_empty);
+
+ selectButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ selectAllOrNone();
+ }
+ });
+ playNowButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ download(false, false, true, false);
+ selectAll(false, false);
+ }
+ });
+ playLastButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ download(true, false, false, false);
+ selectAll(false, false);
+ }
+ });
+ pinButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ download(true, true, false, false);
+ selectAll(false, false);
+ }
+ });
+ unpinButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ unpin();
+ selectAll(false, false);
+ }
+ });
+ deleteButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ delete();
+ selectAll(false, false);
+ }
+ });
+
+ registerForContextMenu(entryList);
+
+ enableButtons();
+
+ String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
+ String name = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_NAME);
+ String playlistId = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ String albumListType = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ int albumListSize = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ int albumListOffset = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+
+ if (playlistId != null) {
+ getPlaylist(playlistId, playlistName);
+ } else if (albumListType != null) {
+ getAlbumList(albumListType, albumListSize, albumListOffset);
+ } else {
+ getMusicDirectory(id, name);
+ }
+
+ // Button 1: play all
+ playAllButton = (ImageButton) findViewById(R.id.action_button_1);
+ playAllButton.setImageResource(R.drawable.action_play_all);
+ playAllButton.setVisibility(View.GONE);
+ playAllButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ playAll();
+ }
+ });
+
+ // Button 2: refresh
+ ImageButton refreshButton = (ImageButton) findViewById(R.id.action_button_2);
+ refreshButton.setImageResource(R.drawable.action_refresh);
+ refreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ refresh();
+ }
+ });
+ }
+
+ private void playAll() {
+ boolean hasSubFolders = false;
+ for (int i = 0; i < entryList.getCount(); i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && entry.isDirectory()) {
+ hasSubFolders = true;
+ break;
+ }
+ }
+
+ String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
+ if (hasSubFolders && id != null) {
+ downloadRecursively(id, false, false, true);
+ } else {
+ selectAll(true, false);
+ download(false, false, true, false);
+ selectAll(false, false);
+ }
+ }
+
+ private void refresh() {
+ finish();
+ Intent intent = getIntent();
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ AdapterView.AdapterContextMenuInfo info =
+ (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(info.position);
+
+ if (entry.isDirectory()) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_album_context, menu);
+ } else {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_song_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(info.position);
+ List songs = new ArrayList(10);
+ songs.add((MusicDirectory.Entry) entryList.getItemAtPosition(info.position));
+ switch (menuItem.getItemId()) {
+ case R.id.album_menu_play_now:
+ downloadRecursively(entry.getId(), false, false, true);
+ break;
+ case R.id.album_menu_play_last:
+ downloadRecursively(entry.getId(), false, true, false);
+ break;
+ case R.id.album_menu_pin:
+ downloadRecursively(entry.getId(), true, true, false);
+ break;
+ case R.id.song_menu_play_now:
+ getDownloadService().download(songs, false, true, true);
+ break;
+ case R.id.song_menu_play_next:
+ getDownloadService().download(songs, false, false, true);
+ break;
+ case R.id.song_menu_play_last:
+ getDownloadService().download(songs, false, false, false);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+ return true;
+ }
+
+ private void getMusicDirectory(final String id, String name) {
+ setTitle(name);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
+ return service.getMusicDirectory(id, refresh, SelectAlbumActivity.this, this);
+ }
+ }.execute();
+ }
+
+ private void getPlaylist(final String playlistId, final String playlistName) {
+ setTitle(playlistName);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPlaylist(playlistId, playlistName, SelectAlbumActivity.this, this);
+ }
+ }.execute();
+ }
+
+ private void getAlbumList(final String albumListType, final int size, final int offset) {
+
+ if ("newest".equals(albumListType)) {
+ setTitle(R.string.main_albums_newest);
+ } else if ("random".equals(albumListType)) {
+ setTitle(R.string.main_albums_random);
+ } else if ("highest".equals(albumListType)) {
+ setTitle(R.string.main_albums_highest);
+ } else if ("recent".equals(albumListType)) {
+ setTitle(R.string.main_albums_recent);
+ } else if ("frequent".equals(albumListType)) {
+ setTitle(R.string.main_albums_frequent);
+ }
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getAlbumList(albumListType, size, offset, SelectAlbumActivity.this, this);
+ }
+
+ @Override
+ protected void done(Pair result) {
+ if (!result.getFirst().getChildren().isEmpty()) {
+ pinButton.setVisibility(View.GONE);
+ unpinButton.setVisibility(View.GONE);
+ deleteButton.setVisibility(View.GONE);
+ moreButton.setVisibility(View.VISIBLE);
+ entryList.addFooterView(footer);
+
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SelectAlbumActivity.this, SelectAlbumActivity.class);
+ String type = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ int size = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ int offset = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0) + size;
+
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, size);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, offset);
+ Util.startActivityWithoutTransition(SelectAlbumActivity.this, intent);
+ }
+ });
+ }
+ super.done(result);
+ }
+ }.execute();
+ }
+
+ private void selectAllOrNone() {
+ boolean someUnselected = false;
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (!entryList.isItemChecked(i) && entryList.getItemAtPosition(i) instanceof MusicDirectory.Entry) {
+ someUnselected = true;
+ break;
+ }
+ }
+ selectAll(someUnselected, true);
+ }
+
+ private void selectAll(boolean selected, boolean toast) {
+ int count = entryList.getCount();
+ int selectedCount = 0;
+ for (int i = 0; i < count; i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && !entry.isDirectory() && !entry.isVideo()) {
+ entryList.setItemChecked(i, selected);
+ selectedCount++;
+ }
+ }
+
+ // Display toast: N tracks selected / N tracks unselected
+ if (toast) {
+ int toastResId = selected ? R.string.select_album_n_selected
+ : R.string.select_album_n_unselected;
+ Util.toast(this, getString(toastResId, selectedCount));
+ }
+
+ enableButtons();
+ }
+
+ private void enableButtons() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ List selection = getSelectedSongs();
+ boolean enabled = !selection.isEmpty();
+ boolean unpinEnabled = false;
+ boolean deleteEnabled = false;
+
+ for (MusicDirectory.Entry song : selection) {
+ DownloadFile downloadFile = getDownloadService().forSong(song);
+ if (downloadFile.isCompleteFileAvailable()) {
+ deleteEnabled = true;
+ }
+ if (downloadFile.isSaved()) {
+ unpinEnabled = true;
+ }
+ }
+
+ playNowButton.setEnabled(enabled);
+ playLastButton.setEnabled(enabled);
+ pinButton.setEnabled(enabled && !Util.isOffline(this));
+ unpinButton.setEnabled(unpinEnabled);
+ deleteButton.setEnabled(deleteEnabled);
+ }
+
+ private List getSelectedSongs() {
+ List songs = new ArrayList(10);
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ songs.add((MusicDirectory.Entry) entryList.getItemAtPosition(i));
+ }
+ }
+ return songs;
+ }
+
+ private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ final List songs = getSelectedSongs();
+ Runnable onValid = new Runnable() {
+ @Override
+ public void run() {
+ if (!append) {
+ getDownloadService().clear();
+ }
+
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().download(songs, save, autoplay, playNext);
+ String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ if (playlistName != null) {
+ getDownloadService().setSuggestedPlaylistName(playlistName);
+ }
+ if (autoplay) {
+ Util.startActivityWithoutTransition(SelectAlbumActivity.this, DownloadActivity.class);
+ } else if (save) {
+ Util.toast(SelectAlbumActivity.this,
+ getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ } else if (append) {
+ Util.toast(SelectAlbumActivity.this,
+ getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size()));
+ }
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+
+ private void delete() {
+ if (getDownloadService() != null) {
+ getDownloadService().delete(getSelectedSongs());
+ }
+ }
+
+ private void unpin() {
+ if (getDownloadService() != null) {
+ getDownloadService().unpin(getSelectedSongs());
+ }
+ }
+
+ private void playVideo(MusicDirectory.Entry entry) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(this, entry.getId())));
+
+ startActivity(intent);
+ }
+
+ private void checkLicenseAndTrialPeriod(Runnable onValid) {
+ if (licenseValid) {
+ onValid.run();
+ return;
+ }
+
+ int trialDaysLeft = Util.getRemainingTrialDays(this);
+ Log.i(TAG, trialDaysLeft + " trial days left.");
+
+ if (trialDaysLeft == 0) {
+ showDonationDialog(trialDaysLeft, null);
+ } else if (trialDaysLeft < Constants.FREE_TRIAL_DAYS / 2) {
+ showDonationDialog(trialDaysLeft, onValid);
+ } else {
+ Util.toast(this, getResources().getString(R.string.select_album_not_licensed, trialDaysLeft));
+ onValid.run();
+ }
+ }
+
+ private void showDonationDialog(int trialDaysLeft, final Runnable onValid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+
+ if (trialDaysLeft == 0) {
+ builder.setTitle(R.string.select_album_donate_dialog_0_trial_days_left);
+ } else {
+ builder.setTitle(getResources().getQuantityString(R.plurals.select_album_donate_dialog_n_trial_days_left,
+ trialDaysLeft, trialDaysLeft));
+ }
+
+ builder.setMessage(R.string.select_album_donate_dialog_message);
+
+ builder.setPositiveButton(R.string.select_album_donate_dialog_now,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DONATION_URL)));
+ }
+ });
+
+ builder.setNegativeButton(R.string.select_album_donate_dialog_later,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ if (onValid != null) {
+ onValid.run();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private abstract class LoadTask extends TabActivityBackgroundTask> {
+
+ public LoadTask() {
+ super(SelectAlbumActivity.this);
+ }
+
+ protected abstract MusicDirectory load(MusicService service) throws Exception;
+
+ @Override
+ protected Pair doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SelectAlbumActivity.this);
+ MusicDirectory dir = load(musicService);
+ boolean valid = musicService.isLicenseValid(SelectAlbumActivity.this, this);
+ return new Pair(dir, valid);
+ }
+
+ @Override
+ protected void done(Pair result) {
+ List entries = result.getFirst().getChildren();
+
+ int songCount = 0;
+ for (MusicDirectory.Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ }
+ }
+
+ if (songCount > 0) {
+ getImageLoader().loadImage(coverArtView, entries.get(0), false, true);
+ entryList.addFooterView(footer);
+ selectButton.setVisibility(View.VISIBLE);
+ playNowButton.setVisibility(View.VISIBLE);
+ playLastButton.setVisibility(View.VISIBLE);
+ }
+
+ boolean isAlbumList = getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+
+ emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
+ playAllButton.setVisibility(isAlbumList || entries.isEmpty() ? View.GONE : View.VISIBLE);
+ entryList.setAdapter(new EntryAdapter(SelectAlbumActivity.this, getImageLoader(), entries, true));
+ licenseValid = result.getSecond();
+
+ boolean playAll = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ if (playAll && songCount > 0) {
+ playAll();
+ }
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/SelectArtistActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/SelectArtistActivity.java
new file mode 100644
index 00000000..91f2c7bf
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/SelectArtistActivity.java
@@ -0,0 +1,228 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.Artist;
+import github.daneren2005.subphonic.domain.Indexes;
+import github.daneren2005.subphonic.domain.MusicFolder;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.ArtistAdapter;
+import github.daneren2005.subphonic.util.BackgroundTask;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.TabActivityBackgroundTask;
+import github.daneren2005.subphonic.util.Util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectArtistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener {
+
+ private static final int MENU_GROUP_MUSIC_FOLDER = 10;
+
+ private ListView artistList;
+ private View folderButton;
+ private TextView folderName;
+ private List musicFolders;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_artist);
+
+ artistList = (ListView) findViewById(R.id.select_artist_list);
+ artistList.setOnItemClickListener(this);
+
+ folderButton = LayoutInflater.from(this).inflate(R.layout.select_artist_header, artistList, false);
+ folderName = (TextView) folderButton.findViewById(R.id.select_artist_folder_2);
+
+ if (!Util.isOffline(this)) {
+ artistList.addHeaderView(folderButton);
+ }
+
+ registerForContextMenu(artistList);
+
+ setTitle(Util.isOffline(this) ? R.string.music_library_label_offline : R.string.music_library_label);
+
+ // Button 1: shuffle
+ ImageButton shuffleButton = (ImageButton) findViewById(R.id.action_button_1);
+ shuffleButton.setImageResource(R.drawable.action_shuffle);
+ shuffleButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SelectArtistActivity.this, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(SelectArtistActivity.this, intent);
+ }
+ });
+
+ // Button 2: refresh
+ ImageButton refreshButton = (ImageButton) findViewById(R.id.action_button_2);
+ refreshButton.setImageResource(R.drawable.action_refresh);
+ refreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ refresh();
+ }
+ });
+
+ musicFolders = null;
+ load();
+ }
+
+ private void refresh() {
+ finish();
+ Intent intent = getIntent();
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void selectFolder() {
+ folderButton.showContextMenu();
+ }
+
+ private void load() {
+ BackgroundTask task = new TabActivityBackgroundTask(this) {
+ @Override
+ protected Indexes doInBackground() throws Throwable {
+ boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
+ MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this);
+ if (!Util.isOffline(SelectArtistActivity.this)) {
+ musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this);
+ }
+ String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
+ return musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this);
+ }
+
+ @Override
+ protected void done(Indexes result) {
+ List artists = new ArrayList(result.getShortcuts().size() + result.getArtists().size());
+ artists.addAll(result.getShortcuts());
+ artists.addAll(result.getArtists());
+ artistList.setAdapter(new ArtistAdapter(SelectArtistActivity.this, artists));
+
+ // Display selected music folder
+ if (musicFolders != null) {
+ String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
+ if (musicFolderId == null) {
+ folderName.setText(R.string.select_artist_all_folders);
+ } else {
+ for (MusicFolder musicFolder : musicFolders) {
+ if (musicFolder.getId().equals(musicFolderId)) {
+ folderName.setText(musicFolder.getName());
+ break;
+ }
+ }
+ }
+ }
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (view == folderButton) {
+ selectFolder();
+ } else {
+ Artist artist = (Artist) parent.getItemAtPosition(position);
+ Intent intent = new Intent(this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ if (artistList.getItemAtPosition(info.position) instanceof Artist) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.select_artist_context, menu);
+ } else if (info.position == 0) {
+ String musicFolderId = Util.getSelectedMusicFolderId(this);
+ MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
+ if (musicFolderId == null) {
+ menuItem.setChecked(true);
+ }
+ if (musicFolders != null) {
+ for (int i = 0; i < musicFolders.size(); i++) {
+ MusicFolder musicFolder = musicFolders.get(i);
+ menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
+ if (musicFolder.getId().equals(musicFolderId)) {
+ menuItem.setChecked(true);
+ }
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+
+ Artist artist = (Artist) artistList.getItemAtPosition(info.position);
+
+ if (artist != null) {
+ switch (menuItem.getItemId()) {
+ case R.id.artist_menu_play_now:
+ downloadRecursively(artist.getId(), false, false, true);
+ break;
+ case R.id.artist_menu_play_last:
+ downloadRecursively(artist.getId(), false, true, false);
+ break;
+ case R.id.artist_menu_pin:
+ downloadRecursively(artist.getId(), true, true, false);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+ } else if (info.position == 0) {
+ MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
+ String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
+ String musicFolderName = selectedFolder == null ? getString(R.string.select_artist_all_folders)
+ : selectedFolder.getName();
+ Util.setSelectedMusicFolderId(this, musicFolderId);
+ folderName.setText(musicFolderName);
+ refresh();
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/SelectPlaylistActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/SelectPlaylistActivity.java
new file mode 100644
index 00000000..9882fad0
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/SelectPlaylistActivity.java
@@ -0,0 +1,141 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.Playlist;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.util.BackgroundTask;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.PlaylistAdapter;
+import github.daneren2005.subphonic.util.TabActivityBackgroundTask;
+import github.daneren2005.subphonic.util.Util;
+
+import java.util.List;
+
+public class SelectPlaylistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener {
+
+ private static final int MENU_ITEM_PLAY_ALL = 1;
+
+ private ListView list;
+ private View emptyTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_playlist);
+
+ list = (ListView) findViewById(R.id.select_playlist_list);
+ emptyTextView = findViewById(R.id.select_playlist_empty);
+ list.setOnItemClickListener(this);
+ registerForContextMenu(list);
+
+ // Title: Playlists
+ setTitle(R.string.playlist_label);
+
+ // Button 1: gone
+ ImageButton searchButton = (ImageButton)findViewById(R.id.action_button_1);
+ searchButton.setVisibility(View.GONE);
+
+ // Button 2: refresh
+ ImageButton refreshButton = (ImageButton) findViewById(R.id.action_button_2);
+ refreshButton.setImageResource(R.drawable.action_refresh);
+ refreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ refresh();
+ }
+ });
+
+ load();
+ }
+
+ private void refresh() {
+ finish();
+ Intent intent = new Intent(this, SelectPlaylistActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void load() {
+ BackgroundTask> task = new TabActivityBackgroundTask>(this) {
+ @Override
+ protected List doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
+ boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
+ return musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
+ }
+
+ @Override
+ protected void done(List result) {
+ list.setAdapter(new PlaylistAdapter(SelectPlaylistActivity.this, PlaylistAdapter.PlaylistComparator.sort(result)));
+ emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ menu.add(Menu.NONE, MENU_ITEM_PLAY_ALL, MENU_ITEM_PLAY_ALL, R.string.common_play_now);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Playlist playlist = (Playlist) list.getItemAtPosition(info.position);
+
+ switch (menuItem.getItemId()) {
+ case MENU_ITEM_PLAY_ALL:
+ Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
+ break;
+ default:
+ return super.onContextItemSelected(menuItem);
+ }
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+
+ Playlist playlist = (Playlist) parent.getItemAtPosition(position);
+
+ Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
+ }
+
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/SettingsActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/SettingsActivity.java
new file mode 100644
index 00000000..5eb32b62
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/SettingsActivity.java
@@ -0,0 +1,297 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.provider.SearchRecentSuggestions;
+import android.util.Log;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.provider.SearchSuggestionProvider1;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.ErrorDialog;
+import github.daneren2005.subphonic.util.FileUtil;
+import github.daneren2005.subphonic.util.ModalBackgroundTask;
+import github.daneren2005.subphonic.util.Util;
+
+import java.io.File;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private static final String TAG = SettingsActivity.class.getSimpleName();
+ private final Map serverSettings = new LinkedHashMap();
+ private boolean testingConnection;
+ private ListPreference theme;
+ private ListPreference maxBitrateWifi;
+ private ListPreference maxBitrateMobile;
+ private ListPreference cacheSize;
+ private EditTextPreference cacheLocation;
+ private ListPreference preloadCount;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.settings);
+
+ theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME);
+ maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
+ maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
+ cacheSize = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
+ cacheLocation = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
+ preloadCount = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
+
+ findPreference("testConnection1").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(1);
+ return false;
+ }
+ });
+
+ findPreference("testConnection2").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(2);
+ return false;
+ }
+ });
+
+ findPreference("testConnection3").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ testConnection(3);
+ return false;
+ }
+ });
+
+ findPreference("clearSearchHistory").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(SettingsActivity.this, SearchSuggestionProvider1.AUTHORITY, SearchSuggestionProvider1.MODE);
+ suggestions.clearHistory();
+ Util.toast(SettingsActivity.this, R.string.settings_search_history_cleared);
+ return false;
+ }
+ });
+
+ for (int i = 1; i <= 3; i++) {
+ String instance = String.valueOf(i);
+ serverSettings.put(instance, new ServerSettings(instance));
+ }
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ update();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ SharedPreferences prefs = Util.getPreferences(this);
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ Log.d(TAG, "Preference changed: " + key);
+ update();
+
+ if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) {
+ setHideMedia(sharedPreferences.getBoolean(key, false));
+ }
+ else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) {
+ setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true));
+ }
+ else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) {
+ setCacheLocation(sharedPreferences.getString(key, ""));
+ }
+ }
+
+ private void update() {
+ if (testingConnection) {
+ return;
+ }
+
+ theme.setSummary(theme.getEntry());
+ maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
+ maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
+ cacheSize.setSummary(cacheSize.getEntry());
+ cacheLocation.setSummary(cacheLocation.getText());
+ preloadCount.setSummary(preloadCount.getEntry());
+ for (ServerSettings ss : serverSettings.values()) {
+ ss.update();
+ }
+ }
+
+ private void setHideMedia(boolean hide) {
+ File nomediaDir = new File(FileUtil.getSubsonicDirectory(), ".nomedia");
+ if (hide && !nomediaDir.exists()) {
+ if (!nomediaDir.mkdir()) {
+ Log.w(TAG, "Failed to create " + nomediaDir);
+ }
+ } else if (nomediaDir.exists()) {
+ if (!nomediaDir.delete()) {
+ Log.w(TAG, "Failed to delete " + nomediaDir);
+ }
+ }
+ Util.toast(this, R.string.settings_hide_media_toast, false);
+ }
+
+ private void setMediaButtonsEnabled(boolean enabled) {
+ if (enabled) {
+ Util.registerMediaButtonEventReceiver(this);
+ } else {
+ Util.unregisterMediaButtonEventReceiver(this);
+ }
+ }
+
+ private void setCacheLocation(String path) {
+ File dir = new File(path);
+ if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
+ Util.toast(this, R.string.settings_cache_location_error, false);
+
+ // Reset it to the default.
+ String defaultPath = FileUtil.getDefaultMusicDirectory().getPath();
+ if (!defaultPath.equals(path)) {
+ SharedPreferences prefs = Util.getPreferences(this);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath);
+ editor.commit();
+ cacheLocation.setSummary(defaultPath);
+ cacheLocation.setText(defaultPath);
+ }
+
+ // Clear download queue.
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ downloadService.clear();
+ }
+ }
+
+ private void testConnection(final int instance) {
+ ModalBackgroundTask task = new ModalBackgroundTask(this, false) {
+ private int previousInstance;
+
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ updateProgress(R.string.settings_testing_connection);
+
+ previousInstance = Util.getActiveServer(SettingsActivity.this);
+ testingConnection = true;
+ Util.setActiveServer(SettingsActivity.this, instance);
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(SettingsActivity.this);
+ musicService.ping(SettingsActivity.this, this);
+ return musicService.isLicenseValid(SettingsActivity.this, null);
+ } finally {
+ Util.setActiveServer(SettingsActivity.this, previousInstance);
+ testingConnection = false;
+ }
+ }
+
+ @Override
+ protected void done(Boolean licenseValid) {
+ if (licenseValid) {
+ Util.toast(SettingsActivity.this, R.string.settings_testing_ok);
+ } else {
+ Util.toast(SettingsActivity.this, R.string.settings_testing_unlicensed);
+ }
+ }
+
+ @Override
+ protected void cancel() {
+ super.cancel();
+ Util.setActiveServer(SettingsActivity.this, previousInstance);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Log.w(TAG, error.toString(), error);
+ new ErrorDialog(SettingsActivity.this, getResources().getString(R.string.settings_connection_failure) +
+ " " + getErrorMessage(error), false);
+ }
+ };
+ task.execute();
+ }
+
+ private class ServerSettings {
+ private EditTextPreference serverName;
+ private EditTextPreference serverUrl;
+ private EditTextPreference username;
+ private PreferenceScreen screen;
+
+ private ServerSettings(String instance) {
+
+ screen = (PreferenceScreen) findPreference("server" + instance);
+ serverName = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance);
+ serverUrl = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance);
+ username = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_USERNAME + instance);
+
+ serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ try {
+ String url = (String) value;
+ new URL(url);
+ if (!url.equals(url.trim()) || url.contains("@")) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false);
+ return false;
+ }
+ return true;
+ }
+ });
+
+ username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String username = (String) value;
+ if (username == null || !username.equals(username.trim())) {
+ new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_username, false);
+ return false;
+ }
+ return true;
+ }
+ });
+ }
+
+ public void update() {
+ serverName.setSummary(serverName.getText());
+ serverUrl.setSummary(serverUrl.getText());
+ username.setSummary(username.getText());
+ screen.setSummary(serverUrl.getText());
+ screen.setTitle(serverName.getText());
+ }
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/SubsonicTabActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/SubsonicTabActivity.java
new file mode 100644
index 00000000..81145264
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/SubsonicTabActivity.java
@@ -0,0 +1,382 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.graphics.Typeface;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.ImageLoader;
+import github.daneren2005.subphonic.util.ModalBackgroundTask;
+import github.daneren2005.subphonic.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SubsonicTabActivity extends Activity {
+
+ private static final String TAG = SubsonicTabActivity.class.getSimpleName();
+ private static ImageLoader IMAGE_LOADER;
+
+ private boolean destroyed;
+ private View homeButton;
+ private View musicButton;
+ private View searchButton;
+ private View playlistButton;
+ private View nowPlayingButton;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ setUncaughtExceptionHandler();
+ applyTheme();
+ super.onCreate(bundle);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ startService(new Intent(this, DownloadServiceImpl.class));
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle bundle) {
+ super.onPostCreate(bundle);
+
+ homeButton = findViewById(R.id.button_bar_home);
+ homeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ musicButton = findViewById(R.id.button_bar_music);
+ musicButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, SelectArtistActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ searchButton = findViewById(R.id.button_bar_search);
+ searchButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ playlistButton = findViewById(R.id.button_bar_playlists);
+ playlistButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(SubsonicTabActivity.this, SelectPlaylistActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
+ }
+ });
+
+ nowPlayingButton = findViewById(R.id.button_bar_now_playing);
+ nowPlayingButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
+ }
+ });
+
+ if (this instanceof MainActivity) {
+ homeButton.setEnabled(false);
+ } else if (this instanceof SelectAlbumActivity || this instanceof SelectArtistActivity) {
+ musicButton.setEnabled(false);
+ } else if (this instanceof SearchActivity) {
+ searchButton.setEnabled(false);
+ } else if (this instanceof SelectPlaylistActivity) {
+ playlistButton.setEnabled(false);
+ } else if (this instanceof DownloadActivity || this instanceof LyricsActivity) {
+ nowPlayingButton.setEnabled(false);
+ }
+
+ updateButtonVisibility();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.registerMediaButtonEventReceiver(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case R.id.menu_exit:
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
+ Util.startActivityWithoutTransition(this, intent);
+ return true;
+
+ case R.id.menu_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ return true;
+
+ case R.id.menu_help:
+ startActivity(new Intent(this, HelpActivity.class));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ destroyed = true;
+ getImageLoader().clear();
+ }
+
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
+ boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
+ boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
+ boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+
+ if (isVolumeAdjust && isJukebox) {
+ getDownloadService().adjustJukeboxVolume(isVolumeUp);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ Util.disablePendingTransition(this);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+
+ // Set the font of title in the action bar.
+ TextView text = (TextView) findViewById(R.id.actionbar_title_text);
+ Typeface typeface = Typeface.createFromAsset(getAssets(), "fonts/Storopia.ttf");
+ text.setTypeface(typeface);
+
+ text.setText(title);
+ }
+
+ @Override
+ public void setTitle(int titleId) {
+ setTitle(getString(titleId));
+ }
+
+ private void applyTheme() {
+ String theme = Util.getTheme(this);
+ if ("dark".equals(theme)) {
+ setTheme(android.R.style.Theme);
+ } else if ("light".equals(theme)) {
+ setTheme(android.R.style.Theme_Light);
+ }
+ }
+
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ private void updateButtonVisibility() {
+ int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE;
+ searchButton.setVisibility(visibility);
+ }
+
+ public void setProgressVisible(boolean visible) {
+ View view = findViewById(R.id.tab_progress);
+ if (view != null) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public void updateProgress(String message) {
+ TextView view = (TextView) findViewById(R.id.tab_progress_message);
+ if (view != null) {
+ view.setText(message);
+ }
+ }
+
+ public DownloadService getDownloadService() {
+ // If service is not available, request it to start and wait for it.
+ for (int i = 0; i < 5; i++) {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService != null) {
+ return downloadService;
+ }
+ Log.w(TAG, "DownloadService not running. Attempting to start it.");
+ startService(new Intent(this, DownloadServiceImpl.class));
+ Util.sleepQuietly(50L);
+ }
+ return DownloadServiceImpl.getInstance();
+ }
+
+ protected void warnIfNetworkOrStorageUnavailable() {
+ if (!Util.isExternalStoragePresent()) {
+ Util.toast(this, R.string.select_album_no_sdcard);
+ } else if (!Util.isOffline(this) && !Util.isNetworkConnected(this)) {
+ Util.toast(this, R.string.select_album_no_network);
+ }
+ }
+
+ protected synchronized ImageLoader getImageLoader() {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(this);
+ }
+ return IMAGE_LOADER;
+ }
+
+ protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay) {
+ ModalBackgroundTask> task = new ModalBackgroundTask>(this, false) {
+
+ private static final int MAX_SONGS = 500;
+
+ @Override
+ protected List doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
+ MusicDirectory root = musicService.getMusicDirectory(id, false, SubsonicTabActivity.this, this);
+ List songs = new LinkedList();
+ getSongsRecursively(root, songs);
+ return songs;
+ }
+
+ private void getSongsRecursively(MusicDirectory parent, List songs) throws Exception {
+ if (songs.size() > MAX_SONGS) {
+ return;
+ }
+
+ for (MusicDirectory.Entry song : parent.getChildren(false, true)) {
+ if (!song.isVideo()) {
+ songs.add(song);
+ }
+ }
+ for (MusicDirectory.Entry dir : parent.getChildren(true, false)) {
+ MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
+ getSongsRecursively(musicService.getMusicDirectory(dir.getId(), false, SubsonicTabActivity.this, this), songs);
+ }
+ }
+
+ @Override
+ protected void done(List songs) {
+ DownloadService downloadService = getDownloadService();
+ if (!songs.isEmpty() && downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.download(songs, save, autoplay, false);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ private void setUncaughtExceptionHandler() {
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
+ if (!(handler instanceof SubsonicUncaughtExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new SubsonicUncaughtExceptionHandler(this));
+ }
+ }
+
+ /**
+ * Logs the stack trace of uncaught exceptions to a file on the SD card.
+ */
+ private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private final Thread.UncaughtExceptionHandler defaultHandler;
+ private final Context context;
+
+ private SubsonicUncaughtExceptionHandler(Context context) {
+ this.context = context;
+ defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ File file = null;
+ PrintWriter printWriter = null;
+ try {
+
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.daneren2005.subphonic", 0);
+ file = new File(Environment.getExternalStorageDirectory(), "subsonic-stacktrace.txt");
+ printWriter = new PrintWriter(file);
+ printWriter.println("Android API level: " + Build.VERSION.SDK);
+ printWriter.println("Subsonic version name: " + packageInfo.versionName);
+ printWriter.println("Subsonic version code: " + packageInfo.versionCode);
+ printWriter.println();
+ throwable.printStackTrace(printWriter);
+ Log.i(TAG, "Stack trace written to " + file);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to write stack trace to " + file, x);
+ } finally {
+ Util.close(printWriter);
+ if (defaultHandler != null) {
+ defaultHandler.uncaughtException(thread, throwable);
+ }
+
+ }
+ }
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subphonic/activity/VoiceQueryReceiverActivity.java b/subsonic-android/src/github/daneren2005/subphonic/activity/VoiceQueryReceiverActivity.java
new file mode 100644
index 00000000..2ae9a1b4
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/activity/VoiceQueryReceiverActivity.java
@@ -0,0 +1,59 @@
+/*
+ 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 github.daneren2005.subphonic.activity;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.SearchRecentSuggestions;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.Util;
+import github.daneren2005.subphonic.provider.SearchSuggestionProvider1;
+
+/**
+ * Receives voice search queries and forwards to the SearchActivity.
+ *
+ * http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html
+ *
+ * @author Sindre Mehus
+ */
+public class VoiceQueryReceiverActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String query = getIntent().getStringExtra(SearchManager.QUERY);
+
+ if (query != null) {
+ SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider1.AUTHORITY,
+ SearchSuggestionProvider1.MODE);
+ suggestions.saveRecentQuery(query, null);
+
+ Intent intent = new Intent(VoiceQueryReceiverActivity.this, SearchActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent);
+ }
+ finish();
+ Util.disablePendingTransition(this);
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/audiofx/EqualizerController.java b/subsonic-android/src/github/daneren2005/subphonic/audiofx/EqualizerController.java
new file mode 100644
index 00000000..f2dbc352
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/audiofx/EqualizerController.java
@@ -0,0 +1,138 @@
+/*
+ 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 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.audiofx;
+
+import java.io.Serializable;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.audiofx.Equalizer;
+import android.util.Log;
+import github.daneren2005.subphonic.util.FileUtil;
+
+/**
+ * Backward-compatible wrapper for {@link Equalizer}, which is API Level 9.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class EqualizerController {
+
+ private static final String TAG = EqualizerController.class.getSimpleName();
+
+ private final Context context;
+ private Equalizer equalizer;
+
+ // Class initialization fails when this throws an exception.
+ static {
+ try {
+ Class.forName("android.media.audiofx.Equalizer");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Throws an exception if the {@link Equalizer} class is not available.
+ */
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public EqualizerController(Context context, MediaPlayer mediaPlayer) {
+ this.context = context;
+ try {
+ equalizer = new Equalizer(0, mediaPlayer.getAudioSessionId());
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to create equalizer.", x);
+ }
+ }
+
+ public void saveSettings() {
+ try {
+ if (isAvailable()) {
+ FileUtil.serialize(context, new EqualizerSettings(equalizer), "equalizer.dat");
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to save equalizer settings.", x);
+ }
+ }
+
+ public void loadSettings() {
+ try {
+ if (isAvailable()) {
+ EqualizerSettings settings = FileUtil.deserialize(context, "equalizer.dat");
+ if (settings != null) {
+ settings.apply(equalizer);
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to load equalizer settings.", x);
+ }
+ }
+
+ public boolean isAvailable() {
+ return equalizer != null;
+ }
+
+ public boolean isEnabled() {
+ return isAvailable() && equalizer.getEnabled();
+ }
+
+ public void release() {
+ if (isAvailable()) {
+ equalizer.release();
+ }
+ }
+
+ public Equalizer getEqualizer() {
+ return equalizer;
+ }
+
+ private static class EqualizerSettings implements Serializable {
+
+ private final short[] bandLevels;
+ private short preset;
+ private final boolean enabled;
+
+ public EqualizerSettings(Equalizer equalizer) {
+ enabled = equalizer.getEnabled();
+ bandLevels = new short[equalizer.getNumberOfBands()];
+ for (short i = 0; i < equalizer.getNumberOfBands(); i++) {
+ bandLevels[i] = equalizer.getBandLevel(i);
+ }
+ try {
+ preset = equalizer.getCurrentPreset();
+ } catch (Exception x) {
+ preset = -1;
+ }
+ }
+
+ public void apply(Equalizer equalizer) {
+ for (short i = 0; i < bandLevels.length; i++) {
+ equalizer.setBandLevel(i, bandLevels[i]);
+ }
+ if (preset >= 0 && preset < equalizer.getNumberOfPresets()) {
+ equalizer.usePreset(preset);
+ }
+ equalizer.setEnabled(enabled);
+ }
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subphonic/audiofx/VisualizerController.java b/subsonic-android/src/github/daneren2005/subphonic/audiofx/VisualizerController.java
new file mode 100644
index 00000000..79c7f596
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/audiofx/VisualizerController.java
@@ -0,0 +1,90 @@
+/*
+ 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 2011 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.audiofx;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.audiofx.Visualizer;
+import android.util.Log;
+
+/**
+ * Backward-compatible wrapper for {@link Visualizer}, which is API Level 9.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class VisualizerController {
+
+ private static final String TAG = VisualizerController.class.getSimpleName();
+ private static final int PREFERRED_CAPTURE_SIZE = 128; // Must be a power of two.
+
+ private final Context context;
+ private Visualizer visualizer;
+
+ // Class initialization fails when this throws an exception.
+ static {
+ try {
+ Class.forName("android.media.audiofx.Visualizer");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Throws an exception if the {@link Visualizer} class is not available.
+ */
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public VisualizerController(Context context, MediaPlayer mediaPlayer) {
+ this.context = context;
+ try {
+ visualizer = new Visualizer(mediaPlayer.getAudioSessionId());
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to create visualizer.", x);
+ }
+
+ if (visualizer != null) {
+ int[] captureSizeRange = Visualizer.getCaptureSizeRange();
+ int captureSize = Math.max(PREFERRED_CAPTURE_SIZE, captureSizeRange[0]);
+ captureSize = Math.min(captureSize, captureSizeRange[1]);
+ visualizer.setCaptureSize(captureSize);
+ }
+ }
+
+ public boolean isAvailable() {
+ return visualizer != null;
+ }
+
+ public boolean isEnabled() {
+ return isAvailable() && visualizer.getEnabled();
+ }
+
+ public void release() {
+ if (isAvailable()) {
+ visualizer.release();
+ }
+ }
+
+ public Visualizer getVisualizer() {
+ return visualizer;
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/Artist.java b/subsonic-android/src/github/daneren2005/subphonic/domain/Artist.java
new file mode 100644
index 00000000..416b8b0a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/Artist.java
@@ -0,0 +1,60 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Artist implements Serializable {
+
+ private String id;
+ private String name;
+ private String index;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/Indexes.java b/subsonic-android/src/github/daneren2005/subphonic/domain/Indexes.java
new file mode 100644
index 00000000..fd42379b
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/Indexes.java
@@ -0,0 +1,50 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+import java.util.List;
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Indexes implements Serializable {
+
+ private final long lastModified;
+ private final List shortcuts;
+ private final List artists;
+
+ public Indexes(long lastModified, List shortcuts, List artists) {
+ this.lastModified = lastModified;
+ this.shortcuts = shortcuts;
+ this.artists = artists;
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+
+ public List getShortcuts() {
+ return shortcuts;
+ }
+
+ public List getArtists() {
+ return artists;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/JukeboxStatus.java b/subsonic-android/src/github/daneren2005/subphonic/domain/JukeboxStatus.java
new file mode 100644
index 00000000..6aa8c584
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/JukeboxStatus.java
@@ -0,0 +1,63 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class JukeboxStatus {
+
+ private Integer positionSeconds;
+ private Integer currentPlayingIndex;
+ private Float gain;
+ private boolean playing;
+
+ public Integer getPositionSeconds() {
+ return positionSeconds;
+ }
+
+ public void setPositionSeconds(Integer positionSeconds) {
+ this.positionSeconds = positionSeconds;
+ }
+
+ public Integer getCurrentPlayingIndex() {
+ return currentPlayingIndex;
+ }
+
+ public void setCurrentIndex(Integer currentPlayingIndex) {
+ this.currentPlayingIndex = currentPlayingIndex;
+ }
+
+ public boolean isPlaying() {
+ return playing;
+ }
+
+ public void setPlaying(boolean playing) {
+ this.playing = playing;
+ }
+
+ public Float getGain() {
+ return gain;
+ }
+
+ public void setGain(float gain) {
+ this.gain = gain;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/Lyrics.java b/subsonic-android/src/github/daneren2005/subphonic/domain/Lyrics.java
new file mode 100644
index 00000000..7d9422da
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/Lyrics.java
@@ -0,0 +1,55 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.domain;
+
+/**
+ * Song lyrics.
+ *
+ * @author Sindre Mehus
+ */
+public class Lyrics {
+
+ private String artist;
+ private String title;
+ private String text;
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/MusicDirectory.java b/subsonic-android/src/github/daneren2005/subphonic/domain/MusicDirectory.java
new file mode 100644
index 00000000..52201032
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/MusicDirectory.java
@@ -0,0 +1,259 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MusicDirectory {
+
+ private String name;
+ private final List children = new ArrayList();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void addChild(Entry child) {
+ children.add(child);
+ }
+
+ public List getChildren() {
+ return getChildren(true, true);
+ }
+
+ public List getChildren(boolean includeDirs, boolean includeFiles) {
+ if (includeDirs && includeFiles) {
+ return children;
+ }
+
+ List result = new ArrayList(children.size());
+ for (Entry child : children) {
+ if (child.isDirectory() && includeDirs || !child.isDirectory() && includeFiles) {
+ result.add(child);
+ }
+ }
+ return result;
+ }
+
+ public static class Entry implements Serializable {
+ private String id;
+ private String parent;
+ private boolean directory;
+ private String title;
+ private String album;
+ private String artist;
+ private Integer track;
+ private Integer year;
+ private String genre;
+ private String contentType;
+ private String suffix;
+ private String transcodedContentType;
+ private String transcodedSuffix;
+ private String coverArt;
+ private Long size;
+ private Integer duration;
+ private Integer bitRate;
+ private String path;
+ private boolean video;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public boolean isDirectory() {
+ return directory;
+ }
+
+ public void setDirectory(boolean directory) {
+ this.directory = directory;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public void setAlbum(String album) {
+ this.album = album;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public Integer getTrack() {
+ return track;
+ }
+
+ public void setTrack(Integer track) {
+ this.track = track;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public String getTranscodedContentType() {
+ return transcodedContentType;
+ }
+
+ public void setTranscodedContentType(String transcodedContentType) {
+ this.transcodedContentType = transcodedContentType;
+ }
+
+ public String getTranscodedSuffix() {
+ return transcodedSuffix;
+ }
+
+ public void setTranscodedSuffix(String transcodedSuffix) {
+ this.transcodedSuffix = transcodedSuffix;
+ }
+
+ public Long getSize() {
+ return size;
+ }
+
+ public void setSize(Long size) {
+ this.size = size;
+ }
+
+ public Integer getDuration() {
+ return duration;
+ }
+
+ public void setDuration(Integer duration) {
+ this.duration = duration;
+ }
+
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public String getCoverArt() {
+ return coverArt;
+ }
+
+ public void setCoverArt(String coverArt) {
+ this.coverArt = coverArt;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public boolean isVideo() {
+ return video;
+ }
+
+ public void setVideo(boolean video) {
+ this.video = video;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Entry entry = (Entry) o;
+ return id.equals(entry.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/MusicFolder.java b/subsonic-android/src/github/daneren2005/subphonic/domain/MusicFolder.java
new file mode 100644
index 00000000..8a72e93e
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/MusicFolder.java
@@ -0,0 +1,46 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+import java.io.Serializable;
+
+/**
+ * Represents a top level directory in which music or other media is stored.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MusicFolder implements Serializable {
+
+ private final String id;
+ private final String name;
+
+ public MusicFolder(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/PlayerState.java b/subsonic-android/src/github/daneren2005/subphonic/domain/PlayerState.java
new file mode 100644
index 00000000..98fd3c4b
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/PlayerState.java
@@ -0,0 +1,34 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public enum PlayerState {
+ IDLE,
+ DOWNLOADING,
+ PREPARING,
+ PREPARED,
+ STARTED,
+ STOPPED,
+ PAUSED,
+ COMPLETED
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/Playlist.java b/subsonic-android/src/github/daneren2005/subphonic/domain/Playlist.java
new file mode 100644
index 00000000..e4413620
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/Playlist.java
@@ -0,0 +1,56 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Playlist implements Serializable {
+
+ private String id;
+ private String name;
+
+ public Playlist(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/RepeatMode.java b/subsonic-android/src/github/daneren2005/subphonic/domain/RepeatMode.java
new file mode 100644
index 00000000..41421ef2
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/RepeatMode.java
@@ -0,0 +1,28 @@
+package github.daneren2005.subphonic.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public enum RepeatMode {
+ OFF {
+ @Override
+ public RepeatMode next() {
+ return ALL;
+ }
+ },
+ ALL {
+ @Override
+ public RepeatMode next() {
+ return SINGLE;
+ }
+ },
+ SINGLE {
+ @Override
+ public RepeatMode next() {
+ return OFF;
+ }
+ };
+
+ public abstract RepeatMode next();
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/SearchCritera.java b/subsonic-android/src/github/daneren2005/subphonic/domain/SearchCritera.java
new file mode 100644
index 00000000..a577ce60
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/SearchCritera.java
@@ -0,0 +1,55 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+/**
+ * The criteria for a music search.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchCritera {
+
+ private final String query;
+ private final int artistCount;
+ private final int albumCount;
+ private final int songCount;
+
+ public SearchCritera(String query, int artistCount, int albumCount, int songCount) {
+ this.query = query;
+ this.artistCount = artistCount;
+ this.albumCount = albumCount;
+ this.songCount = songCount;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public int getArtistCount() {
+ return artistCount;
+ }
+
+ public int getAlbumCount() {
+ return albumCount;
+ }
+
+ public int getSongCount() {
+ return songCount;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/SearchResult.java b/subsonic-android/src/github/daneren2005/subphonic/domain/SearchResult.java
new file mode 100644
index 00000000..8dc08bc0
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/SearchResult.java
@@ -0,0 +1,51 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+import java.util.List;
+
+/**
+ * The result of a search. Contains matching artists, albums and songs.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchResult {
+
+ private final List artists;
+ private final List albums;
+ private final List songs;
+
+ public SearchResult(List artists, List albums, List songs) {
+ this.artists = artists;
+ this.albums = albums;
+ this.songs = songs;
+ }
+
+ public List getArtists() {
+ return artists;
+ }
+
+ public List getAlbums() {
+ return albums;
+ }
+
+ public List getSongs() {
+ return songs;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/ServerInfo.java b/subsonic-android/src/github/daneren2005/subphonic/domain/ServerInfo.java
new file mode 100644
index 00000000..fc0252af
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/ServerInfo.java
@@ -0,0 +1,46 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.domain;
+
+/**
+ * Information about the Subsonic server.
+ *
+ * @author Sindre Mehus
+ */
+public class ServerInfo {
+
+ private boolean isLicenseValid;
+ private Version restVersion;
+
+ public boolean isLicenseValid() {
+ return isLicenseValid;
+ }
+
+ public void setLicenseValid(boolean licenseValid) {
+ isLicenseValid = licenseValid;
+ }
+
+ public Version getRestVersion() {
+ return restVersion;
+ }
+
+ public void setRestVersion(Version restVersion) {
+ this.restVersion = restVersion;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/domain/Version.java b/subsonic-android/src/github/daneren2005/subphonic/domain/Version.java
new file mode 100644
index 00000000..e827c95a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/domain/Version.java
@@ -0,0 +1,142 @@
+/*
+ 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 github.daneren2005.subphonic.domain;
+
+/**
+ * Represents the version number of the Subsonic Android app.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $
+ */
+public class Version implements Comparable {
+ private int major;
+ private int minor;
+ private int beta;
+ private int bugfix;
+
+ /**
+ * Creates a new version instance by parsing the given string.
+ * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public Version(String version) {
+ String[] s = version.split("\\.");
+ major = Integer.valueOf(s[0]);
+ minor = Integer.valueOf(s[1]);
+
+ if (s.length > 2) {
+ if (s[2].contains("beta")) {
+ beta = Integer.valueOf(s[2].replace("beta", ""));
+ } else {
+ bugfix = Integer.valueOf(s[2]);
+ }
+ }
+ }
+
+ public int getMajor() {
+ return major;
+ }
+
+ public int getMinor() {
+ return minor;
+ }
+
+ /**
+ * Return whether this object is equal to another.
+ * @param o Object to compare to.
+ * @return Whether this object is equals to another.
+ */
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Version version = (Version) o;
+
+ if (beta != version.beta) return false;
+ if (bugfix != version.bugfix) return false;
+ if (major != version.major) return false;
+ return minor == version.minor;
+ }
+
+ /**
+ * Returns a hash code for this object.
+ * @return A hash code for this object.
+ */
+ public int hashCode() {
+ int result;
+ result = major;
+ result = 29 * result + minor;
+ result = 29 * result + beta;
+ result = 29 * result + bugfix;
+ return result;
+ }
+
+ /**
+ * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(major).append('.').append(minor);
+ if (beta != 0) {
+ buf.append(".beta").append(beta);
+ } else if (bugfix != 0) {
+ buf.append('.').append(bugfix);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Compares this object with the specified object for order.
+ * @param version The object to compare to.
+ * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or
+ * greater than the specified object.
+ */
+ @Override
+ public int compareTo(Version version) {
+ if (major < version.major) {
+ return -1;
+ } else if (major > version.major) {
+ return 1;
+ }
+
+ if (minor < version.minor) {
+ return -1;
+ } else if (minor > version.minor) {
+ return 1;
+ }
+
+ if (bugfix < version.bugfix) {
+ return -1;
+ } else if (bugfix > version.bugfix) {
+ return 1;
+ }
+
+ int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta;
+ int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta;
+
+ if (thisBeta < otherBeta) {
+ return -1;
+ } else if (thisBeta > otherBeta) {
+ return 1;
+ }
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/provider/SearchSuggestionProvider1.java b/subsonic-android/src/github/daneren2005/subphonic/provider/SearchSuggestionProvider1.java
new file mode 100644
index 00000000..8cf8b8c7
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/provider/SearchSuggestionProvider1.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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.provider;
+
+import android.content.SearchRecentSuggestionsProvider;
+
+/**
+ * Provides search suggestions based on recent searches.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchSuggestionProvider1 extends SearchRecentSuggestionsProvider {
+
+ public static final String AUTHORITY = SearchSuggestionProvider1.class.getName();
+ public static final int MODE = DATABASE_MODE_QUERIES;
+
+ public SearchSuggestionProvider1() {
+ setupSuggestions(AUTHORITY, MODE);
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/provider/SubsonicAppWidgetProvider1.java b/subsonic-android/src/github/daneren2005/subphonic/provider/SubsonicAppWidgetProvider1.java
new file mode 100644
index 00000000..b0ffdc3a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/provider/SubsonicAppWidgetProvider1.java
@@ -0,0 +1,238 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.provider;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Environment;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.RemoteViews;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.activity.DownloadActivity;
+import github.daneren2005.subphonic.activity.MainActivity;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import github.daneren2005.subphonic.util.FileUtil;
+
+/**
+ * Simple widget to show currently playing album art along
+ * with play/pause and next track buttons.
+ *
+ * Based on source code from the stock Android Music app.
+ *
+ * @author Sindre Mehus
+ */
+public class SubsonicAppWidgetProvider1 extends AppWidgetProvider {
+
+ private static SubsonicAppWidgetProvider1 instance;
+ private static final String TAG = SubsonicAppWidgetProvider1.class.getSimpleName();
+
+ public static synchronized SubsonicAppWidgetProvider1 getInstance() {
+ if (instance == null) {
+ instance = new SubsonicAppWidgetProvider1();
+ }
+ return instance;
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Subsonic on default click
+ * and hide actions if service not running.
+ */
+ private void defaultAppWidget(Context context, int[] appWidgetIds) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget);
+
+ views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
+
+ linkButtons(context, views, false);
+ pushUpdate(context, appWidgetIds, views);
+ }
+
+ private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) {
+ // Update specific list of appWidgetIds if given, otherwise default to all
+ final AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ manager.updateAppWidget(appWidgetIds, views);
+ } else {
+ manager.updateAppWidget(new ComponentName(context, this.getClass()), views);
+ }
+ }
+
+ /**
+ * Handle a change notification coming over from {@link DownloadService}
+ */
+ public void notifyChange(Context context, DownloadService service, boolean playing) {
+ if (hasInstances(context)) {
+ performUpdate(context, service, null, playing);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this widget.
+ */
+ private boolean hasInstances(Context context) {
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
+ return (appWidgetIds.length > 0);
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget);
+
+ MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong();
+ String title = currentPlaying == null ? null : currentPlaying.getTitle();
+ CharSequence artist = currentPlaying == null ? null : currentPlaying.getArtist();
+ CharSequence errorState = null;
+
+ // Show error message?
+ String status = Environment.getExternalStorageState();
+ if (status.equals(Environment.MEDIA_SHARED) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ errorState = res.getText(R.string.widget_sdcard_busy);
+ } else if (status.equals(Environment.MEDIA_REMOVED)) {
+ errorState = res.getText(R.string.widget_sdcard_missing);
+ } else if (currentPlaying == null) {
+ errorState = res.getText(R.string.widget_initial_text);
+ }
+
+ if (errorState != null) {
+ // Show error state to user
+ views.setTextViewText(R.id.title,null);
+ views.setTextViewText(R.id.artist, errorState);
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_default);
+ } else {
+ // No error, so show normal titles
+ views.setTextViewText(R.id.title, title);
+ views.setTextViewText(R.id.artist, artist);
+ }
+
+ // Set correct drawable for pause state
+ if (playing) {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause);
+ } else {
+ views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play);
+ }
+
+ // Set the cover art
+ try {
+ int size = context.getResources().getDrawable(R.drawable.appwidget_art_default).getIntrinsicHeight();
+ Bitmap bitmap = currentPlaying == null ? null : FileUtil.getAlbumArtBitmap(context, currentPlaying, size);
+
+ if (bitmap == null) {
+ // Set default cover art
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ } else {
+ bitmap = getRoundedCornerBitmap(bitmap);
+ views.setImageViewBitmap(R.id.appwidget_coverart, bitmap);
+ }
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load cover art", x);
+ views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown);
+ }
+
+ // Link actions buttons to intents
+ linkButtons(context, views, currentPlaying != null);
+
+ pushUpdate(context, appWidgetIds, views);
+ }
+
+ /**
+ * Round the corners of a bitmap for the cover art image
+ */
+ private static Bitmap getRoundedCornerBitmap(Bitmap bitmap) {
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ final int color = 0xff424242;
+ final Paint paint = new Paint();
+ final float roundPx = 10;
+
+ // Add extra width to the rect so the right side wont be rounded.
+ final Rect rect = new Rect(0, 0, bitmap.getWidth() + (int) roundPx, bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+ canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntent}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * widget click will launch {@link DownloadActivity},
+ * otherwise we launch {@link MainActivity}.
+ */
+ private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
+
+ Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
+ views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
+
+ // Emulate media button clicks.
+ intent = new Intent("1");
+ intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
+
+ intent = new Intent("2"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+
+ intent = new Intent("3"); // Use a unique action name to ensure a different PendingIntent to be created.
+ intent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/receiver/BluetoothIntentReceiver.java b/subsonic-android/src/github/daneren2005/subphonic/receiver/BluetoothIntentReceiver.java
new file mode 100644
index 00000000..f643375e
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/receiver/BluetoothIntentReceiver.java
@@ -0,0 +1,53 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import github.daneren2005.subphonic.util.Util;
+
+/**
+ * Request media button focus when connected to Bluetooth A2DP.
+ *
+ * @author Sindre Mehus
+ */
+public class BluetoothIntentReceiver extends BroadcastReceiver {
+
+ private static final String TAG = BluetoothIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int state = intent.getIntExtra("android.bluetooth.a2dp.extra.SINK_STATE", -1);
+ Log.i(TAG, "android.bluetooth.a2dp.extra.SINK_STATE, state = " + state);
+ boolean connected = state == 2; // android.bluetooth.BluetoothA2dp.STATE_CONNECTED
+ if (connected) {
+ Log.i(TAG, "Connected to Bluetooth A2DP, requesting media button focus.");
+ Util.registerMediaButtonEventReceiver(context);
+ }
+
+ boolean disconnected = state == 0; // android.bluetooth.BluetoothA2dp.STATE_DISCONNECTED
+ if (disconnected) {
+ Log.i(TAG, "Disconnected from Bluetooth A2DP, requesting pause.");
+ context.sendBroadcast(new Intent(DownloadServiceImpl.CMD_PAUSE));
+ }
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/receiver/MediaButtonIntentReceiver.java b/subsonic-android/src/github/daneren2005/subphonic/receiver/MediaButtonIntentReceiver.java
new file mode 100644
index 00000000..f1c2a01a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/receiver/MediaButtonIntentReceiver.java
@@ -0,0 +1,50 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.KeyEvent;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+
+/**
+ * @author Sindre Mehus
+ */
+public class MediaButtonIntentReceiver extends BroadcastReceiver {
+
+ private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
+ Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
+
+ Intent serviceIntent = new Intent(context, DownloadServiceImpl.class);
+ serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+ context.startService(serviceIntent);
+
+ try {
+ abortBroadcast();
+ } catch (Exception x) {
+ // Ignored.
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/service/CachedMusicService.java b/subsonic-android/src/github/daneren2005/subphonic/service/CachedMusicService.java
new file mode 100644
index 00000000..2d0ebf55
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import github.daneren2005.subphonic.domain.Indexes;
+import github.daneren2005.subphonic.domain.JukeboxStatus;
+import github.daneren2005.subphonic.domain.Lyrics;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.MusicFolder;
+import github.daneren2005.subphonic.domain.Playlist;
+import github.daneren2005.subphonic.domain.SearchCritera;
+import github.daneren2005.subphonic.domain.SearchResult;
+import github.daneren2005.subphonic.domain.Version;
+import github.daneren2005.subphonic.util.CancellableTask;
+import github.daneren2005.subphonic.util.LRUCache;
+import github.daneren2005.subphonic.util.ProgressListener;
+import github.daneren2005.subphonic.util.TimeLimitedCache;
+import github.daneren2005.subphonic.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, String name, Context context, ProgressListener progressListener) throws Exception {
+ return musicService.getPlaylist(id, name, 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/github/daneren2005/subphonic/service/DownloadFile.java b/subsonic-android/src/github/daneren2005/subphonic/service/DownloadFile.java
new file mode 100644
index 00000000..192bd823
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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 github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.util.CancellableTask;
+import github.daneren2005.subphonic.util.FileUtil;
+import github.daneren2005.subphonic.util.Util;
+import github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/DownloadService.java b/subsonic-android/src/github/daneren2005/subphonic/service/DownloadService.java
new file mode 100644
index 00000000..f6b5b3ee
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.service;
+
+import java.util.List;
+
+import github.daneren2005.subphonic.audiofx.EqualizerController;
+import github.daneren2005.subphonic.audiofx.VisualizerController;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.PlayerState;
+import github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/DownloadServiceImpl.java b/subsonic-android/src/github/daneren2005/subphonic/service/DownloadServiceImpl.java
new file mode 100644
index 00000000..d4670cb2
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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 github.daneren2005.subphonic.audiofx.EqualizerController;
+import github.daneren2005.subphonic.audiofx.VisualizerController;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.PlayerState;
+import github.daneren2005.subphonic.domain.RepeatMode;
+import github.daneren2005.subphonic.util.CancellableTask;
+import github.daneren2005.subphonic.util.LRUCache;
+import github.daneren2005.subphonic.util.ShufflePlayBuffer;
+import github.daneren2005.subphonic.util.SimpleServiceBinder;
+import github.daneren2005.subphonic.util.Util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static github.daneren2005.subphonic.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 = "github.daneren2005.subphonic.CMD_PLAY";
+ public static final String CMD_TOGGLEPAUSE = "github.daneren2005.subphonic.CMD_TOGGLEPAUSE";
+ public static final String CMD_PAUSE = "github.daneren2005.subphonic.CMD_PAUSE";
+ public static final String CMD_STOP = "github.daneren2005.subphonic.CMD_STOP";
+ public static final String CMD_PREVIOUS = "github.daneren2005.subphonic.CMD_PREVIOUS";
+ public static final String CMD_NEXT = "github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/DownloadServiceLifecycleSupport.java b/subsonic-android/src/github/daneren2005/subphonic/service/DownloadServiceLifecycleSupport.java
new file mode 100644
index 00000000..1fdd3add
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/service/DownloadServiceLifecycleSupport.java
@@ -0,0 +1,271 @@
+/*
+ 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 github.daneren2005.subphonic.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 github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.PlayerState;
+import github.daneren2005.subphonic.util.CacheCleaner;
+import github.daneren2005.subphonic.util.FileUtil;
+import github.daneren2005.subphonic.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;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ downloadService.start();
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ downloadService.pause();
+ 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/github/daneren2005/subphonic/service/JukeboxService.java b/subsonic-android/src/github/daneren2005/subphonic/service/JukeboxService.java
new file mode 100644
index 00000000..cf2f5dba
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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 github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.JukeboxStatus;
+import github.daneren2005.subphonic.domain.PlayerState;
+import github.daneren2005.subphonic.service.parser.SubsonicRESTException;
+import github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/MediaStoreService.java b/subsonic-android/src/github/daneren2005/subphonic/service/MediaStoreService.java
new file mode 100644
index 00000000..c330f18c
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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 github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/MusicService.java b/subsonic-android/src/github/daneren2005/subphonic/service/MusicService.java
new file mode 100644
index 00000000..b74af3d4
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.service;
+
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import github.daneren2005.subphonic.domain.Indexes;
+import github.daneren2005.subphonic.domain.JukeboxStatus;
+import github.daneren2005.subphonic.domain.Lyrics;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.MusicFolder;
+import github.daneren2005.subphonic.domain.Playlist;
+import github.daneren2005.subphonic.domain.SearchCritera;
+import github.daneren2005.subphonic.domain.SearchResult;
+import github.daneren2005.subphonic.domain.Version;
+import github.daneren2005.subphonic.util.CancellableTask;
+import github.daneren2005.subphonic.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, String name, 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/github/daneren2005/subphonic/service/MusicServiceFactory.java b/subsonic-android/src/github/daneren2005/subphonic/service/MusicServiceFactory.java
new file mode 100644
index 00000000..9181f1c8
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.service;
+
+import android.content.Context;
+import github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/OfflineException.java b/subsonic-android/src/github/daneren2005/subphonic/service/OfflineException.java
new file mode 100644
index 00000000..714cc7e9
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/OfflineMusicService.java b/subsonic-android/src/github/daneren2005/subphonic/service/OfflineMusicService.java
new file mode 100644
index 00000000..0b79e02d
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/service/OfflineMusicService.java
@@ -0,0 +1,273 @@
+/*
+ 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 github.daneren2005.subphonic.service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.FileReader;
+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 github.daneren2005.subphonic.domain.Artist;
+import github.daneren2005.subphonic.domain.Indexes;
+import github.daneren2005.subphonic.domain.JukeboxStatus;
+import github.daneren2005.subphonic.domain.Lyrics;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.MusicFolder;
+import github.daneren2005.subphonic.domain.Playlist;
+import github.daneren2005.subphonic.domain.SearchCritera;
+import github.daneren2005.subphonic.domain.SearchResult;
+import github.daneren2005.subphonic.service.parser.PlaylistParser;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.FileUtil;
+import github.daneren2005.subphonic.util.ProgressListener;
+import github.daneren2005.subphonic.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 {
+ List playlists = new ArrayList();
+ File root = FileUtil.getPlaylistDirectory();
+ for (File file : FileUtil.listFiles(root)) {
+ Playlist playlist = new Playlist(file.getName(), file.getName());
+ playlists.add(playlist);
+ }
+ return playlists;
+ }
+
+ @Override
+ public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService == null) {
+ return new MusicDirectory();
+ }
+
+ Reader reader = null;
+ try {
+ reader = new FileReader(FileUtil.getPlaylistFile(name));
+ MusicDirectory fullList = new PlaylistParser(context).parse(reader, progressListener);
+ MusicDirectory playlist = new MusicDirectory();
+ for(MusicDirectory.Entry song: fullList.getChildren()) {
+ DownloadFile downloadFile = downloadService.forSong(song);
+ File completeFile = downloadFile.getCompleteFile();
+ if(completeFile.exists()) {
+ playlist.addChild(song);
+ }
+ }
+ return playlist;
+ } finally {
+ Util.close(reader);
+ }
+ }
+
+ @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/github/daneren2005/subphonic/service/RESTMusicService.java b/subsonic-android/src/github/daneren2005/subphonic/service/RESTMusicService.java
new file mode 100644
index 00000000..7a7d1415
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/service/RESTMusicService.java
@@ -0,0 +1,785 @@
+/*
+ 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 github.daneren2005.subphonic.service;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.FileReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+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 github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.Indexes;
+import github.daneren2005.subphonic.domain.JukeboxStatus;
+import github.daneren2005.subphonic.domain.Lyrics;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.MusicFolder;
+import github.daneren2005.subphonic.domain.Playlist;
+import github.daneren2005.subphonic.domain.SearchCritera;
+import github.daneren2005.subphonic.domain.SearchResult;
+import github.daneren2005.subphonic.domain.ServerInfo;
+import github.daneren2005.subphonic.domain.Version;
+import github.daneren2005.subphonic.service.parser.AlbumListParser;
+import github.daneren2005.subphonic.service.parser.ErrorParser;
+import github.daneren2005.subphonic.service.parser.IndexesParser;
+import github.daneren2005.subphonic.service.parser.JukeboxStatusParser;
+import github.daneren2005.subphonic.service.parser.LicenseParser;
+import github.daneren2005.subphonic.service.parser.LyricsParser;
+import github.daneren2005.subphonic.service.parser.MusicDirectoryParser;
+import github.daneren2005.subphonic.service.parser.MusicFoldersParser;
+import github.daneren2005.subphonic.service.parser.PlaylistParser;
+import github.daneren2005.subphonic.service.parser.PlaylistsParser;
+import github.daneren2005.subphonic.service.parser.RandomSongsParser;
+import github.daneren2005.subphonic.service.parser.SearchResult2Parser;
+import github.daneren2005.subphonic.service.parser.SearchResultParser;
+import github.daneren2005.subphonic.service.parser.VersionParser;
+import github.daneren2005.subphonic.service.ssl.SSLSocketFactory;
+import github.daneren2005.subphonic.service.ssl.TrustSelfSignedStrategy;
+import github.daneren2005.subphonic.util.CancellableTask;
+import github.daneren2005.subphonic.util.Constants;
+import github.daneren2005.subphonic.util.FileUtil;
+import github.daneren2005.subphonic.util.ProgressListener;
+import github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/ssl/TrustManagerDecorator.java b/subsonic-android/src/github/daneren2005/subphonic/service/ssl/TrustManagerDecorator.java
new file mode 100644
index 00000000..b8966c59
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/ssl/TrustSelfSignedStrategy.java b/subsonic-android/src/github/daneren2005/subphonic/service/ssl/TrustSelfSignedStrategy.java
new file mode 100644
index 00000000..7a583a95
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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/github/daneren2005/subphonic/service/ssl/TrustStrategy.java b/subsonic-android/src/github/daneren2005/subphonic/service/ssl/TrustStrategy.java
new file mode 100644
index 00000000..93cac139
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/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 github.daneren2005.subphonic.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;
+
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/AlbumView.java b/subsonic-android/src/github/daneren2005/subphonic/util/AlbumView.java
new file mode 100644
index 00000000..84fdd6d9
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/AlbumView.java
@@ -0,0 +1,55 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class AlbumView extends LinearLayout {
+
+ private TextView titleView;
+ private TextView artistView;
+ private View coverArtView;
+
+ public AlbumView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.album_title);
+ artistView = (TextView) findViewById(R.id.album_artist);
+ coverArtView = findViewById(R.id.album_coverart);
+ }
+
+ public void setAlbum(MusicDirectory.Entry album, ImageLoader imageLoader) {
+ titleView.setText(album.getTitle());
+ artistView.setText(album.getArtist());
+ artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE);
+ imageLoader.loadImage(coverArtView, album, false, true);
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/ArtistAdapter.java b/subsonic-android/src/github/daneren2005/subphonic/util/ArtistAdapter.java
new file mode 100644
index 00000000..6e60d1d8
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/ArtistAdapter.java
@@ -0,0 +1,78 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.util;
+
+import github.daneren2005.subphonic.domain.Artist;
+import github.daneren2005.subphonic.R;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class ArtistAdapter extends ArrayAdapter implements SectionIndexer {
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ public ArtistAdapter(Context context, List artists) {
+ super(context, R.layout.artist_list_item, artists);
+
+ Set sectionSet = new LinkedHashSet(30);
+ List positionList = new ArrayList(30);
+ for (int i = 0; i < artists.size(); i++) {
+ Artist artist = artists.get(i);
+ String index = artist.getIndex();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/BackgroundTask.java b/subsonic-android/src/github/daneren2005/subphonic/util/BackgroundTask.java
new file mode 100644
index 00000000..92521aa0
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/BackgroundTask.java
@@ -0,0 +1,96 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import github.daneren2005.subphonic.R;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class BackgroundTask implements ProgressListener {
+
+ private static final String TAG = BackgroundTask.class.getSimpleName();
+ private final Activity activity;
+ private final Handler handler;
+
+ public BackgroundTask(Activity activity) {
+ this.activity = activity;
+ handler = new Handler();
+ }
+
+ protected Activity getActivity() {
+ return activity;
+ }
+
+ protected Handler getHandler() {
+ return handler;
+ }
+
+ public abstract void execute();
+
+ protected abstract T doInBackground() throws Throwable;
+
+ protected abstract void done(T result);
+
+ protected void error(Throwable error) {
+ Log.w(TAG, "Got exception: " + error, error);
+ new ErrorDialog(activity, getErrorMessage(error), true);
+ }
+
+ protected String getErrorMessage(Throwable error) {
+
+ if (error instanceof IOException && !Util.isNetworkConnected(activity)) {
+ return activity.getResources().getString(R.string.background_task_no_network);
+ }
+
+ if (error instanceof FileNotFoundException) {
+ return activity.getResources().getString(R.string.background_task_not_found);
+ }
+
+ if (error instanceof IOException) {
+ return activity.getResources().getString(R.string.background_task_network_error);
+ }
+
+ if (error instanceof XmlPullParserException) {
+ return activity.getResources().getString(R.string.background_task_parse_error);
+ }
+
+ String message = error.getMessage();
+ if (message != null) {
+ return message;
+ }
+ return error.getClass().getSimpleName();
+ }
+
+ @Override
+ public abstract void updateProgress(final String message);
+
+ @Override
+ public void updateProgress(int messageId) {
+ updateProgress(activity.getResources().getString(messageId));
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/CacheCleaner.java b/subsonic-android/src/github/daneren2005/subphonic/util/CacheCleaner.java
new file mode 100644
index 00000000..f66ae910
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/CacheCleaner.java
@@ -0,0 +1,171 @@
+package github.daneren2005.subphonic.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.content.Context;
+import android.util.Log;
+import android.os.StatFs;
+import github.daneren2005.subphonic.service.DownloadFile;
+import github.daneren2005.subphonic.service.DownloadService;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheCleaner {
+
+ private static final String TAG = CacheCleaner.class.getSimpleName();
+ private static final double MAX_FILE_SYSTEM_USAGE = 0.95;
+
+ private final Context context;
+ private final DownloadService downloadService;
+
+ public CacheCleaner(Context context, DownloadService downloadService) {
+ this.context = context;
+ this.downloadService = downloadService;
+ }
+
+ public void clean() {
+
+ Log.i(TAG, "Starting cache cleaning.");
+
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return;
+ }
+
+ try {
+
+ List files = new ArrayList();
+ List dirs = new ArrayList();
+
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
+ sortByAscendingModificationTime(files);
+
+ Set undeletable = findUndeletableFiles();
+
+ deleteFiles(files, undeletable);
+ deleteEmptyDirs(dirs, undeletable);
+ Log.i(TAG, "Completed cache cleaning.");
+
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+ }
+
+ private void deleteEmptyDirs(List dirs, Set undeletable) {
+ for (File dir : dirs) {
+ if (undeletable.contains(dir)) {
+ continue;
+ }
+
+ File[] children = dir.listFiles();
+
+ // Delete empty directory and associated album artwork.
+ if (children.length == 0) {
+ Util.delete(dir);
+ Util.delete(FileUtil.getAlbumArtFile(dir));
+ }
+ }
+ }
+
+ private void deleteFiles(List files, Set undeletable) {
+
+ if (files.isEmpty()) {
+ return;
+ }
+
+ long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
+
+ long bytesUsedBySubsonic = 0L;
+ for (File file : files) {
+ bytesUsedBySubsonic += file.length();
+ }
+
+ // Ensure that file system is not more than 95% full.
+ StatFs stat = new StatFs(files.get(0).getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
+ long minFsAvailability = Math.round(MAX_FILE_SYSTEM_USAGE * (double) bytesTotalFs);
+
+ long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
+ long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
+ long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
+
+ Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
+ Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes));
+ Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
+ Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
+
+ long bytesDeleted = 0L;
+ for (File file : files) {
+
+ if (file.getName().equals(Constants.ALBUM_ART_FILE)) {
+ // Move artwork to new folder.
+ file.renameTo(FileUtil.getAlbumArtFile(file.getParentFile()));
+
+ } else if (bytesToDelete > bytesDeleted || file.getName().endsWith(".partial") || file.getName().contains(".partial.")) {
+ if (!undeletable.contains(file)) {
+ long size = file.length();
+ if (Util.delete(file)) {
+ bytesDeleted += size;
+ }
+ }
+ }
+ }
+
+ Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
+ Log.i(TAG, "Cache size after : " + Util.formatBytes(bytesUsedBySubsonic - bytesDeleted));
+ }
+
+ private void findCandidatesForDeletion(File file, List files, List dirs) {
+ if (file.isFile()) {
+ String name = file.getName();
+ boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
+ boolean isAlbumArtFile = name.equals(Constants.ALBUM_ART_FILE);
+ if (isCacheFile || isAlbumArtFile) {
+ files.add(file);
+ }
+ } else {
+ // Depth-first
+ for (File child : FileUtil.listFiles(file)) {
+ findCandidatesForDeletion(child, files, dirs);
+ }
+ dirs.add(file);
+ }
+ }
+
+ private void sortByAscendingModificationTime(List files) {
+ Collections.sort(files, new Comparator() {
+ @Override
+ public int compare(File a, File b) {
+ if (a.lastModified() < b.lastModified()) {
+ return -1;
+ }
+ if (a.lastModified() > b.lastModified()) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ }
+
+ private Set findUndeletableFiles() {
+ Set undeletable = new HashSet(5);
+
+ for (DownloadFile downloadFile : downloadService.getDownloads()) {
+ undeletable.add(downloadFile.getPartialFile());
+ undeletable.add(downloadFile.getCompleteFile());
+ }
+
+ undeletable.add(FileUtil.getMusicDirectory(context));
+ return undeletable;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/CancellableTask.java b/subsonic-android/src/github/daneren2005/subphonic/util/CancellableTask.java
new file mode 100644
index 00000000..1886e4d0
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/CancellableTask.java
@@ -0,0 +1,87 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import android.util.Log;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class CancellableTask {
+
+ private static final String TAG = CancellableTask.class.getSimpleName();
+
+ private final AtomicBoolean running = new AtomicBoolean(false);
+ private final AtomicBoolean cancelled = new AtomicBoolean(false);
+ private final AtomicReference thread = new AtomicReference();
+ private final AtomicReference cancelListener = new AtomicReference();
+
+ public void cancel() {
+ Log.d(TAG, "Cancelling " + CancellableTask.this);
+ cancelled.set(true);
+
+ OnCancelListener listener = cancelListener.get();
+ if (listener != null) {
+ try {
+ listener.onCancel();
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when invoking OnCancelListener.", x);
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return cancelled.get();
+ }
+
+ public void setOnCancelListener(OnCancelListener listener) {
+ cancelListener.set(listener);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public abstract void execute();
+
+ public void start() {
+ thread.set(new Thread() {
+ @Override
+ public void run() {
+ running.set(true);
+ Log.d(TAG, "Starting thread for " + CancellableTask.this);
+ try {
+ execute();
+ } finally {
+ running.set(false);
+ Log.d(TAG, "Stopping thread for " + CancellableTask.this);
+ }
+ }
+ });
+ thread.get().start();
+ }
+
+ public static interface OnCancelListener {
+ void onCancel();
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/Constants.java b/subsonic-android/src/github/daneren2005/subphonic/util/Constants.java
new file mode 100644
index 00000000..c9b339d6
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/Constants.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 github.daneren2005.subphonic.util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Constants {
+
+ // Character encoding used throughout.
+ public static final String UTF_8 = "UTF-8";
+
+ // REST protocol version and client ID.
+ // Note: Keep it as low as possible to maintain compatibility with older servers.
+ public static final String REST_PROTOCOL_VERSION = "1.2.0";
+ public static final String REST_CLIENT_ID = "android";
+
+ // Names for intent extras.
+ public static final String INTENT_EXTRA_NAME_ID = "subsonic.id";
+ public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name";
+ public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist";
+ public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title";
+ public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall";
+ public static final String INTENT_EXTRA_NAME_ERROR = "subsonic.error";
+ public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
+ public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
+ public static final String INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh";
+ public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch";
+ public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ;
+
+ // Notification IDs.
+ public static final int NOTIFICATION_ID_PLAYING = 100;
+ public static final int NOTIFICATION_ID_ERROR = 101;
+
+ // Preferences keys.
+ public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
+ public static final String PREFERENCES_KEY_SERVER_NAME = "serverName";
+ public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl";
+ public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId";
+ public static final String PREFERENCES_KEY_USERNAME = "username";
+ public static final String PREFERENCES_KEY_PASSWORD = "password";
+ public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
+ public static final String PREFERENCES_KEY_THEME = "theme";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile";
+ public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
+ public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount";
+ public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia";
+ public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons";
+ public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload";
+ public static final String PREFERENCES_KEY_SCROBBLE = "scrobble";
+ public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
+ public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
+
+ // Name of the preferences file.
+ public static final String PREFERENCES_FILE_NAME = "github.daneren2005.subphonic_preferences";
+
+ // Number of free trial days for non-licensed servers.
+ public static final int FREE_TRIAL_DAYS = 30;
+
+ // URL for project donations.
+ public static final String DONATION_URL = "http://subsonic.org/pages/android-donation.jsp";
+
+ public static final String ALBUM_ART_FILE = "folder.jpeg";
+
+ private Constants() {
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/EntryAdapter.java b/subsonic-android/src/github/daneren2005/subphonic/util/EntryAdapter.java
new file mode 100644
index 00000000..ba675a1a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/EntryAdapter.java
@@ -0,0 +1,71 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.util;
+
+import java.util.List;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.subphonic.activity.SubsonicTabActivity;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+
+/**
+ * @author Sindre Mehus
+ */
+public class EntryAdapter extends ArrayAdapter {
+
+ private final SubsonicTabActivity activity;
+ private final ImageLoader imageLoader;
+ private final boolean checkable;
+
+ public EntryAdapter(SubsonicTabActivity activity, ImageLoader imageLoader, List entries, boolean checkable) {
+ super(activity, android.R.layout.simple_list_item_1, entries);
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+ this.checkable = checkable;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+
+ if (entry.isDirectory()) {
+ AlbumView view;
+ // TODO: Reuse AlbumView objects once cover art loading is working.
+// if (convertView != null && convertView instanceof AlbumView) {
+// view = (AlbumView) convertView;
+// } else {
+ view = new AlbumView(activity);
+// }
+ view.setAlbum(entry, imageLoader);
+ return view;
+
+ } else {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(activity);
+ }
+ view.setSong(entry, checkable);
+ return view;
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/ErrorDialog.java b/subsonic-android/src/github/daneren2005/subphonic/util/ErrorDialog.java
new file mode 100644
index 00000000..9adddccd
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/ErrorDialog.java
@@ -0,0 +1,61 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import github.daneren2005.subphonic.R;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ErrorDialog {
+
+ public ErrorDialog(Activity activity, int messageId, boolean finishActivityOnCancel) {
+ this(activity, activity.getResources().getString(messageId), finishActivityOnCancel);
+ }
+
+ public ErrorDialog(final Activity activity, String message, final boolean finishActivityOnClose) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setTitle(R.string.error_label);
+ builder.setMessage(message);
+ builder.setCancelable(true);
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ if (finishActivityOnClose) {
+ activity.finish();
+ }
+ }
+ });
+ builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (finishActivityOnClose) {
+ activity.finish();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/FileUtil.java b/subsonic-android/src/github/daneren2005/subphonic/util/FileUtil.java
new file mode 100644
index 00000000..a97d13a5
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/FileUtil.java
@@ -0,0 +1,311 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.Iterator;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.util.Log;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+
+/**
+ * @author Sindre Mehus
+ */
+public class FileUtil {
+
+ private static final String TAG = FileUtil.class.getSimpleName();
+ private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">"};
+ private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">"};
+ private static final List MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
+ private static final File DEFAULT_MUSIC_DIR = createDirectory("music");
+
+ public static File getSongFile(Context context, MusicDirectory.Entry song) {
+ File dir = getAlbumDirectory(context, song);
+
+ StringBuilder fileName = new StringBuilder();
+ Integer track = song.getTrack();
+ if (track != null) {
+ if (track < 10) {
+ fileName.append("0");
+ }
+ fileName.append(track).append("-");
+ }
+
+ fileName.append(fileSystemSafe(song.getTitle())).append(".");
+
+ if (song.getTranscodedSuffix() != null) {
+ fileName.append(song.getTranscodedSuffix());
+ } else {
+ fileName.append(song.getSuffix());
+ }
+
+ return new File(dir, fileName.toString());
+ }
+
+ public static File getPlaylistFile(String id) {
+ File playlistDir = getPlaylistDirectory();
+ return new File(playlistDir, id);
+ }
+ public static File getPlaylistDirectory() {
+ File playlistDir = new File(getSubsonicDirectory(), "playlists");
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+
+ public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
+ File albumDir = getAlbumDirectory(context, entry);
+ return getAlbumArtFile(albumDir);
+ }
+
+ public static File getAlbumArtFile(File albumDir) {
+ File albumArtDir = getAlbumArtDirectory();
+ return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg");
+ }
+
+ public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
+ File albumArtFile = getAlbumArtFile(context, entry);
+ if (albumArtFile.exists()) {
+ Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath());
+ return bitmap == null ? null : Bitmap.createScaledBitmap(bitmap, size, size, true);
+ }
+ return null;
+ }
+
+ public static File getAlbumArtDirectory() {
+ File albumArtDir = new File(getSubsonicDirectory(), "artwork");
+ ensureDirectoryExistsAndIsReadWritable(albumArtDir);
+ ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia"));
+ return albumArtDir;
+ }
+
+ private static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
+ File dir;
+ if (entry.getPath() != null) {
+ File f = new File(fileSystemSafeDir(entry.getPath()));
+ dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
+ } else {
+ String artist = fileSystemSafe(entry.getArtist());
+ String album = fileSystemSafe(entry.getAlbum());
+ dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
+ }
+ return dir;
+ }
+
+ public static void createDirectoryForParent(File file) {
+ File dir = file.getParentFile();
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory " + dir);
+ }
+ }
+ }
+
+ private static File createDirectory(String name) {
+ File dir = new File(getSubsonicDirectory(), name);
+ if (!dir.exists() && !dir.mkdirs()) {
+ Log.e(TAG, "Failed to create " + name);
+ }
+ return dir;
+ }
+
+ public static File getSubsonicDirectory() {
+ return new File(Environment.getExternalStorageDirectory(), "subsonic");
+ }
+
+ public static File getDefaultMusicDirectory() {
+ return DEFAULT_MUSIC_DIR;
+ }
+
+ public static File getMusicDirectory(Context context) {
+ String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath());
+ File dir = new File(path);
+ return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory();
+ }
+
+ public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) {
+ if (dir == null) {
+ return false;
+ }
+
+ if (dir.exists()) {
+ if (!dir.isDirectory()) {
+ Log.w(TAG, dir + " exists but is not a directory.");
+ return false;
+ }
+ } else {
+ if (dir.mkdirs()) {
+ Log.i(TAG, "Created directory " + dir);
+ } else {
+ Log.w(TAG, "Failed to create directory " + dir);
+ return false;
+ }
+ }
+
+ if (!dir.canRead()) {
+ Log.w(TAG, "No read permission for directory " + dir);
+ return false;
+ }
+
+ if (!dir.canWrite()) {
+ Log.w(TAG, "No write permission for directory " + dir);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param filename The filename in question.
+ * @return The filename with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafe(String filename) {
+ if (filename == null || filename.trim().length() == 0) {
+ return "unnamed";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-");
+ }
+ return filename;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like colons (":")
+ * with dashes ("-").
+ *
+ * @param path The path of the directory in question.
+ * @return The the directory name with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafeDir(String path) {
+ if (path == null || path.trim().length() == 0) {
+ return "";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE_DIR) {
+ path = path.replace(s, "-");
+ }
+ return path;
+ }
+
+ /**
+ * Similar to {@link File#listFiles()}, but returns a sorted set.
+ * Never returns {@code null}, instead a warning is logged, and an empty set is returned.
+ */
+ public static SortedSet listFiles(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ Log.w(TAG, "Failed to list children for " + dir.getPath());
+ return new TreeSet();
+ }
+
+ return new TreeSet(Arrays.asList(files));
+ }
+
+ public static SortedSet listMusicFiles(File dir) {
+ SortedSet files = listFiles(dir);
+ Iterator iterator = files.iterator();
+ while (iterator.hasNext()) {
+ File file = iterator.next();
+ if (!file.isDirectory() && !isMusicFile(file)) {
+ iterator.remove();
+ }
+ }
+ return files;
+ }
+
+ private static boolean isMusicFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension);
+ }
+
+ /**
+ * Returns the extension (the substring after the last dot) of the given file. The dot
+ * is not included in the returned extension.
+ *
+ * @param name The filename in question.
+ * @return The extension, or an empty string if no extension is found.
+ */
+ public static String getExtension(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? "" : name.substring(index + 1).toLowerCase();
+ }
+
+ /**
+ * Returns the base name (the substring before the last dot) of the given file. The dot
+ * is not included in the returned basename.
+ *
+ * @param name The filename in question.
+ * @return The base name, or an empty string if no basename is found.
+ */
+ public static String getBaseName(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? name : name.substring(0, index);
+ }
+
+ public static boolean serialize(Context context, T obj, String fileName) {
+ File file = new File(context.getCacheDir(), fileName);
+ ObjectOutputStream out = null;
+ try {
+ out = new ObjectOutputStream(new FileOutputStream(file));
+ out.writeObject(obj);
+ Log.i(TAG, "Serialized object to " + file);
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize object to " + file);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ public static T deserialize(Context context, String fileName) {
+ File file = new File(context.getCacheDir(), fileName);
+ if (!file.exists() || !file.isFile()) {
+ return null;
+ }
+
+ ObjectInputStream in = null;
+ try {
+ in = new ObjectInputStream(new FileInputStream(file));
+ T result = (T) in.readObject();
+ Log.i(TAG, "Deserialized object from " + file);
+ return result;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize object from " + file, x);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/HorizontalSlider.java b/subsonic-android/src/github/daneren2005/subphonic/util/HorizontalSlider.java
new file mode 100644
index 00000000..f6c5f841
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/HorizontalSlider.java
@@ -0,0 +1,141 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ProgressBar;
+import github.daneren2005.subphonic.R;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class HorizontalSlider extends ProgressBar {
+
+ private final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slider_knob);
+ private boolean slidingEnabled;
+ private OnSliderChangeListener listener;
+ private static final int PADDING = 2;
+ private boolean sliding;
+ private int sliderPosition;
+ private int startPosition;
+
+ public interface OnSliderChangeListener {
+ void onSliderChanged(View view, int position, boolean inProgress);
+ }
+
+ public HorizontalSlider(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public HorizontalSlider(Context context, AttributeSet attrs) {
+ super(context, attrs, android.R.attr.progressBarStyleHorizontal);
+ }
+
+ public HorizontalSlider(Context context) {
+ super(context);
+ }
+
+ public void setSlidingEnabled(boolean slidingEnabled) {
+ if (this.slidingEnabled != slidingEnabled) {
+ this.slidingEnabled = slidingEnabled;
+ invalidate();
+ }
+ }
+
+ public boolean isSlidingEnabled() {
+ return slidingEnabled;
+ }
+
+ public void setOnSliderChangeListener(OnSliderChangeListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int max = getMax();
+ if (!slidingEnabled || max == 0) {
+ return;
+ }
+
+ int paddingLeft = getPaddingLeft();
+ int paddingRight = getPaddingRight();
+ int paddingTop = getPaddingTop();
+ int paddingBottom = getPaddingBottom();
+
+ int w = getWidth() - paddingLeft - paddingRight;
+ int h = getHeight() - paddingTop - paddingBottom;
+ int position = sliding ? sliderPosition : getProgress();
+
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getWidth();
+ float x = paddingLeft + w * ((float) position / max) - bitmapWidth / 2.0F;
+ x = Math.max(x, paddingLeft);
+ x = Math.min(x, paddingLeft + w - bitmapWidth);
+ float y = paddingTop + h / 2.0F - bitmapHeight / 2.0F;
+
+ canvas.drawBitmap(bitmap, x, y, null);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!slidingEnabled) {
+ return false;
+ }
+
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ sliding = true;
+ startPosition = getProgress();
+ }
+
+ float x = event.getX() - PADDING;
+ float width = getWidth() - 2 * PADDING;
+ sliderPosition = Math.round((float) getMax() * (x / width));
+ sliderPosition = Math.max(sliderPosition, 0);
+
+ setProgress(Math.min(startPosition, sliderPosition));
+ setSecondaryProgress(Math.max(startPosition, sliderPosition));
+ if (listener != null) {
+ listener.onSliderChanged(this, sliderPosition, true);
+ }
+
+ } else if (action == MotionEvent.ACTION_UP) {
+ sliding = false;
+ setProgress(sliderPosition);
+ setSecondaryProgress(0);
+ if (listener != null) {
+ listener.onSliderChanged(this, sliderPosition, false);
+ }
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/ImageLoader.java b/subsonic-android/src/github/daneren2005/subphonic/util/ImageLoader.java
new file mode 100644
index 00000000..5a649250
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/ImageLoader.java
@@ -0,0 +1,252 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Asynchronous loading of images, with caching.
+ *
+ * There should normally be only one instance of this class.
+ *
+ * @author Sindre Mehus
+ */
+public class ImageLoader implements Runnable {
+
+ private static final String TAG = ImageLoader.class.getSimpleName();
+ private static final int CONCURRENCY = 5;
+
+ private final LRUCache cache = new LRUCache(100);
+ private final BlockingQueue queue;
+ private final int imageSizeDefault;
+ private final int imageSizeLarge;
+ private Drawable largeUnknownImage;
+
+ public ImageLoader(Context context) {
+ queue = new LinkedBlockingQueue(500);
+
+ // Determine the density-dependent image sizes.
+ imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels) * 0.6);
+
+ for (int i = 0; i < CONCURRENCY; i++) {
+ new Thread(this, "ImageLoader").start();
+ }
+
+ createLargeUnknownImage(context);
+ }
+
+ private void createLargeUnknownImage(Context context) {
+ BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large);
+ Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true);
+ bitmap = createReflection(bitmap);
+ largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap);
+ }
+
+ public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
+ if (entry == null || entry.getCoverArt() == null) {
+ setUnknownImage(view, large);
+ return;
+ }
+
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ Drawable drawable = cache.get(getKey(entry.getCoverArt(), size));
+ if (drawable != null) {
+ setImage(view, drawable, large);
+ return;
+ }
+
+ if (!large) {
+ setUnknownImage(view, large);
+ }
+ queue.offer(new Task(view, entry, size, large, large, crossfade));
+ }
+
+ private String getKey(String coverArtId, int size) {
+ return coverArtId + size;
+ }
+
+ private void setImage(View view, Drawable drawable, boolean crossfade) {
+ if (view instanceof TextView) {
+ // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though.
+ TextView textView = (TextView) view;
+ textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ } else if (view instanceof ImageView) {
+ ImageView imageView = (ImageView) view;
+ if (crossfade) {
+
+ Drawable existingDrawable = imageView.getDrawable();
+ if (existingDrawable == null) {
+ Bitmap emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ existingDrawable = new BitmapDrawable(emptyImage);
+ }
+
+ Drawable[] layers = new Drawable[]{existingDrawable, drawable};
+
+ TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(250);
+ } else {
+ imageView.setImageDrawable(drawable);
+ }
+ }
+ }
+
+ private void setUnknownImage(View view, boolean large) {
+ if (large) {
+ setImage(view, largeUnknownImage, false);
+ } else {
+ if (view instanceof TextView) {
+ ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0);
+ } else if (view instanceof ImageView) {
+ ((ImageView) view).setImageResource(R.drawable.unknown_album);
+ }
+ }
+ }
+
+ public void clear() {
+ queue.clear();
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ Task task = queue.take();
+ task.execute();
+ } catch (Throwable x) {
+ Log.e(TAG, "Unexpected exception in ImageLoader.", x);
+ }
+ }
+ }
+
+ private Bitmap createReflection(Bitmap originalImage) {
+
+ int width = originalImage.getWidth();
+ int height = originalImage.getHeight();
+
+ // The gap we want between the reflection and the original image
+ final int reflectionGap = 4;
+
+ // This will not scale but will flip on the Y axis
+ Matrix matrix = new Matrix();
+ matrix.preScale(1, -1);
+
+ // Create a Bitmap with the flip matix applied to it.
+ // We only want the bottom half of the image
+ Bitmap reflectionImage = Bitmap.createBitmap(originalImage, 0, height / 2, width, height / 2, matrix, false);
+
+ // Create a new bitmap with same width but taller to fit reflection
+ Bitmap bitmapWithReflection = Bitmap.createBitmap(width, (height + height / 2), Bitmap.Config.ARGB_8888);
+
+ // Create a new Canvas with the bitmap that's big enough for
+ // the image plus gap plus reflection
+ Canvas canvas = new Canvas(bitmapWithReflection);
+
+ // Draw in the original image
+ canvas.drawBitmap(originalImage, 0, 0, null);
+
+ // Draw in the gap
+ Paint defaultPaint = new Paint();
+ canvas.drawRect(0, height, width, height + reflectionGap, defaultPaint);
+
+ // Draw in the reflection
+ canvas.drawBitmap(reflectionImage, 0, height + reflectionGap, null);
+
+ // Create a shader that is a linear gradient that covers the reflection
+ Paint paint = new Paint();
+ LinearGradient shader = new LinearGradient(0, originalImage.getHeight(), 0,
+ bitmapWithReflection.getHeight() + reflectionGap, 0x70000000, 0xff000000,
+ Shader.TileMode.CLAMP);
+
+ // Set the paint to use this shader (linear gradient)
+ paint.setShader(shader);
+
+ // Draw a rectangle using the paint with our linear gradient
+ canvas.drawRect(0, height, width, bitmapWithReflection.getHeight() + reflectionGap, paint);
+
+ return bitmapWithReflection;
+ }
+
+ private class Task {
+ private final View view;
+ private final MusicDirectory.Entry entry;
+ private final Handler handler;
+ private final int size;
+ private final boolean reflection;
+ private final boolean saveToFile;
+ private final boolean crossfade;
+
+ public Task(View view, MusicDirectory.Entry entry, int size, boolean reflection, boolean saveToFile, boolean crossfade) {
+ this.view = view;
+ this.entry = entry;
+ this.size = size;
+ this.reflection = reflection;
+ this.saveToFile = saveToFile;
+ this.crossfade = crossfade;
+ handler = new Handler();
+ }
+
+ public void execute() {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
+ Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, null);
+
+ if (reflection) {
+ bitmap = createReflection(bitmap);
+ }
+
+ final Drawable drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap);
+ cache.put(getKey(entry.getCoverArt(), size), drawable);
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ setImage(view, drawable, crossfade);
+ }
+ });
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ }
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/LRUCache.java b/subsonic-android/src/github/daneren2005/subphonic/util/LRUCache.java
new file mode 100644
index 00000000..3e008da4
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/LRUCache.java
@@ -0,0 +1,102 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Sindre Mehus
+ */
+public class LRUCache{
+
+ private final int capacity;
+ private final Map map;
+
+ public LRUCache(int capacity) {
+ map = new HashMap(capacity);
+ this.capacity = capacity;
+ }
+
+ public synchronized V get(K key) {
+ TimestampedValue value = map.get(key);
+
+ V result = null;
+ if (value != null) {
+ value.updateTimestamp();
+ result = value.getValue();
+ }
+
+ return result;
+ }
+
+ public synchronized void put(K key, V value) {
+ if (map.size() >= capacity) {
+ removeOldest();
+ }
+ map.put(key, new TimestampedValue(value));
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ private void removeOldest() {
+ K oldestKey = null;
+ long oldestTimestamp = Long.MAX_VALUE;
+
+ for (Map.Entry entry : map.entrySet()) {
+ K key = entry.getKey();
+ TimestampedValue value = entry.getValue();
+ if (value.getTimestamp() < oldestTimestamp) {
+ oldestTimestamp = value.getTimestamp();
+ oldestKey = key;
+ }
+ }
+
+ if (oldestKey != null) {
+ map.remove(oldestKey);
+ }
+ }
+
+ private final class TimestampedValue {
+
+ private final SoftReference value;
+ private long timestamp;
+
+ public TimestampedValue(V value) {
+ this.value = new SoftReference(value);
+ updateTimestamp();
+ }
+
+ public V getValue() {
+ return value.get();
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void updateTimestamp() {
+ timestamp = System.currentTimeMillis();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/MergeAdapter.java b/subsonic-android/src/github/daneren2005/subphonic/util/MergeAdapter.java
new file mode 100644
index 00000000..0002cb57
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/MergeAdapter.java
@@ -0,0 +1,290 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed 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.
+ */
+
+package github.daneren2005.subphonic.util;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+/**
+ * Adapter that merges multiple child adapters and views
+ * into a single contiguous whole.
+ *
+ * Adapters used as pieces within MergeAdapter must
+ * have view type IDs monotonically increasing from 0. Ideally,
+ * adapters also have distinct ranges for their row ids, as
+ * returned by getItemId().
+ */
+public class MergeAdapter extends BaseAdapter {
+
+ private final CascadeDataSetObserver observer = new CascadeDataSetObserver();
+ private final ArrayList pieces = new ArrayList();
+
+ /**
+ * Stock constructor, simply chaining to the superclass.
+ */
+ public MergeAdapter() {
+ super();
+ }
+
+ /**
+ * Adds a new adapter to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param adapter Source for row views for this section
+ */
+ public void addAdapter(ListAdapter adapter) {
+ pieces.add(adapter);
+ adapter.registerDataSetObserver(observer);
+ }
+
+ public void removeAdapter(ListAdapter adapter) {
+ adapter.unregisterDataSetObserver(observer);
+ pieces.remove(adapter);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ */
+ public ListAdapter addView(View view) {
+ return addView(view, false);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addView(View view, boolean enabled) {
+ return addViews(Arrays.asList(view), enabled);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ */
+ public ListAdapter addViews(List views) {
+ return addViews(views, false);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addViews(List views, boolean enabled) {
+ ListAdapter adapter = enabled ? new EnabledSackAdapter(views) : new SackOfViewsAdapter(views);
+ addAdapter(adapter);
+ return adapter;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItem(position));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getCount();
+ }
+
+ return (total);
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getViewTypeCount();
+ }
+
+ return (Math.max(total, 1)); // needed for setListAdapter() before content add'
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int typeOffset = 0;
+ int result = -1;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ result = typeOffset + piece.getItemViewType(position);
+ break;
+ }
+
+ position -= size;
+ typeOffset += piece.getViewTypeCount();
+ }
+
+ return (result);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.isEnabled(position));
+ }
+
+ position -= size;
+ }
+
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+
+ return (piece.getView(position, convertView, parent));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItemId(position));
+ }
+
+ position -= size;
+ }
+
+ return (-1);
+ }
+
+ private static class EnabledSackAdapter extends SackOfViewsAdapter {
+ public EnabledSackAdapter(List views) {
+ super(views);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (true);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return (true);
+ }
+ }
+
+ private class CascadeDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/ModalBackgroundTask.java b/subsonic-android/src/github/daneren2005/subphonic/util/ModalBackgroundTask.java
new file mode 100644
index 00000000..4bd384db
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/ModalBackgroundTask.java
@@ -0,0 +1,139 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.util.Log;
+import github.daneren2005.subphonic.R;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class ModalBackgroundTask extends BackgroundTask {
+
+ private static final String TAG = ModalBackgroundTask.class.getSimpleName();
+
+ private final AlertDialog progressDialog;
+ private Thread thread;
+ private final boolean finishActivityOnCancel;
+ private boolean cancelled;
+
+ public ModalBackgroundTask(Activity activity, boolean finishActivityOnCancel) {
+ super(activity);
+ this.finishActivityOnCancel = finishActivityOnCancel;
+ progressDialog = createProgressDialog();
+ }
+
+ public ModalBackgroundTask(Activity activity) {
+ this(activity, true);
+ }
+
+ private AlertDialog createProgressDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+ builder.setTitle(R.string.background_task_wait);
+ builder.setMessage(R.string.background_task_loading);
+ builder.setCancelable(true);
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ cancel();
+ }
+ });
+ builder.setPositiveButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ cancel();
+ }
+ });
+
+ return builder.create();
+ }
+
+ public void execute() {
+ cancelled = false;
+ progressDialog.show();
+
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (cancelled) {
+ progressDialog.dismiss();
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ progressDialog.dismiss();
+ done(result);
+ }
+ });
+
+ } catch (final Throwable t) {
+ if (cancelled) {
+ return;
+ }
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ progressDialog.dismiss();
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ protected void cancel() {
+ cancelled = true;
+ if (thread != null) {
+ thread.interrupt();
+ }
+
+ if (finishActivityOnCancel) {
+ getActivity().finish();
+ }
+ }
+
+ protected boolean isCancelled() {
+ return cancelled;
+ }
+
+ protected void error(Throwable error) {
+ Log.w(TAG, "Got exception: " + error, error);
+ new ErrorDialog(getActivity(), getErrorMessage(error), finishActivityOnCancel);
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ progressDialog.setMessage(message);
+ }
+ });
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/MyViewFlipper.java b/subsonic-android/src/github/daneren2005/subphonic/util/MyViewFlipper.java
new file mode 100644
index 00000000..18d0f362
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/MyViewFlipper.java
@@ -0,0 +1,53 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ViewFlipper;
+
+/**
+ * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191)
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MyViewFlipper extends ViewFlipper {
+
+ public MyViewFlipper(Context context) {
+ super(context);
+ }
+
+ public MyViewFlipper(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+ @Override
+ protected void onDetachedFromWindow() {
+ try {
+ super.onDetachedFromWindow();
+ }
+ catch (IllegalArgumentException e) {
+ // Call stopFlipping() in order to kick off updateRunning()
+ stopFlipping();
+ }
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/Pair.java b/subsonic-android/src/github/daneren2005/subphonic/util/Pair.java
new file mode 100644
index 00000000..1f40fdf1
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/Pair.java
@@ -0,0 +1,54 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Pair implements Serializable {
+
+ private S first;
+ private T second;
+
+ public Pair() {
+ }
+
+ public Pair(S first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public S getFirst() {
+ return first;
+ }
+
+ public void setFirst(S first) {
+ this.first = first;
+ }
+
+ public T getSecond() {
+ return second;
+ }
+
+ public void setSecond(T second) {
+ this.second = second;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/PlaylistAdapter.java b/subsonic-android/src/github/daneren2005/subphonic/util/PlaylistAdapter.java
new file mode 100644
index 00000000..c434479a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/PlaylistAdapter.java
@@ -0,0 +1,99 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.Playlist;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+* @author Sindre Mehus
+* @version $Id$
+*/
+public class PlaylistAdapter extends ArrayAdapter implements SectionIndexer {
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ /**
+ * Note: playlists must be sorted alphabetically.
+ */
+ public PlaylistAdapter(Context context, List playlists) {
+ super(context, R.layout.playlist_list_item, playlists);
+
+ Set sectionSet = new LinkedHashSet(30);
+ List positionList = new ArrayList(30);
+ for (int i = 0; i < playlists.size(); i++) {
+ Playlist playlist = playlists.get(i);
+ if (playlist.getName().length() > 0) {
+ String index = playlist.getName().substring(0, 1).toUpperCase();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+
+ public static class PlaylistComparator implements Comparator {
+ @Override
+ public int compare(Playlist playlist1, Playlist playlist2) {
+ return playlist1.getName().compareToIgnoreCase(playlist2.getName());
+ }
+
+ public static List sort(List playlists) {
+ Collections.sort(playlists, new PlaylistComparator());
+ return playlists;
+ }
+
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/ProgressListener.java b/subsonic-android/src/github/daneren2005/subphonic/util/ProgressListener.java
new file mode 100644
index 00000000..0742b6f6
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/ProgressListener.java
@@ -0,0 +1,27 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface ProgressListener {
+ void updateProgress(String message);
+ void updateProgress(int messageId);
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/SackOfViewsAdapter.java b/subsonic-android/src/github/daneren2005/subphonic/util/SackOfViewsAdapter.java
new file mode 100644
index 00000000..426d14a2
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/SackOfViewsAdapter.java
@@ -0,0 +1,181 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed 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.
+ */
+package github.daneren2005.subphonic.util;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that simply returns row views from a list.
+ *
+ * If you supply a size, you must implement newView(), to
+ * create a required view. The adapter will then cache these
+ * views.
+ *
+ * If you supply a list of views in the constructor, that
+ * list will be used directly. If any elements in the list
+ * are null, then newView() will be called just for those
+ * slots.
+ *
+ * Subclasses may also wish to override areAllItemsEnabled()
+ * (default: false) and isEnabled() (default: false), if some
+ * of their rows should be selectable.
+ *
+ * It is assumed each view is unique, and therefore will not
+ * get recycled.
+ *
+ * Note that this adapter is not designed for long lists. It
+ * is more for screens that should behave like a list. This
+ * is particularly useful if you combine this with other
+ * adapters (e.g., SectionedAdapter) that might have an
+ * arbitrary number of rows, so it all appears seamless.
+ */
+public class SackOfViewsAdapter extends BaseAdapter {
+ private List views = null;
+
+ /**
+ * Constructor creating an empty list of views, but with
+ * a specified count. Subclasses must override newView().
+ */
+ public SackOfViewsAdapter(int count) {
+ super();
+
+ views = new ArrayList(count);
+
+ for (int i = 0; i < count; i++) {
+ views.add(null);
+ }
+ }
+
+ /**
+ * Constructor wrapping a supplied list of views.
+ * Subclasses must override newView() if any of the elements
+ * in the list are null.
+ */
+ public SackOfViewsAdapter(List views) {
+ for (View view : views) {
+ view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ this.views = views;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ return (views.get(position));
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ return (views.size());
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ return (getCount());
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return (position);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ View result = views.get(position);
+
+ if (result == null) {
+ result = newView(position, parent);
+ views.set(position, result);
+ }
+
+ return (result);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ return (position);
+ }
+
+ /**
+ * Create a new View to go into the list at the specified
+ * position.
+ *
+ * @param position Position of the item whose data we want
+ * @param parent ViewGroup containing the returned View
+ */
+ protected View newView(int position, ViewGroup parent) {
+ throw new RuntimeException("You must override newView()!");
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/ShufflePlayBuffer.java b/subsonic-android/src/github/daneren2005/subphonic/util/ShufflePlayBuffer.java
new file mode 100644
index 00000000..c6bbf785
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/ShufflePlayBuffer.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 github.daneren2005.subphonic.util;
+
+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.Context;
+import android.util.Log;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.service.MusicService;
+import github.daneren2005.subphonic.service.MusicServiceFactory;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ShufflePlayBuffer {
+
+ private static final String TAG = ShufflePlayBuffer.class.getSimpleName();
+ private static final int CAPACITY = 50;
+ private static final int REFILL_THRESHOLD = 40;
+
+ private final ScheduledExecutorService executorService;
+ private final List buffer = new ArrayList();
+ private Context context;
+ private int currentServer;
+
+ public ShufflePlayBuffer(Context context) {
+ this.context = context;
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+ executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
+ }
+
+ public List get(int size) {
+ clearBufferIfnecessary();
+
+ List result = new ArrayList(size);
+ synchronized (buffer) {
+ while (!buffer.isEmpty() && result.size() < size) {
+ result.add(buffer.remove(buffer.size() - 1));
+ }
+ }
+ Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining.");
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ }
+
+ private void refill() {
+
+ // Check if active server has changed.
+ clearBufferIfnecessary();
+
+ if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !Util.isOffline(context))) {
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ int n = CAPACITY - buffer.size();
+ MusicDirectory songs = service.getRandomSongs(n, context, null);
+
+ synchronized (buffer) {
+ buffer.addAll(songs.getChildren());
+ Log.i(TAG, "Refilled shuffle play buffer with " + songs.getChildren().size() + " songs.");
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to refill shuffle play buffer.", x);
+ }
+ }
+
+ private void clearBufferIfnecessary() {
+ synchronized (buffer) {
+ if (currentServer != Util.getActiveServer(context)) {
+ currentServer = Util.getActiveServer(context);
+ buffer.clear();
+ }
+ }
+ }
+
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/SilentBackgroundTask.java b/subsonic-android/src/github/daneren2005/subphonic/util/SilentBackgroundTask.java
new file mode 100644
index 00000000..56abbec4
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/SilentBackgroundTask.java
@@ -0,0 +1,67 @@
+/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subphonic.util;
+
+import android.app.Activity;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class SilentBackgroundTask extends BackgroundTask {
+
+ public SilentBackgroundTask(Activity activity) {
+ super(activity);
+ }
+
+ @Override
+ public void execute() {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ done(result);
+ }
+ });
+
+ } catch (final Throwable t) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ @Override
+ public void updateProgress(int messageId) {
+ }
+
+ @Override
+ public void updateProgress(String message) {
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/SimpleServiceBinder.java b/subsonic-android/src/github/daneren2005/subphonic/util/SimpleServiceBinder.java
new file mode 100644
index 00000000..864ca662
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/SimpleServiceBinder.java
@@ -0,0 +1,37 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.os.Binder;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SimpleServiceBinder extends Binder {
+
+ private final S service;
+
+ public SimpleServiceBinder(S service) {
+ this.service = service;
+ }
+
+ public S getService() {
+ return service;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/SongView.java b/subsonic-android/src/github/daneren2005/subphonic/util/SongView.java
new file mode 100644
index 00000000..1d89f0d2
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/SongView.java
@@ -0,0 +1,178 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.CheckedTextView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.service.DownloadService;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import github.daneren2005.subphonic.service.DownloadFile;
+
+import java.io.File;
+import java.util.WeakHashMap;
+
+/**
+ * Used to display songs in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class SongView extends LinearLayout implements Checkable {
+
+ private static final String TAG = SongView.class.getSimpleName();
+ private static final WeakHashMap INSTANCES = new WeakHashMap();
+ private static Handler handler;
+
+ private CheckedTextView checkedTextView;
+ private TextView titleTextView;
+ private TextView artistTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private MusicDirectory.Entry song;
+
+ public SongView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true);
+
+ checkedTextView = (CheckedTextView) findViewById(R.id.song_check);
+ titleTextView = (TextView) findViewById(R.id.song_title);
+ artistTextView = (TextView) findViewById(R.id.song_artist);
+ durationTextView = (TextView) findViewById(R.id.song_duration);
+ statusTextView = (TextView) findViewById(R.id.song_status);
+
+ INSTANCES.put(this, null);
+ int instanceCount = INSTANCES.size();
+ if (instanceCount > 50) {
+ Log.w(TAG, instanceCount + " live SongView instances");
+ }
+ startUpdater();
+ }
+
+ public void setSong(MusicDirectory.Entry song, boolean checkable) {
+ this.song = song;
+ StringBuilder artist = new StringBuilder(40);
+
+ String bitRate = null;
+ if (song.getBitRate() != null) {
+ bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate());
+ }
+
+ String fileFormat = null;
+ if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) {
+ fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix());
+ } else {
+ fileFormat = song.getSuffix();
+ }
+
+ artist.append(song.getArtist()).append(" (")
+ .append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat))
+ .append(")");
+
+ titleTextView.setText(song.getTitle());
+ artistTextView.setText(artist);
+ durationTextView.setText(Util.formatDuration(song.getDuration()));
+ checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE);
+
+ update();
+ }
+
+ private void update() {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService == null) {
+ return;
+ }
+
+ DownloadFile downloadFile = downloadService.forSong(song);
+ File completeFile = downloadFile.getCompleteFile();
+ File partialFile = downloadFile.getPartialFile();
+
+ int leftImage = 0;
+ int rightImage = 0;
+
+ if (completeFile.exists()) {
+ leftImage = downloadFile.isSaved() ? R.drawable.saved : R.drawable.downloaded;
+ }
+
+ if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) {
+ statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext()));
+ rightImage = R.drawable.downloading;
+ } else {
+ statusTextView.setText(null);
+ }
+ statusTextView.setCompoundDrawablesWithIntrinsicBounds(leftImage, 0, rightImage, 0);
+
+ boolean playing = downloadService.getCurrentPlaying() == downloadFile;
+ if (playing) {
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.stat_notify_playing, 0, 0, 0);
+ } else {
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+
+ private static synchronized void startUpdater() {
+ if (handler != null) {
+ return;
+ }
+
+ handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ updateAll();
+ handler.postDelayed(this, 1000L);
+ }
+ };
+ handler.postDelayed(runnable, 1000L);
+ }
+
+ private static void updateAll() {
+ try {
+ for (SongView view : INSTANCES.keySet()) {
+ if (view.isShown()) {
+ view.update();
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean b) {
+ checkedTextView.setChecked(b);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return checkedTextView.isChecked();
+ }
+
+ @Override
+ public void toggle() {
+ checkedTextView.toggle();
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/TabActivityBackgroundTask.java b/subsonic-android/src/github/daneren2005/subphonic/util/TabActivityBackgroundTask.java
new file mode 100644
index 00000000..62066a91
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/TabActivityBackgroundTask.java
@@ -0,0 +1,67 @@
+package github.daneren2005.subphonic.util;
+
+import github.daneren2005.subphonic.activity.SubsonicTabActivity;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class TabActivityBackgroundTask extends BackgroundTask {
+
+ private final SubsonicTabActivity tabActivity;
+
+ public TabActivityBackgroundTask(SubsonicTabActivity activity) {
+ super(activity);
+ tabActivity = activity;
+ }
+
+ @Override
+ public void execute() {
+ tabActivity.setProgressVisible(true);
+
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabActivity.setProgressVisible(false);
+ done(result);
+ }
+ });
+ } catch (final Throwable t) {
+ if (isCancelled()) {
+ return;
+ }
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabActivity.setProgressVisible(false);
+ error(t);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ private boolean isCancelled() {
+ return tabActivity.isDestroyed();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabActivity.updateProgress(message);
+ }
+ });
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/TimeLimitedCache.java b/subsonic-android/src/github/daneren2005/subphonic/util/TimeLimitedCache.java
new file mode 100644
index 00000000..4d1220f6
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/TimeLimitedCache.java
@@ -0,0 +1,55 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class TimeLimitedCache {
+
+ private SoftReference value;
+ private final long ttlMillis;
+ private long expires;
+
+ public TimeLimitedCache(long ttl, TimeUnit timeUnit) {
+ this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit);
+ }
+
+ public T get() {
+ return System.currentTimeMillis() < expires ? value.get() : null;
+ }
+
+ public void set(T value) {
+ set(value, ttlMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void set(T value, long ttl, TimeUnit timeUnit) {
+ this.value = new SoftReference(value);
+ expires = System.currentTimeMillis() + timeUnit.toMillis(ttl);
+ }
+
+ public void clear() {
+ expires = 0L;
+ value = null;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subphonic/util/Util.java b/subsonic-android/src/github/daneren2005/subphonic/util/Util.java
new file mode 100644
index 00000000..c8711a77
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subphonic/util/Util.java
@@ -0,0 +1,829 @@
+/*
+ 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 github.daneren2005.subphonic.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Environment;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+import android.widget.Toast;
+import github.daneren2005.subphonic.R;
+import github.daneren2005.subphonic.activity.DownloadActivity;
+import github.daneren2005.subphonic.domain.MusicDirectory;
+import github.daneren2005.subphonic.domain.PlayerState;
+import github.daneren2005.subphonic.domain.RepeatMode;
+import github.daneren2005.subphonic.domain.Version;
+import github.daneren2005.subphonic.provider.SubsonicAppWidgetProvider1;
+import github.daneren2005.subphonic.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.subphonic.service.DownloadServiceImpl;
+import org.apache.http.HttpEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.security.MessageDigest;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Util {
+
+ private static final String TAG = Util.class.getSimpleName();
+
+ private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB");
+ private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB");
+ private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB");
+
+ private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat BYTE_LOCALIZED_FORMAT = null;
+
+ public static final String EVENT_META_CHANGED = "github.daneren2005.subphonic.EVENT_META_CHANGED";
+ public static final String EVENT_PLAYSTATE_CHANGED = "github.daneren2005.subphonic.EVENT_PLAYSTATE_CHANGED";
+
+ private static final Map SERVER_REST_VERSIONS = new ConcurrentHashMap();
+
+ // Used by hexEncode()
+ private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ private final static Pair NOTIFICATION_TEXT_COLORS = new Pair();
+ private static Toast toast;
+
+ private Util() {
+ }
+
+ public static boolean isOffline(Context context) {
+ return getActiveServer(context) == 0;
+ }
+
+ public static boolean isScreenLitOnDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false);
+ }
+
+ public static RepeatMode getRepeatMode(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name()));
+ }
+
+ public static void setRepeatMode(Context context, RepeatMode repeatMode) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name());
+ editor.commit();
+ }
+
+ public static boolean isScrobblingEnabled(Context context) {
+ if (isOffline(context)) {
+ return false;
+ }
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false);
+ }
+
+ public static void setActiveServer(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
+ editor.commit();
+ }
+
+ public static int getActiveServer(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ }
+
+ public static String getServerName(Context context, int instance) {
+ if (instance == 0) {
+ return context.getResources().getString(R.string.main_offline);
+ }
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+
+ public static void setServerRestVersion(Context context, Version version) {
+ SERVER_REST_VERSIONS.put(getActiveServer(context), version);
+ }
+
+ public static Version getServerRestVersion(Context context) {
+ return SERVER_REST_VERSIONS.get(getActiveServer(context));
+ }
+
+ public static void setSelectedMusicFolderId(Context context, String musicFolderId) {
+ int instance = getActiveServer(context);
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
+ editor.commit();
+ }
+
+ public static String getSelectedMusicFolderId(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = getActiveServer(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ }
+
+ public static String getTheme(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_THEME, null);
+ }
+
+ public static int getMaxBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getPreloadCount(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int preloadCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_PRELOAD_COUNT, "-1"));
+ return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount;
+ }
+
+ public static int getCacheSizeMB(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1"));
+ return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize;
+ }
+
+ public static String getRestUrl(Context context, String method) {
+ StringBuilder builder = new StringBuilder();
+
+ SharedPreferences prefs = getPreferences(context);
+
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+
+ // Slightly obfuscate password
+ password = "enc:" + Util.utf8HexEncode(password);
+
+ builder.append(serverUrl);
+ if (builder.charAt(builder.length() - 1) != '/') {
+ builder.append("/");
+ }
+ builder.append("rest/").append(method).append(".view");
+ builder.append("?u=").append(username);
+ builder.append("&p=").append(password);
+ builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION);
+ builder.append("&c=").append(Constants.REST_CLIENT_ID);
+
+ return builder.toString();
+ }
+
+ public static SharedPreferences getPreferences(Context context) {
+ return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0);
+ }
+
+ public static String getContentType(HttpEntity entity) {
+ if (entity == null || entity.getContentType() == null) {
+ return null;
+ }
+ return entity.getContentType().getValue();
+ }
+
+ public static int getRemainingTrialDays(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L);
+
+ if (installTime == 0L) {
+ installTime = System.currentTimeMillis();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime);
+ editor.commit();
+ }
+
+ long now = System.currentTimeMillis();
+ long millisPerDay = 24L * 60L * 60L * 1000L;
+ int daysSinceInstall = (int) ((now - installTime) / millisPerDay);
+ return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall);
+ }
+
+ /**
+ * Get the contents of an InputStream as a byte[].
+ *
+ * This method buffers the input internally, so there is no need to use a
+ * BufferedInputStream.
+ *
+ * @param input the InputStream to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ public static long copy(InputStream input, OutputStream output)
+ throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ long count = 0;
+ int n;
+ while (-1 != (n = input.read(buffer))) {
+ output.write(buffer, 0, n);
+ count += n;
+ }
+ return count;
+ }
+
+ public static void atomicCopy(File from, File to) throws IOException {
+ FileInputStream in = null;
+ FileOutputStream out = null;
+ File tmp = null;
+ try {
+ tmp = new File(to.getPath() + ".tmp");
+ in = new FileInputStream(from);
+ out = new FileOutputStream(tmp);
+ in.getChannel().transferTo(0, from.length(), out.getChannel());
+ out.close();
+ if (!tmp.renameTo(to)) {
+ throw new IOException("Failed to rename " + tmp + " to " + to);
+ }
+ Log.i(TAG, "Copied " + from + " to " + to);
+ } catch (IOException x) {
+ close(out);
+ delete(to);
+ throw x;
+ } finally {
+ close(in);
+ close(out);
+ delete(tmp);
+ }
+ }
+
+ public static void close(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static boolean delete(File file) {
+ if (file != null && file.exists()) {
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete file " + file);
+ return false;
+ }
+ Log.i(TAG, "Deleted file " + file);
+ }
+ return true;
+ }
+
+ public static void toast(Context context, int messageId) {
+ toast(context, messageId, true);
+ }
+
+ public static void toast(Context context, int messageId, boolean shortDuration) {
+ toast(context, context.getString(messageId), shortDuration);
+ }
+
+ public static void toast(Context context, String message) {
+ toast(context, message, true);
+ }
+
+ public static void toast(Context context, String message, boolean shortDuration) {
+ if (toast == null) {
+ toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ } else {
+ toast.setText(message);
+ toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ }
+ toast.show();
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ *
+ *
format(918) returns "918 B".
+ *
format(98765) returns "96 KB".
+ *
format(1238476) returns "1.2 MB".
+ *
+ * This method assumes that 1 KB is 1024 bytes.
+ * To get a localized string, please use formatLocalizedBytes instead.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatBytes(long byteCount) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT;
+ return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ NumberFormat megaByteFormat = MEGA_BYTE_FORMAT;
+ return megaByteFormat.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ NumberFormat kiloByteFormat = KILO_BYTE_FORMAT;
+ return kiloByteFormat.format((double) byteCount / 1024);
+ }
+
+ return byteCount + " B";
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ *
+ *
format(918) returns "918 B".
+ *
format(98765) returns "96 KB".
+ *
format(1238476) returns "1.2 MB".
+ *
+ * This method assumes that 1 KB is 1024 bytes.
+ * This version of the method returns a localized string.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatLocalizedBytes(long byteCount, Context context) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
+ GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte));
+ }
+
+ return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
+ MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte));
+ }
+
+ return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ if (KILO_BYTE_LOCALIZED_FORMAT == null) {
+ KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte));
+ }
+
+ return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024);
+ }
+
+ if (BYTE_LOCALIZED_FORMAT == null) {
+ BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte));
+ }
+
+ return BYTE_LOCALIZED_FORMAT.format((double) byteCount);
+ }
+
+ public static String formatDuration(Integer seconds) {
+ if (seconds == null) {
+ return null;
+ }
+
+ int minutes = seconds / 60;
+ int secs = seconds % 60;
+
+ StringBuilder builder = new StringBuilder(6);
+ builder.append(minutes).append(":");
+ if (secs < 10) {
+ builder.append("0");
+ }
+ builder.append(secs);
+ return builder.toString();
+ }
+
+ public static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 == null || object2 == null) {
+ return false;
+ }
+ return object1.equals(object2);
+
+ }
+
+ /**
+ * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to encode.
+ * @return The encoded string.
+ */
+ public static String utf8HexEncode(String s) {
+ if (s == null) {
+ return null;
+ }
+ byte[] utf8;
+ try {
+ utf8 = s.getBytes(Constants.UTF_8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ return hexEncode(utf8);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data Bytes to convert to hexadecimal characters.
+ * @return A string containing hexadecimal characters.
+ */
+ public static String hexEncode(byte[] data) {
+ int length = data.length;
+ char[] out = new char[length << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < length; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param s Data to digest.
+ * @return MD5 digest as a hex string.
+ */
+ public static String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return hexEncode(md5.digest(s.getBytes(Constants.UTF_8)));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ public static boolean isNetworkConnected(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+
+ boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ boolean wifiRequired = isWifiRequiredForDownload(context);
+
+ return connected && (!wifiRequired || wifiConnected);
+ }
+
+ public static boolean isExternalStoragePresent() {
+ return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ }
+
+ private static boolean isWifiRequiredForDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false);
+ }
+
+ public static void info(Context context, int titleId, int messageId) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId);
+ }
+
+ private static void showDialog(Context context, int icon, int titleId, int messageId) {
+ new AlertDialog.Builder(context)
+ .setIcon(icon)
+ .setTitle(titleId)
+ .setMessage(messageId)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+ }
+
+ public static void showPlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler, MusicDirectory.Entry song) {
+
+ // Use the same text for the ticker and the expanded notification
+ String title = song.getTitle();
+ String text = song.getArtist();
+
+ // Set the icon, scrolling text and timestamp
+ final Notification notification = new Notification(R.drawable.stat_notify_playing, title, System.currentTimeMillis());
+ notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+
+ RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.notification);
+
+ // Set the album art.
+ try {
+ int size = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size);
+ if (bitmap == null) {
+ // set default album art
+ contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ } else {
+ contentView.setImageViewBitmap(R.id.notification_image, bitmap);
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to get notification cover art", x);
+ contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ }
+
+ // set the text for the notifications
+ contentView.setTextViewText(R.id.notification_title, title);
+ contentView.setTextViewText(R.id.notification_artist, text);
+
+ Pair colors = getNotificationTextColors(context);
+ if (colors.getFirst() != null) {
+ contentView.setTextColor(R.id.notification_title, colors.getFirst());
+ }
+ if (colors.getSecond() != null) {
+ contentView.setTextColor(R.id.notification_artist, colors.getSecond());
+ }
+
+ notification.contentView = contentView;
+
+ Intent notificationIntent = new Intent(context, DownloadActivity.class);
+ notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
+
+ // Send the notification and put the service in the foreground.
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ startForeground(downloadService, Constants.NOTIFICATION_ID_PLAYING, notification);
+ }
+ });
+
+ // Update widget
+ SubsonicAppWidgetProvider1.getInstance().notifyChange(context, downloadService, true);
+ }
+
+ public static void hidePlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler) {
+
+ // Remove notification and remove the service from the foreground
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ stopForeground(downloadService, true);
+ }
+ });
+
+ // Update widget
+ SubsonicAppWidgetProvider1.getInstance().notifyChange(context, downloadService, false);
+ }
+
+ public static void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException x) {
+ Log.w(TAG, "Interrupted from sleep.", x);
+ }
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Class extends Activity> newActivitiy) {
+ startActivityWithoutTransition(currentActivity, new Intent(currentActivity, newActivitiy));
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Intent intent) {
+ currentActivity.startActivity(intent);
+ disablePendingTransition(currentActivity);
+ }
+
+ public static void disablePendingTransition(Activity activity) {
+
+ // Activity.overridePendingTransition() was introduced in Android 2.0. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Method method = Activity.class.getMethod("overridePendingTransition", int.class, int.class);
+ method.invoke(activity, 0, 0);
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static Drawable createDrawableFromBitmap(Context context, Bitmap bitmap) {
+ // BitmapDrawable(Resources, Bitmap) was introduced in Android 1.6. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Constructor constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class);
+ return constructor.newInstance(context.getResources(), bitmap);
+ } catch (Throwable x) {
+ return new BitmapDrawable(bitmap);
+ }
+ }
+
+ public static void registerMediaButtonEventReceiver(Context context) {
+
+ // Only do it if enabled in the settings.
+ SharedPreferences prefs = getPreferences(context);
+ boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true);
+
+ if (enabled) {
+
+ // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+ }
+
+ public static void unregisterMediaButtonEventReceiver(Context context) {
+ // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+
+ private static void startForeground(Service service, int notificationId, Notification notification) {
+ // Service.startForeground() was introduced in Android 2.0.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ Method method = Service.class.getMethod("startForeground", int.class, Notification.class);
+ method.invoke(service, notificationId, notification);
+ Log.i(TAG, "Successfully invoked Service.startForeground()");
+ } catch (Throwable x) {
+ NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(Constants.NOTIFICATION_ID_PLAYING, notification);
+ Log.i(TAG, "Service.startForeground() not available. Using work-around.");
+ }
+ }
+
+ private static void stopForeground(Service service, boolean removeNotification) {
+ // Service.stopForeground() was introduced in Android 2.0.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ Method method = Service.class.getMethod("stopForeground", boolean.class);
+ method.invoke(service, removeNotification);
+ Log.i(TAG, "Successfully invoked Service.stopForeground()");
+ } catch (Throwable x) {
+ NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(Constants.NOTIFICATION_ID_PLAYING);
+ Log.i(TAG, "Service.stopForeground() not available. Using work-around.");
+ }
+ }
+
+ /**
+ *
Broadcasts the given song info as the new song being played.