diff options
author | Scott Jackson <daneren2005@gmail.com> | 2012-10-01 20:41:50 -0700 |
---|---|---|
committer | Scott Jackson <daneren2005@gmail.com> | 2012-10-01 20:41:50 -0700 |
commit | cb5389c471bbf8b2952abf77db1bdc303f577718 (patch) | |
tree | 21d8952defa73bb3cb87a0c79d2a54a40365224e /subsonic-android | |
parent | 56458b12e19310b51d228c601012bf4b65a29f17 (diff) | |
parent | 57b20385fd36584efa0ffb3d9ef4761b463287cf (diff) | |
download | dsub-cb5389c471bbf8b2952abf77db1bdc303f577718.tar.gz dsub-cb5389c471bbf8b2952abf77db1bdc303f577718.tar.bz2 dsub-cb5389c471bbf8b2952abf77db1bdc303f577718.zip |
Merge star support
Diffstat (limited to 'subsonic-android')
24 files changed, 797 insertions, 433 deletions
diff --git a/subsonic-android/project.properties b/subsonic-android/project.properties index 0b3adc12..a39542f9 100644 --- a/subsonic-android/project.properties +++ b/subsonic-android/project.properties @@ -9,4 +9,4 @@ # Project target. target=android-15 -android.library.reference.1=../../ActionBarSherlock +android.library.reference.1=../ActionBarSherlock/library diff --git a/subsonic-android/res/layout-land/download.xml b/subsonic-android/res/layout-land/download.xml index 01a9b68d..b0303e52 100644 --- a/subsonic-android/res/layout-land/download.xml +++ b/subsonic-android/res/layout-land/download.xml @@ -14,15 +14,15 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_alignParentRight="true"
- android:background="@color/mediaControlBackground"
- >
+ android:background="@color/mediaControlBackground">
<LinearLayout
+ android:id="@+id/download_play_controls_layout"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
- android:layout_alignParentLeft="true">
+ android:layout_centerHorizontal="true">
<ImageButton
android:id="@+id/download_previous"
@@ -87,7 +87,6 @@ android:id="@+id/download_song_title"
android:layout_width="150dip"
android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
android:layout_above="@+id/download_status"
android:layout_centerHorizontal="true"
android:layout_marginLeft="12dip"
@@ -99,6 +98,7 @@ android:textColor="@color/mediaControlForeground"/>
<LinearLayout
+ android:id="@+id/download_other_controls_layout"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -111,6 +111,7 @@ android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_below="@+id/download_play_controls_layout"
android:padding="9dip"/>
<ImageButton
android:id="@+id/download_repeat"
@@ -120,6 +121,13 @@ android:layout_height="wrap_content"
android:padding="9dip"/>
<ImageButton
+ android:id="@+id/download_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="@null"
+ android:src="@android:drawable/star_big_off"/>
+ <ImageButton
android:id="@+id/download_toggle_list"
android:src="@drawable/media_toggle_list"
android:background="@android:color/transparent"
diff --git a/subsonic-android/res/layout-port/download.xml b/subsonic-android/res/layout-port/download.xml index e2a198a2..e6a2358a 100644 --- a/subsonic-android/res/layout-port/download.xml +++ b/subsonic-android/res/layout-port/download.xml @@ -72,6 +72,15 @@ android:paddingTop="12dip"
android:paddingRight="12dip"
android:paddingBottom="12dip"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignRight="@+id/download_repeat"
+ android:layout_below="@+id/download_jukebox"
+ android:background="@null"
+ android:src="@android:drawable/star_big_off"/>
<ImageView
android:id="@+id/download_album_art_image"
diff --git a/subsonic-android/res/layout/album_list_item.xml b/subsonic-android/res/layout/album_list_item.xml index 2e74390f..88f3cf8b 100644 --- a/subsonic-android/res/layout/album_list_item.xml +++ b/subsonic-android/res/layout/album_list_item.xml @@ -36,6 +36,15 @@ android:singleLine="true"/>
</LinearLayout>
+
+ <ImageButton
+ android:id="@+id/album_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:background="@null"
+ android:focusable="false"
+ android:src="@android:drawable/star_big_off" />
<ImageView
android:src="@drawable/list_item_more"
diff --git a/subsonic-android/res/layout/main_buttons.xml b/subsonic-android/res/layout/main_buttons.xml index 655854be..48d5b53f 100644 --- a/subsonic-android/res/layout/main_buttons.xml +++ b/subsonic-android/res/layout/main_buttons.xml @@ -104,6 +104,18 @@ android:paddingRight="6dip"
android:minHeight="50dip"/>
<TextView
+ android:id="@+id/main_albums_starred"
+ android:text="@string/main.albums_starred"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
android:id="@+id/main_albums_random"
android:text="@string/main.albums_random"
android:drawableRight="@drawable/list_item_more"
diff --git a/subsonic-android/res/layout/song_list_item.xml b/subsonic-android/res/layout/song_list_item.xml index f8d518ad..b2029ccd 100644 --- a/subsonic-android/res/layout/song_list_item.xml +++ b/subsonic-android/res/layout/song_list_item.xml @@ -73,4 +73,14 @@ </LinearLayout>
</LinearLayout>
+
+ <ImageButton
+ android:id="@+id/song_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:background="@null"
+ android:focusable="false"
+ android:src="@android:drawable/btn_star_big_off" />
+
</LinearLayout>
diff --git a/subsonic-android/res/values/strings.xml b/subsonic-android/res/values/strings.xml index 5ff31e79..96f1fc93 100644 --- a/subsonic-android/res/values/strings.xml +++ b/subsonic-android/res/values/strings.xml @@ -31,6 +31,7 @@ <string name="main.albums_recent">Recently played</string>
<string name="main.albums_frequent">Most played</string>
<string name="main.albums_highest">Top rated</string>
+ <string name="main.albums_starred">Starred</string>
<string name="main.albums_random">Random</string>
<string name="menu.search">Search</string>
@@ -116,6 +117,8 @@ <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
<string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
<string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users > Settings</b> on your Subsonic server.</string>
+
+ <string name="starring_content_error">Failed to update \"%s\", please try later.</string>
<string name="song_details.all">%1$s %2$s</string>
<string name="song_details.kbps">%d kbps</string>
diff --git a/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java index 1e568c03..61065c18 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/DownloadActivity.java @@ -42,6 +42,7 @@ import android.view.GestureDetector.OnGestureListener; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.AnimationUtils; @@ -106,6 +107,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi private Button visualizerButton; private Button jukeboxButton; private View toggleListButton; + private ImageButton starButton; private ScheduledExecutorService executorService; private DownloadFile currentPlaying; private long currentRevision; @@ -157,6 +159,19 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi LinearLayout visualizerViewLayout = (LinearLayout) findViewById(R.id.download_visualizer_view_layout); toggleListButton = findViewById(R.id.download_toggle_list); + + starButton = (ImageButton) findViewById(R.id.download_star); + starButton.setVisibility(Util.isOffline(this) ? View.GONE : View.VISIBLE); + starButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + DownloadFile currentDownload = getDownloadService().getCurrentPlaying(); + if (currentDownload != null) { + MusicDirectory.Entry currentSong = currentDownload.getSong(); + toggleStarredInBackground(currentSong, starButton); + } + } + }); View.OnTouchListener touchListener = new View.OnTouchListener() { @Override @@ -714,11 +729,13 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi albumTextView.setText(song.getAlbum()); artistTextView.setText(song.getArtist()); getImageLoader().loadImage(albumArtImageView, song, true, true); + starButton.setImageResource(song.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off); } else { songTitleTextView.setText(null); albumTextView.setText(null); artistTextView.setText(null); getImageLoader().loadImage(albumArtImageView, null, true, false); + starButton.setImageResource(android.R.drawable.btn_star_big_off); } } diff --git a/subsonic-android/src/github/daneren2005/dsub/activity/MainActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/MainActivity.java index 2fab77e7..23b1ba0d 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/MainActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/MainActivity.java @@ -79,6 +79,7 @@ public class MainActivity extends SubsonicTabActivity { 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 albumsStarredButton = buttons.findViewById(R.id.main_albums_starred); final View dummyView = findViewById(R.id.main_dummy); @@ -92,7 +93,7 @@ public class MainActivity extends SubsonicTabActivity { adapter.addViews(Arrays.asList(serverButton), true); if (!Util.isOffline(this)) { adapter.addView(albumsTitle, false); - adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton, albumsHighestButton, albumsRecentButton, albumsFrequentButton), true); + adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton, albumsHighestButton, albumsStarredButton, albumsRecentButton, albumsFrequentButton), true); } list.setAdapter(adapter); registerForContextMenu(dummyView); @@ -108,6 +109,8 @@ public class MainActivity extends SubsonicTabActivity { showAlbumList("random"); } else if (view == albumsHighestButton) { showAlbumList("highest"); + } else if (view == albumsStarredButton) { + showAlbumList("starred"); } else if (view == albumsRecentButton) { showAlbumList("recent"); } else if (view == albumsFrequentButton) { diff --git a/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java index 3d029db3..9a600d59 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/SelectAlbumActivity.java @@ -314,7 +314,13 @@ public class SelectAlbumActivity extends SubsonicTabActivity { new LoadTask() { @Override protected MusicDirectory load(MusicService service) throws Exception { - return service.getAlbumList(albumListType, size, offset, SelectAlbumActivity.this, this); + MusicDirectory result; + if ("starred".equals(albumListType)) { + result = service.getStarredList(SelectAlbumActivity.this, this); + } else { + result = service.getAlbumList(albumListType, size, offset, SelectAlbumActivity.this, this); + } + return result; } @Override diff --git a/subsonic-android/src/github/daneren2005/dsub/activity/SubsonicTabActivity.java b/subsonic-android/src/github/daneren2005/dsub/activity/SubsonicTabActivity.java index c7ef57b5..32b6c550 100644 --- a/subsonic-android/src/github/daneren2005/dsub/activity/SubsonicTabActivity.java +++ b/subsonic-android/src/github/daneren2005/dsub/activity/SubsonicTabActivity.java @@ -1,348 +1,392 @@ -/* - 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 <http://www.gnu.org/licenses/>. - - Copyright 2009 (C) Sindre Mehus - */ -package github.daneren2005.dsub.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.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.View; -import android.widget.TextView; -import github.daneren2005.dsub.R; -import com.actionbarsherlock.app.SherlockActivity; -import github.daneren2005.dsub.domain.MusicDirectory; -import github.daneren2005.dsub.service.DownloadService; -import github.daneren2005.dsub.service.DownloadServiceImpl; -import github.daneren2005.dsub.service.MusicService; -import github.daneren2005.dsub.service.MusicServiceFactory; -import github.daneren2005.dsub.util.ImageLoader; -import github.daneren2005.dsub.util.ModalBackgroundTask; -import github.daneren2005.dsub.util.Util; - -/** - * @author Sindre Mehus - */ -public class SubsonicTabActivity extends SherlockActivity { - - private static final String TAG = SubsonicTabActivity.class.getSimpleName(); - private static ImageLoader IMAGE_LOADER; - private String theme; - - private boolean destroyed; - private View homeButton; - private View musicButton; - private View playlistButton; - private View nowPlayingButton; - - @Override - protected void onCreate(Bundle bundle) { - setUncaughtExceptionHandler(); - applyTheme(); - super.onCreate(bundle); - 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); - } - }); - - 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 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); - - // Make sure to update theme - if (theme != null && !theme.equals(Util.getTheme(this))) { - restart(); - } - } - - @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); - } - - protected void restart() { - Intent intent = new Intent(this, this.getClass()); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtras(getIntent()); - Util.startActivityWithoutTransition(this, intent); - } - - @Override - public void finish() { - super.finish(); - Util.disablePendingTransition(this); - } - - private void applyTheme() { - theme = Util.getTheme(this); - if ("dark".equals(theme)) { - setTheme(R.style.Theme_DSub_Dark); - } else if ("light".equals(theme)) { - setTheme(R.style.Theme_DSub_Light); - } else if ("dark_fullscreen".equals(theme)) { - setTheme(R.style.Theme_DSub_Dark_Fullscreen); - } else if ("light_fullscreen".equals(theme)) { - setTheme(R.style.Theme_DSub_Light_Fullscreen); - }else { - setTheme(R.style.Theme_DSub_Light); - } - } - - public boolean isDestroyed() { - return destroyed; - } - - private void updateButtonVisibility() { - int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE; - } - - 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, final boolean shuffle, final boolean background) { - downloadRecursively(id, "", true, save, append, autoplay, shuffle, background); - } - protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) { - downloadRecursively(id, name, false, save, append, autoplay, shuffle, background); - } - protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) { - ModalBackgroundTask<List<MusicDirectory.Entry>> task = new ModalBackgroundTask<List<MusicDirectory.Entry>>(this, false) { - private static final int MAX_SONGS = 500; - - @Override - protected List<MusicDirectory.Entry> doInBackground() throws Throwable { - MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this); - MusicDirectory root; - if(isDirectory) - root = musicService.getMusicDirectory(id, false, SubsonicTabActivity.this, this); - else - root = musicService.getPlaylist(id, name, SubsonicTabActivity.this, this); - List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>(); - getSongsRecursively(root, songs); - return songs; - } - - private void getSongsRecursively(MusicDirectory parent, List<MusicDirectory.Entry> 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<MusicDirectory.Entry> songs) { - DownloadService downloadService = getDownloadService(); - if (!songs.isEmpty() && downloadService != null) { - if (!append) { - downloadService.clear(); - } - warnIfNetworkOrStorageUnavailable(); - if(!background) { - downloadService.download(songs, save, autoplay, false, shuffle); - Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class); - } - else { - downloadService.downloadBackground(songs, save); - } - } - } - }; - - 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.dsub", 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); - } - - } - } - } -} - +/*
+ 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 <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.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.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.View;
+import android.view.Window;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import com.actionbarsherlock.app.SherlockActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.ModalBackgroundTask;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SubsonicTabActivity extends SherlockActivity {
+
+ private static final String TAG = SubsonicTabActivity.class.getSimpleName();
+ private static ImageLoader IMAGE_LOADER;
+ private String theme;
+
+ private boolean destroyed;
+ private View homeButton;
+ private View musicButton;
+ private View playlistButton;
+ private View nowPlayingButton;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ setUncaughtExceptionHandler();
+ applyTheme();
+ super.onCreate(bundle);
+ 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);
+ }
+ });
+
+ 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 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);
+
+ // Make sure to update theme
+ if (theme != null && !theme.equals(Util.getTheme(this))) {
+ restart();
+ }
+ }
+
+ @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);
+ }
+
+ protected void restart() {
+ Intent intent = new Intent(this, this.getClass());
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtras(getIntent());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ Util.disablePendingTransition(this);
+ }
+
+ private void applyTheme() {
+ theme = Util.getTheme(this);
+ if ("dark".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Dark);
+ } else if ("light".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Light);
+ } else if ("dark_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Dark_Fullscreen);
+ } else if ("light_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Light_Fullscreen);
+ }else {
+ setTheme(R.style.Theme_DSub_Light);
+ }
+ }
+
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ private void updateButtonVisibility() {
+ int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE;
+ }
+
+ public void toggleStarredInBackground(final MusicDirectory.Entry entry, final ImageButton button) {
+
+ final boolean starred = !entry.isStarred();
+
+ button.setImageResource(starred ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ entry.setStarred(starred);
+
+ // Util.toast(SubsonicTabActivity.this, getResources().getString(R.string.starring_content, entry.getTitle()));
+ new SilentBackgroundTask<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
+ musicService.setStarred(entry.getId(), starred, SubsonicTabActivity.this, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // Util.toast(SubsonicTabActivity.this, getResources().getString(R.string.starring_content_done, entry.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ button.setImageResource(!starred ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ entry.setStarred(!starred);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(SubsonicTabActivity.this, msg, false);
+ }
+ }.execute();
+ }
+
+ 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, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, "", true, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, name, false, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ ModalBackgroundTask<List<MusicDirectory.Entry>> task = new ModalBackgroundTask<List<MusicDirectory.Entry>>(this, false) {
+ private static final int MAX_SONGS = 500;
+
+ @Override
+ protected List<MusicDirectory.Entry> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
+ MusicDirectory root;
+ if(isDirectory)
+ root = musicService.getMusicDirectory(id, false, SubsonicTabActivity.this, this);
+ else
+ root = musicService.getPlaylist(id, name, SubsonicTabActivity.this, this);
+ List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>();
+ getSongsRecursively(root, songs);
+ return songs;
+ }
+
+ private void getSongsRecursively(MusicDirectory parent, List<MusicDirectory.Entry> 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<MusicDirectory.Entry> songs) {
+ DownloadService downloadService = getDownloadService();
+ if (!songs.isEmpty() && downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ warnIfNetworkOrStorageUnavailable();
+ if(!background) {
+ downloadService.download(songs, save, autoplay, false, shuffle);
+ Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
+ }
+ else {
+ downloadService.downloadBackground(songs, save);
+ }
+ }
+ }
+ };
+
+ 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.dsub", 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/dsub/domain/MusicDirectory.java b/subsonic-android/src/github/daneren2005/dsub/domain/MusicDirectory.java index 420ce813..da0d8cbc 100644 --- a/subsonic-android/src/github/daneren2005/dsub/domain/MusicDirectory.java +++ b/subsonic-android/src/github/daneren2005/dsub/domain/MusicDirectory.java @@ -80,6 +80,7 @@ public class MusicDirectory { private Integer bitRate; private String path; private boolean video; + private boolean starred; public String getId() { return id; @@ -232,6 +233,14 @@ public class MusicDirectory { public void setVideo(boolean video) { this.video = video; } + + public boolean isStarred() { + return starred; + } + + public void setStarred(boolean starred) { + this.starred = starred; + } @Override public boolean equals(Object o) { diff --git a/subsonic-android/src/github/daneren2005/dsub/service/CachedMusicService.java b/subsonic-android/src/github/daneren2005/dsub/service/CachedMusicService.java index 094c959f..832b2037 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/CachedMusicService.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/CachedMusicService.java @@ -164,6 +164,11 @@ public class CachedMusicService implements MusicService { } @Override + public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception { + return musicService.getStarredList(context, progressListener); + } + + @Override public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { return musicService.getRandomSongs(size, context, progressListener); } @@ -222,6 +227,11 @@ public class CachedMusicService implements MusicService { public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { return musicService.setJukeboxGain(gain, context, progressListener); } + + @Override + public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception { + musicService.setStarred(id, starred, context, progressListener); + } private void checkSettingsChanged(Context context) { String newUrl = Util.getRestUrl(context, null); diff --git a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java index 64e71850..7ba54832 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/DownloadServiceImpl.java @@ -18,22 +18,14 @@ */ package github.daneren2005.dsub.service; -import android.annotation.TargetApi; -import android.app.PendingIntent; -import android.app.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.os.Build; -import android.os.Handler; -import android.os.IBinder; -import android.os.PowerManager; -import android.util.Log; +import static github.daneren2005.dsub.domain.PlayerState.COMPLETED; +import static github.daneren2005.dsub.domain.PlayerState.DOWNLOADING; +import static github.daneren2005.dsub.domain.PlayerState.IDLE; +import static github.daneren2005.dsub.domain.PlayerState.PAUSED; +import static github.daneren2005.dsub.domain.PlayerState.PREPARED; +import static github.daneren2005.dsub.domain.PlayerState.PREPARING; +import static github.daneren2005.dsub.domain.PlayerState.STARTED; +import static github.daneren2005.dsub.domain.PlayerState.STOPPED; import github.daneren2005.dsub.audiofx.EqualizerController; import github.daneren2005.dsub.audiofx.VisualizerController; import github.daneren2005.dsub.domain.MusicDirectory; @@ -41,10 +33,12 @@ import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RepeatMode; import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.LRUCache; import github.daneren2005.dsub.util.ShufflePlayBuffer; import github.daneren2005.dsub.util.SimpleServiceBinder; import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.RemoteControlClientHelper; import java.io.File; import java.util.ArrayList; @@ -52,8 +46,17 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import static github.daneren2005.dsub.domain.PlayerState.*; -import github.daneren2005.dsub.util.*; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.os.Handler; +import android.os.IBinder; +import android.os.PowerManager; +import android.util.Log; /** * @author Sindre Mehus @@ -71,8 +74,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { public static final String CMD_NEXT = "github.daneren2005.dsub.CMD_NEXT"; - private RemoteControlClient mRemoteControlClient; - private ImageLoader imageLoader; + private RemoteControlClientHelper mRemoteControl; private final IBinder binder = new SimpleServiceBinder<DownloadService>(this); private MediaPlayer mediaPlayer; @@ -121,12 +123,9 @@ public class DownloadServiceImpl extends Service implements DownloadService { } } - @TargetApi(14) @Override public void onCreate() { super.onCreate(); - - imageLoader = new ImageLoader(this); mediaPlayer = new MediaPlayer(); mediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK); @@ -139,36 +138,15 @@ public class DownloadServiceImpl extends Service implements DownloadService { } }); - if (Build.VERSION.SDK_INT >= 14) { - - Util.requestAudioFocus(this); - Util.registerMediaButtonEventReceiver(this); - - // Use the remote control APIs (if available) to set the playback state - if (mRemoteControlClient == null) { - AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); - // audioManager.registerMediaButtonEventReceiver(mediaButtonReceiverComponent); - // build the PendingIntent for the remote control client - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(mediaButtonReceiverComponent); - PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, 0); - // create and register the remote control client - mRemoteControlClient = new RemoteControlClient(mediaPendingIntent); - audioManager.registerRemoteControlClient(mRemoteControlClient); - } - - mRemoteControlClient.setPlaybackState( - RemoteControlClient.PLAYSTATE_STOPPED); + Util.requestAudioFocus(this); + Util.registerMediaButtonEventReceiver(this); - mRemoteControlClient.setTransportControlFlags( - RemoteControlClient.FLAG_KEY_MEDIA_PLAY | - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | - RemoteControlClient.FLAG_KEY_MEDIA_NEXT | - RemoteControlClient.FLAG_KEY_MEDIA_STOP); - } + if (mRemoteControl == null) { + // Use the remote control APIs (if available) to set the playback state + mRemoteControl = RemoteControlClientHelper.createInstance(); + ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); + mRemoteControl.register(this, mediaButtonReceiverComponent); + } if (equalizerAvailable) { equalizerController = new EqualizerController(this, mediaPlayer); @@ -194,12 +172,13 @@ public class DownloadServiceImpl extends Service implements DownloadService { } @Override - public void onStart(Intent intent, int startId) { - super.onStart(intent, startId); + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); lifecycleSupport.onStart(intent); + return START_NOT_STICKY; } - @Override + @Override public void onDestroy() { super.onDestroy(); lifecycleSupport.onDestroy(); @@ -211,6 +190,10 @@ public class DownloadServiceImpl extends Service implements DownloadService { if (visualizerController != null) { visualizerController.release(); } + if (mRemoteControl != null) { + mRemoteControl.unregister(this); + mRemoteControl = null; + } instance = null; } @@ -447,7 +430,6 @@ public class DownloadServiceImpl extends Service implements DownloadService { } } - @TargetApi(14) synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) { this.currentPlaying = currentPlaying; @@ -464,23 +446,9 @@ public class DownloadServiceImpl extends Service implements DownloadService { Util.hidePlayingNotification(this, this, handler); } - if (mRemoteControlClient != null) { - MusicDirectory.Entry currentSong = ((currentPlaying == null) ? null: currentPlaying.getSong()); - // Update the remote controls - mRemoteControlClient.editMetadata(true) - .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, currentSong == null ? null : currentSong.getArtist()) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, currentSong == null ? null : currentSong.getAlbum()) - .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, currentSong == null ? null : currentSong.getTitle()) - .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ? 0 : ((currentSong.getDuration() == null) ? 0 : currentSong.getDuration())) - .apply(); - if (currentSong == null) { - mRemoteControlClient.editMetadata(true) - .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null) - .apply(); - } else { - imageLoader.loadImage(this, mRemoteControlClient, currentSong); - } - } + MusicDirectory.Entry currentSong = (currentPlaying == null) ? null: currentPlaying.getSong(); + mRemoteControl.updateMetadata(this, currentSong); + } @Override @@ -697,7 +665,6 @@ public class DownloadServiceImpl extends Service implements DownloadService { return playerState; } - @TargetApi(14) synchronized void setPlayerState(PlayerState playerState) { Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); @@ -710,9 +677,7 @@ public class DownloadServiceImpl extends Service implements DownloadService { Util.broadcastPlaybackStatusChange(this, playerState); this.playerState = playerState; - if (mRemoteControlClient != null) { - mRemoteControlClient.setPlaybackState(playerState.getRemoteControlClientPlayState()); - } + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); if (show) { Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); diff --git a/subsonic-android/src/github/daneren2005/dsub/service/MusicService.java b/subsonic-android/src/github/daneren2005/dsub/service/MusicService.java index e8fca8f0..15abbdf8 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/MusicService.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/MusicService.java @@ -53,6 +53,8 @@ public interface MusicService { SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception; + MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception; List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception; @@ -88,4 +90,6 @@ public interface MusicService { JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception; JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception; + + void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception; }
\ No newline at end of file diff --git a/subsonic-android/src/github/daneren2005/dsub/service/OfflineMusicService.java b/subsonic-android/src/github/daneren2005/dsub/service/OfflineMusicService.java index 9c49d550..29992029 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/OfflineMusicService.java @@ -308,6 +308,11 @@ public class OfflineMusicService extends RESTMusicService { public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException("Jukebox not available in offline mode"); } + + @Override + public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Starring not available in offline mode"); + } @Override public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { diff --git a/subsonic-android/src/github/daneren2005/dsub/service/RESTMusicService.java b/subsonic-android/src/github/daneren2005/dsub/service/RESTMusicService.java index f475d9d5..a993df0d 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/RESTMusicService.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/RESTMusicService.java @@ -93,6 +93,7 @@ import github.daneren2005.dsub.service.parser.PlaylistsParser; import github.daneren2005.dsub.service.parser.RandomSongsParser; import github.daneren2005.dsub.service.parser.SearchResult2Parser; import github.daneren2005.dsub.service.parser.SearchResultParser; +import github.daneren2005.dsub.service.parser.StarredListParser; import github.daneren2005.dsub.service.parser.VersionParser; import github.daneren2005.dsub.service.ssl.SSLSocketFactory; import github.daneren2005.dsub.service.ssl.TrustSelfSignedStrategy; @@ -415,6 +416,16 @@ public class RESTMusicService implements MusicService { } @Override + public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getStarred", null); + try { + return new StarredListParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { HttpParams params = new BasicHttpParams(); HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); @@ -602,6 +613,17 @@ public class RESTMusicService implements MusicService { Util.close(reader); } } + + @Override + public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.8", "Starring is not supported."); + Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", null, "id", id); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception { return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList()); diff --git a/subsonic-android/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java b/subsonic-android/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java index 3a1826e5..d724f63c 100644 --- a/subsonic-android/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java +++ b/subsonic-android/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java @@ -38,6 +38,7 @@ public class MusicDirectoryEntryParser extends AbstractParser { entry.setDirectory(getBoolean("isDir")); entry.setCoverArt(get("coverArt")); entry.setArtist(get("artist")); + entry.setStarred(get("starred") != null); if (!entry.isDirectory()) { entry.setAlbum(get("album")); diff --git a/subsonic-android/src/github/daneren2005/dsub/service/parser/StarredListParser.java b/subsonic-android/src/github/daneren2005/dsub/service/parser/StarredListParser.java new file mode 100644 index 00000000..c3c16949 --- /dev/null +++ b/subsonic-android/src/github/daneren2005/dsub/service/parser/StarredListParser.java @@ -0,0 +1,62 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Kurt Hardin + */ +public class StarredListParser extends MusicDirectoryEntryParser { + + public StarredListParser(Context context) { + super(context); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("album".equals(name) || "song".equals(name)) { + dir.addChild(parseEntry()); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return dir; + } +}
\ No newline at end of file diff --git a/subsonic-android/src/github/daneren2005/dsub/util/AlbumView.java b/subsonic-android/src/github/daneren2005/dsub/util/AlbumView.java index 98bd500f..8c799aad 100644 --- a/subsonic-android/src/github/daneren2005/dsub/util/AlbumView.java +++ b/subsonic-android/src/github/daneren2005/dsub/util/AlbumView.java @@ -21,9 +21,11 @@ package github.daneren2005.dsub.util; import android.content.Context; import android.view.LayoutInflater; import android.view.View; +import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.activity.SubsonicTabActivity; import github.daneren2005.dsub.domain.MusicDirectory; /** @@ -32,10 +34,13 @@ import github.daneren2005.dsub.domain.MusicDirectory; * @author Sindre Mehus */ public class AlbumView extends LinearLayout { + + private MusicDirectory.Entry album; private TextView titleView; private TextView artistView; private View coverArtView; + private ImageButton starButton; public AlbumView(Context context) { super(context); @@ -44,12 +49,26 @@ public class AlbumView extends LinearLayout { titleView = (TextView) findViewById(R.id.album_title); artistView = (TextView) findViewById(R.id.album_artist); coverArtView = findViewById(R.id.album_coverart); + starButton = (ImageButton) findViewById(R.id.album_star); + starButton.setVisibility(Util.isOffline(getContext()) ? View.GONE : View.VISIBLE); + starButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + SubsonicTabActivity activity = (SubsonicTabActivity) getContext(); + activity.toggleStarredInBackground(album, starButton); + } + }); } public void setAlbum(MusicDirectory.Entry album, ImageLoader imageLoader) { + this.album = album; + titleView.setText(album.getTitle()); artistView.setText(album.getArtist()); artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE); imageLoader.loadImage(coverArtView, album, false, true); + + starButton.setImageResource(album.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off); + starButton.setFocusable(false); } } diff --git a/subsonic-android/src/github/daneren2005/dsub/util/SongView.java b/subsonic-android/src/github/daneren2005/dsub/util/SongView.java index 61846c27..1bc2b9c0 100644 --- a/subsonic-android/src/github/daneren2005/dsub/util/SongView.java +++ b/subsonic-android/src/github/daneren2005/dsub/util/SongView.java @@ -25,9 +25,11 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.Checkable; import android.widget.CheckedTextView; +import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.activity.SubsonicTabActivity; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.DownloadServiceImpl; @@ -46,13 +48,15 @@ public class SongView extends LinearLayout implements Checkable { private static final String TAG = SongView.class.getSimpleName(); private static final WeakHashMap<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>(); private static Handler handler; + + private MusicDirectory.Entry song; private CheckedTextView checkedTextView; private TextView titleTextView; private TextView artistTextView; private TextView durationTextView; private TextView statusTextView; - private MusicDirectory.Entry song; + private ImageButton starButton; public SongView(Context context) { super(context); @@ -63,6 +67,15 @@ public class SongView extends LinearLayout implements Checkable { artistTextView = (TextView) findViewById(R.id.song_artist); durationTextView = (TextView) findViewById(R.id.song_duration); statusTextView = (TextView) findViewById(R.id.song_status); + starButton = (ImageButton) findViewById(R.id.song_star); + starButton.setVisibility(Util.isOffline(getContext()) ? View.GONE : View.VISIBLE); + starButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + SubsonicTabActivity activity = (SubsonicTabActivity) getContext(); + activity.toggleStarredInBackground(song, starButton); + } + }); INSTANCES.put(this, null); int instanceCount = INSTANCES.size(); @@ -96,6 +109,9 @@ public class SongView extends LinearLayout implements Checkable { artistTextView.setText(artist); durationTextView.setText(Util.formatDuration(song.getDuration())); checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE); + + starButton.setImageResource(song.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off); + starButton.setFocusable(false); update(); } diff --git a/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java new file mode 100644 index 00000000..c3f3f70c --- /dev/null +++ b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java @@ -0,0 +1,32 @@ +package github.daneren2005.dsub.util.compat; + +import github.daneren2005.dsub.domain.MusicDirectory.Entry; +import android.content.ComponentName; +import android.content.Context; +import android.util.Log; + +public class RemoteControlClientBase extends RemoteControlClientHelper { + + private static final String TAG = RemoteControlClientBase.class.getSimpleName(); + + @Override + public void register(Context context, ComponentName mediaButtonReceiverComponent) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + + @Override + public void unregister(Context context) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + + @Override + public void setPlaybackState(int state) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + + @Override + public void updateMetadata(Context context, Entry currentSong) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + +} diff --git a/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java new file mode 100644 index 00000000..ddaa9f43 --- /dev/null +++ b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java @@ -0,0 +1,27 @@ +package github.daneren2005.dsub.util.compat; + +import github.daneren2005.dsub.domain.MusicDirectory; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; + +public abstract class RemoteControlClientHelper { + + public static RemoteControlClientHelper createInstance() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return new RemoteControlClientBase(); + } else { + return new RemoteControlClientICS(); + } + } + + protected RemoteControlClientHelper() { + // Avoid instantiation + } + + public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent); + public abstract void unregister(final Context context); + public abstract void setPlaybackState(final int state); + public abstract void updateMetadata(final Context context, final MusicDirectory.Entry currentSong); + +} diff --git a/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java new file mode 100644 index 00000000..c5b09876 --- /dev/null +++ b/subsonic-android/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java @@ -0,0 +1,71 @@ +package github.daneren2005.dsub.util.compat; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ImageLoader; +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; + +@TargetApi(14) +public class RemoteControlClientICS extends RemoteControlClientHelper { + + private RemoteControlClient mRemoteControl; + + public void register(final Context context, final ComponentName mediaButtonReceiverComponent) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + // build the PendingIntent for the remote control client + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(mediaButtonReceiverComponent); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, mediaButtonIntent, 0); + + // create and register the remote control client + mRemoteControl = new RemoteControlClient(mediaPendingIntent); + audioManager.registerRemoteControlClient(mRemoteControl); + + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + + mRemoteControl.setTransportControlFlags( + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_STOP); + } + + public void unregister(final Context context) { + if (mRemoteControl != null) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.unregisterRemoteControlClient(mRemoteControl); + } + } + + public void setPlaybackState(final int state) { + mRemoteControl.setPlaybackState(state); + } + + public void updateMetadata(final Context context, final MusicDirectory.Entry currentSong) { + // Update the remote controls + mRemoteControl.editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum()) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle()) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ? + 0 : ((currentSong.getDuration() == null) ? 0 : currentSong.getDuration())) + .apply(); + if (currentSong == null) { + mRemoteControl.editMetadata(true) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null) + .apply(); + } else { + new ImageLoader(context).loadImage(context, mRemoteControl, currentSong); + } + } + +} |