Major import from DSub for stability and quality

This commit is contained in:
Joshua Bahnsen 2013-05-16 00:59:55 -07:00
parent 486b02f2e2
commit 596fab757c
65 changed files with 4041 additions and 1918 deletions

View File

@ -9,19 +9,19 @@ package com.handmark.pulltorefresh.library;
public final class R { public final class R {
public static final class id { public static final class id {
public static final int pullFromStart = 0x7f060001; public static final int pullFromStart = 0x7f060001;
public static final int pull_to_refresh_progress = 0x7f06007f; public static final int pull_to_refresh_progress = 0x7f060081;
public static final int rotate = 0x7f060007; public static final int rotate = 0x7f060007;
public static final int both = 0x7f060003; public static final int both = 0x7f060003;
public static final int webview = 0x7f06000a; public static final int webview = 0x7f06000a;
public static final int pull_to_refresh_text = 0x7f060080; public static final int pull_to_refresh_text = 0x7f060082;
public static final int pullDownFromTop = 0x7f060005; public static final int pullDownFromTop = 0x7f060005;
public static final int gridview = 0x7f060009; public static final int gridview = 0x7f060009;
public static final int pullUpFromBottom = 0x7f060006; public static final int pullUpFromBottom = 0x7f060006;
public static final int scrollview = 0x7f06000b; public static final int scrollview = 0x7f06000b;
public static final int pullFromEnd = 0x7f060002; public static final int pullFromEnd = 0x7f060002;
public static final int pull_to_refresh_image = 0x7f06007e; public static final int pull_to_refresh_image = 0x7f060080;
public static final int pull_to_refresh_sub_text = 0x7f060081; public static final int pull_to_refresh_sub_text = 0x7f060083;
public static final int fl_inner = 0x7f06007d; public static final int fl_inner = 0x7f06007f;
public static final int flip = 0x7f060008; public static final int flip = 0x7f060008;
public static final int disabled = 0x7f060000; public static final int disabled = 0x7f060000;
public static final int manualOnly = 0x7f060004; public static final int manualOnly = 0x7f060004;
@ -41,8 +41,8 @@ public final class R {
public static final int pull_to_refresh_from_bottom_release_label = 0x7f080004; public static final int pull_to_refresh_from_bottom_release_label = 0x7f080004;
} }
public static final class layout { public static final class layout {
public static final int pull_to_refresh_header_vertical = 0x7f030018; public static final int pull_to_refresh_header_vertical = 0x7f030019;
public static final int pull_to_refresh_header_horizontal = 0x7f030017; public static final int pull_to_refresh_header_horizontal = 0x7f030018;
} }
public static final class styleable { public static final class styleable {
public static final int PullToRefresh_ptrDrawableStart = 7; public static final int PullToRefresh_ptrDrawableStart = 7;
@ -67,10 +67,10 @@ public final class R {
public static final int PullToRefresh_ptrHeaderSubTextColor = 3; public static final int PullToRefresh_ptrHeaderSubTextColor = 3;
} }
public static final class drawable { public static final class drawable {
public static final int indicator_bg_top = 0x7f020045; public static final int indicator_bg_top = 0x7f020046;
public static final int indicator_bg_bottom = 0x7f020044; public static final int indicator_bg_bottom = 0x7f020045;
public static final int default_ptr_flip = 0x7f02000c; public static final int default_ptr_flip = 0x7f02000c;
public static final int indicator_arrow = 0x7f020043; public static final int indicator_arrow = 0x7f020044;
public static final int default_ptr_rotate = 0x7f02000d; public static final int default_ptr_rotate = 0x7f02000d;
} }
public static final class attr { public static final class attr {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<gradient
android:angle="90"
android:endColor="#00000000"
android:startColor="#80000000"
android:type="linear" />
</shape>

View File

@ -26,88 +26,7 @@
a:paddingBottom="12dip" a:paddingBottom="12dip"
a:paddingTop="12dip" > a:paddingTop="12dip" >
<ImageView <include layout="@layout/media_buttons" />
a:id="@+id/download_toggle_list"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:paddingRight="4dip"
a:src="?attr/media_toggle" />
<ImageView
a:id="@+id/download_shuffle"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:paddingLeft="4dip"
a:src="?attr/media_shuffle" />
<ImageView
a:id="@+id/download_previous"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_previous" />
<ImageView
a:id="@+id/download_start"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_play" />
<ImageView
a:id="@+id/download_pause"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_pause" />
<ImageView
a:id="@+id/download_stop"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_stop" />
<ImageView
a:id="@+id/download_next"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_next" />
<ImageView
a:id="@+id/download_repeat"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_repeat_off" />
<ImageView
a:id="@+id/download_star"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_weight="1"
a:focusable="true"
a:gravity="center_vertical"
a:src="?attr/star_hollow" />
</LinearLayout> </LinearLayout>

View File

@ -64,99 +64,8 @@
<include layout="@layout/download_playlist" /> <include layout="@layout/download_playlist" />
</com.thejoshwa.ultrasonic.androidapp.util.MyViewFlipper> </com.thejoshwa.ultrasonic.androidapp.util.MyViewFlipper>
<LinearLayout <include layout="@layout/media_buttons" />
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_marginTop="0dip"
a:orientation="horizontal"
a:paddingBottom="12dip"
a:paddingTop="12dip" >
<ImageView
a:id="@+id/download_toggle_list"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:paddingRight="4dip"
a:src="?attr/media_toggle" />
<ImageView
a:id="@+id/download_shuffle"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:paddingLeft="4dip"
a:focusable="true"
a:src="?attr/media_shuffle" />
<ImageView
a:id="@+id/download_previous"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_previous" />
<ImageView
a:id="@+id/download_start"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_play" />
<ImageView
a:id="@+id/download_pause"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_pause" />
<ImageView
a:id="@+id/download_stop"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_stop" />
<ImageView
a:id="@+id/download_next"
a:layout_width="0dp"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_next" />
<ImageView
a:id="@+id/download_repeat"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_repeat_off" />
<ImageView
a:id="@+id/download_star"
a:layout_width="0dip"
a:layout_height="match_parent"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/star_hollow" />
</LinearLayout>
<include layout="@layout/download_slider" /> <include layout="@layout/download_slider" />
<include layout="@layout/download_button_bar_flipper" /> <include layout="@layout/download_button_bar_flipper" />

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="10dp" > android:padding="6dp" >
<ImageView <ImageView
android:id="@+id/select_album_select" android:id="@+id/select_album_select"

View File

@ -1,47 +1,45 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:a="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
a:orientation="vertical" android:orientation="vertical"
a:layout_width="fill_parent" android:layout_width="fill_parent"
a:layout_height="fill_parent" android:layout_height="fill_parent"
a:background="@drawable/album_art_background" android:padding="16dip">
a:padding="16dip">
<CheckBox <CheckBox
a:id="@+id/equalizer_enabled" android:id="@+id/equalizer_enabled"
a:layout_width="wrap_content" android:layout_width="wrap_content"
a:layout_height="wrap_content" android:layout_height="wrap_content"
a:text="@string/equalizer.enabled" android:text="@string/equalizer.enabled"
a:textColor="#c0c0c0" android:textColor="#c0c0c0"
a:textAppearance="?android:attr/textAppearanceMedium"/> android:textAppearance="?android:attr/textAppearanceMedium"/>
<ScrollView <ScrollView
a:layout_width="fill_parent" android:layout_width="fill_parent"
a:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
a:orientation="vertical" android:orientation="vertical"
a:layout_width="fill_parent" android:layout_width="fill_parent"
a:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
a:id="@+id/equalizer_layout" android:id="@+id/equalizer_layout"
a:orientation="vertical" android:orientation="vertical"
a:layout_width="fill_parent" android:layout_width="fill_parent"
a:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<Button <Button
a:id="@+id/equalizer_preset" android:id="@+id/equalizer_preset"
a:text="@string/equalizer.preset" android:text="@string/equalizer.preset"
a:layout_width="wrap_content" android:layout_width="wrap_content"
a:layout_height="wrap_content" android:layout_height="wrap_content"
a:layout_gravity="center" android:layout_gravity="center"
a:layout_marginTop="20dip" android:layout_marginTop="20dip"
a:paddingLeft="40dip" android:paddingLeft="40dip"
a:paddingRight="40dip"/> android:paddingRight="40dip"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:paddingBottom="12dip"
a:paddingTop="12dip" >
<ImageView
a:id="@+id/download_toggle_list"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_toggle" />
<ImageView
a:id="@+id/download_shuffle"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_shuffle" />
<com.thejoshwa.ultrasonic.androidapp.view.AutoRepeatButton
a:id="@+id/download_previous"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_previous" />
<ImageView
a:id="@+id/download_start"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_play" />
<ImageView
a:id="@+id/download_pause"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_pause" />
<ImageView
a:id="@+id/download_stop"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_stop" />
<com.thejoshwa.ultrasonic.androidapp.view.AutoRepeatButton
a:id="@+id/download_next"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_next" />
<ImageView
a:id="@+id/download_repeat"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/media_repeat_off" />
<ImageView
a:id="@+id/download_star"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:focusable="true"
a:src="?attr/star_hollow" />
</LinearLayout>

View File

@ -4,15 +4,24 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:background="@drawable/border" android:orientation="vertical"
android:orientation="horizontal"
android:visibility="gone" > android:visibility="gone" >
<LinearLayout
android:layout_height="4dip"
android:layout_width="fill_parent"
android:background="@drawable/drop_shadow" />
<LinearLayout
android:id="@+id/now_playing_view"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<ImageView <ImageView
android:id="@+id/now_playing_image" android:id="@+id/now_playing_image"
android:layout_width="64.0dip" android:layout_width="64.0dip"
android:layout_height="64.0dip" android:layout_height="64.0dip"
android:layout_weight="0.0"
android:focusable="true" android:focusable="true"
android:gravity="center" /> android:gravity="center" />
@ -55,5 +64,7 @@
android:layout_weight="0.0" android:layout_weight="0.0"
android:focusable="false" android:focusable="false"
android:src="?attr/media_pause" /> android:src="?attr/media_pause" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1,10 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
a:id="@android:id/text1" android:orientation="horizontal"
a:layout_width="fill_parent" android:layout_width="fill_parent"
a:layout_height="wrap_content" android:layout_height="wrap_content">
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_vertical" <TextView
a:paddingLeft="6dip" android:id="@+id/playlist_name"
a:paddingRight="6dip" android:layout_width="0dip"
a:minHeight="50dip"/> android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:gravity="left|center_vertical"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:minHeight="50dip"/>
</LinearLayout>

View File

@ -0,0 +1,70 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/get_playlist_name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:textSize="20dp"
android:text="@string/common.name" />
<EditText
android:id="@+id/get_playlist_name"
android:inputType="text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="4dp"
android:hint="@string/common.name" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/get_playlist_comment_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:textSize="20dp"
android:text="@string/common.comment" />
<EditText
android:id="@+id/get_playlist_comment"
android:inputType="text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="4dp"
android:hint="@string/common.comment" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/get_playlist_public_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:textSize="20dp"
android:text="@string/common.public" />
<CheckBox
android:id="@+id/get_playlist_public"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="4dp"
android:checked="false"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/playlist_info"
android:title="@string/common.info"/>
<item
android:id="@+id/playlist_menu_play_now"
android:title="@string/common.play_now"/>
<item
android:id="@+id/playlist_menu_play_shuffled"
android:title="@string/common.play_shuffled"/>
<item
android:id="@+id/playlist_menu_pin"
android:title="@string/common.pin"/>
<item
android:id="@+id/playlist_update_info"
android:title="@string/playlist.update_info"/>
<item
android:id="@+id/playlist_menu_delete"
android:title="@string/common.delete"/>
</menu>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/playlist_menu_play_now"
android:title="@string/common.play_now"/>
<item
android:id="@+id/playlist_menu_play_shuffled"
android:title="@string/common.play_shuffled"/>
</menu>

View File

@ -12,6 +12,12 @@
<string name="common.delete">Delete</string> <string name="common.delete">Delete</string>
<string name="common.various_artists">Various Artists</string> <string name="common.various_artists">Various Artists</string>
<string name="common.multiple_genres">Multiple Genres</string> <string name="common.multiple_genres">Multiple Genres</string>
<string name="common.info">Details</string>
<string name="common.play_shuffled">Play Shuffled</string>
<string name="common.confirm">Confirm</string>
<string name="common.name">Name</string>
<string name="common.comment">Comment</string>
<string name="common.public">Public</string>
<string name="button_bar.home">UltraSonic Main</string> <string name="button_bar.home">UltraSonic Main</string>
<string name="button_bar.browse">Media Library</string> <string name="button_bar.browse">Media Library</string>
<string name="button_bar.search">Search</string> <string name="button_bar.search">Search</string>
@ -47,8 +53,15 @@
<string name="menu.about">About</string> <string name="menu.about">About</string>
<string name="menu.search">Search</string> <string name="menu.search">Search</string>
<string name="menu.navigation">Navigation</string> <string name="menu.navigation">Navigation</string>
<string name="menu.common">Common</string> <string name="menu.common">Common</string>
<string name="menu.add_playlist">Add To Playlist</string>
<string name="menu.remove_playlist">Remove From Playlist</string>
<string name="menu.deleted_playlist">Deleted playlist %s</string>
<string name="menu.deleted_playlist_error">Failed to delete playlist %s</string>
<string name="playlist.label">Playlists</string> <string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Update Information</string>
<string name="playlist.updated_info">Updated playlist information for %s</string>
<string name="playlist.updated_info_error">Failed to update playlist information for %s</string>
<string name="help.label">Help</string> <string name="help.label">Help</string>
<string name="help.title">Welcome to UltraSonic</string> <string name="help.title">Welcome to UltraSonic</string>
<string name="help.back">Back</string> <string name="help.back">Back</string>
@ -118,6 +131,11 @@
<string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string> <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_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 &gt; Settings</b> on your Subsonic server.</string> <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string>
<string name="playlist_error">Failed to grab list of playlists</string>
<string name="updated_playlist">Added %1$s songs to \"%2$s\"</string>
<string name="updated_playlist_error">Failed to update \"%s\", please try later.</string>
<string name="removed_playlist">Removed %1$s songs from \"%2$s\"</string>
<string name="delete_playlist">Do you want to delete %1$s</string>
<string name="song_details.all">%1$s%2$s</string> <string name="song_details.all">%1$s%2$s</string>
<string name="song_details.kbps">%d kbps</string> <string name="song_details.kbps">%d kbps</string>
<string name="lyrics.nomatch">No lyrics found</string> <string name="lyrics.nomatch">No lyrics found</string>
@ -231,6 +249,10 @@
<string name="settings.show_lockscreen_controls_summary">Show playback controls on the lock screen</string> <string name="settings.show_lockscreen_controls_summary">Show playback controls on the lock screen</string>
<string name="settings.use_stream_proxy">Use Stream Proxy</string> <string name="settings.use_stream_proxy">Use Stream Proxy</string>
<string name="settings.use_stream_proxy_summary">Stream media playback through a proxy (may help stutter)</string> <string name="settings.use_stream_proxy_summary">Stream media playback through a proxy (may help stutter)</string>
<string name="settings.download_transition">Show Downloads On Play</string>
<string name="settings.download_transition_summary">Transition to download activity when starting playback</string>
<string name="settings.gapless_playback">Gapless Playback</string>
<string name="settings.gapless_playback_summary">Enable gapless playback</string>
<string name="settings.show_now_playing">Show Now Playing</string> <string name="settings.show_now_playing">Show Now Playing</string>
<string name="settings.show_now_playing_summary">Show currently playing track in all activities</string> <string name="settings.show_now_playing_summary">Show currently playing track in all activities</string>
<string name="settings.max_albums">Max Albums</string> <string name="settings.max_albums">Max Albums</string>
@ -253,6 +275,12 @@
<string name="settings.default_artists">Default Artists</string> <string name="settings.default_artists">Default Artists</string>
<string name="settings.default_albums">Default Albums</string> <string name="settings.default_albums">Default Albums</string>
<string name="settings.default_songs">Default Songs</string> <string name="settings.default_songs">Default Songs</string>
<string name="shuffle.startYear">Start Year:</string>
<string name="shuffle.endYear">End Year:</string>
<string name="shuffle.genre">Genre:</string>
<string name="playlist_error">Failed to grab list of playlists</string>
<string name="updated_playlist">Added %1$s songs to \"%2$s\"</string>
<string name="updated_playlist_error">Failed to update \"%s\", please try later.</string>
<string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string> <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>
<string name="background_task.wait">Please wait&#8230;</string> <string name="background_task.wait">Please wait&#8230;</string>
<string name="background_task.loading">Loading.</string> <string name="background_task.loading">Loading.</string>

View File

@ -159,6 +159,16 @@
a:key="useStreamProxy" a:key="useStreamProxy"
a:summary="@string/settings.use_stream_proxy_summary" a:summary="@string/settings.use_stream_proxy_summary"
a:title="@string/settings.use_stream_proxy" /> a:title="@string/settings.use_stream_proxy" />
<CheckBoxPreference
a:defaultValue="true"
a:key="transitionToDownloadOnPlay"
a:summary="@string/settings.download_transition_summary"
a:title="@string/settings.download_transition" />
<CheckBoxPreference
a:defaultValue="false"
a:key="gaplessPlayback"
a:summary="@string/settings.gapless_playback_summary"
a:title="@string/settings.gapless_playback" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory a:title="@string/settings.other_title" > <PreferenceCategory a:title="@string/settings.other_title" >
<CheckBoxPreference <CheckBoxPreference

View File

@ -51,7 +51,6 @@ import android.view.animation.AnimationUtils;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
@ -69,8 +68,9 @@ import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.util.Constants; import com.thejoshwa.ultrasonic.androidapp.util.Constants;
import com.thejoshwa.ultrasonic.androidapp.util.SilentBackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.SilentBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.SongView;
import com.thejoshwa.ultrasonic.androidapp.util.Util; import com.thejoshwa.ultrasonic.androidapp.util.Util;
import com.thejoshwa.ultrasonic.androidapp.view.AutoRepeatButton;
import com.thejoshwa.ultrasonic.androidapp.view.SongView;
import com.thejoshwa.ultrasonic.androidapp.view.VisualizerView; import com.thejoshwa.ultrasonic.androidapp.view.VisualizerView;
import static com.thejoshwa.ultrasonic.androidapp.domain.PlayerState.*; import static com.thejoshwa.ultrasonic.androidapp.domain.PlayerState.*;
@ -78,10 +78,10 @@ import static com.thejoshwa.ultrasonic.androidapp.domain.PlayerState.*;
public class DownloadActivity extends SubsonicTabActivity implements OnGestureListener { public class DownloadActivity extends SubsonicTabActivity implements OnGestureListener {
private static final String TAG = DownloadActivity.class.getSimpleName(); private static final String TAG = DownloadActivity.class.getSimpleName();
private static final int DIALOG_SAVE_PLAYLIST = 100; private static final int DIALOG_SAVE_PLAYLIST = 100;
private static final int INCREMENT_TIME = 5000;
private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5; private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5;
private ViewFlipper playlistFlipper; private ViewFlipper playlistFlipper;
private ViewFlipper buttonBarFlipper;
private TextView emptyTextView; private TextView emptyTextView;
private TextView songTitleTextView; private TextView songTitleTextView;
private TextView albumTextView; private TextView albumTextView;
@ -92,8 +92,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private TextView durationTextView; private TextView durationTextView;
private TextView statusTextView; private TextView statusTextView;
private static SeekBar progressBar; private static SeekBar progressBar;
private View previousButton; private AutoRepeatButton previousButton;
private View nextButton; private AutoRepeatButton nextButton;
private View pauseButton; private View pauseButton;
private View stopButton; private View stopButton;
private View startButton; private View startButton;
@ -112,8 +112,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
private int swipeDistance; private int swipeDistance;
private int swipeVelocity; private int swipeVelocity;
private VisualizerView visualizerView; private VisualizerView visualizerView;
private boolean nowPlaying = true;
private boolean visualizerAvailable; private boolean visualizerAvailable;
private boolean equalizerAvailable; private boolean equalizerAvailable;
private SilentBackgroundTask<Void> onProgressChangedTask;
/** /**
* Called when the activity is first created. * Called when the activity is first created.
*/ */
@ -129,7 +132,6 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
gestureScanner = new GestureDetector(this); gestureScanner = new GestureDetector(this);
playlistFlipper = (ViewFlipper) findViewById(R.id.download_playlist_flipper); playlistFlipper = (ViewFlipper) findViewById(R.id.download_playlist_flipper);
buttonBarFlipper = (ViewFlipper) findViewById(R.id.download_button_bar_flipper);
emptyTextView = (TextView) findViewById(R.id.download_empty); emptyTextView = (TextView) findViewById(R.id.download_empty);
songTitleTextView = (TextView) findViewById(R.id.download_song_title); songTitleTextView = (TextView) findViewById(R.id.download_song_title);
albumTextView = (TextView) findViewById(R.id.download_album); albumTextView = (TextView) findViewById(R.id.download_album);
@ -140,8 +142,8 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
statusTextView = (TextView) findViewById(R.id.download_status); statusTextView = (TextView) findViewById(R.id.download_status);
progressBar = (SeekBar) findViewById(R.id.download_progress_bar); progressBar = (SeekBar) findViewById(R.id.download_progress_bar);
playlistView = (ListView) findViewById(R.id.download_list); playlistView = (ListView) findViewById(R.id.download_list);
previousButton = findViewById(R.id.download_previous); previousButton = (AutoRepeatButton)findViewById(R.id.download_previous);
nextButton = findViewById(R.id.download_next); nextButton = (AutoRepeatButton)findViewById(R.id.download_next);
pauseButton = findViewById(R.id.download_pause); pauseButton = findViewById(R.id.download_pause);
stopButton = findViewById(R.id.download_stop); stopButton = findViewById(R.id.download_stop);
startButton = findViewById(R.id.download_start); startButton = findViewById(R.id.download_start);
@ -152,21 +154,6 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
toggleListButton = findViewById(R.id.download_toggle_list); 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);
buttonBarFlipper.setOnTouchListener(touchListener);
emptyTextView.setOnTouchListener(touchListener);
albumArtImageView.setOnTouchListener(touchListener);
albumArtImageView.setOnClickListener(new View.OnClickListener() { albumArtImageView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
@ -177,40 +164,98 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
previousButton.setOnClickListener(new View.OnClickListener() { previousButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
getDownloadService().previous();
onCurrentChanged(); new SilentBackgroundTask<Void>(DownloadActivity.this) {
onSliderProgressChanged(); @Override
protected Void doInBackground() throws Throwable {
getDownloadService().previous();
return null;
}
@Override
protected void done(Void result) {
onCurrentChanged();
onSliderProgressChanged();
}
}.execute();
} }
}); });
previousButton.setOnRepeatListener(new Runnable() {
public void run() {
changeProgress(-INCREMENT_TIME);
}
});
nextButton.setOnClickListener(new View.OnClickListener() { nextButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) {
getDownloadService().next(); new SilentBackgroundTask<Boolean>(DownloadActivity.this) {
onCurrentChanged(); @Override
onSliderProgressChanged(); protected Boolean doInBackground() throws Throwable {
} if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) {
getDownloadService().next();
return true;
} else {
return false;
}
}
@Override
protected void done(Boolean result) {
if(result) {
onCurrentChanged();
onSliderProgressChanged();
}
}
}.execute();
} }
}); });
nextButton.setOnRepeatListener(new Runnable() {
public void run() {
changeProgress(INCREMENT_TIME);
}
});
pauseButton.setOnClickListener(new View.OnClickListener() { pauseButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
getDownloadService().pause(); new SilentBackgroundTask<Void>(DownloadActivity.this) {
onCurrentChanged(); @Override
onSliderProgressChanged(); protected Void doInBackground() throws Throwable {
getDownloadService().pause();
return null;
}
@Override
protected void done(Void result) {
onCurrentChanged();
onSliderProgressChanged();
}
}.execute();
} }
}); });
stopButton.setOnClickListener(new View.OnClickListener() { stopButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
getDownloadService().reset(); new SilentBackgroundTask<Void>(DownloadActivity.this) {
onCurrentChanged(); @Override
onSliderProgressChanged(); protected Void doInBackground() throws Throwable {
getDownloadService().reset();
return null;
}
@Override
protected void done(Void result) {
onCurrentChanged();
onSliderProgressChanged();
}
}.execute();
} }
}); });
@ -218,9 +263,20 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
public void onClick(View view) { public void onClick(View view) {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
start();
onCurrentChanged(); new SilentBackgroundTask<Void>(DownloadActivity.this) {
onSliderProgressChanged(); @Override
protected Void doInBackground() throws Throwable {
start();
return null;
}
@Override
protected void done(Void result) {
onCurrentChanged();
onSliderProgressChanged();
}
}.execute();
} }
}); });
@ -265,8 +321,19 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
getDownloadService().seekTo(getProgressBar().getProgress()); new SilentBackgroundTask<Void>(DownloadActivity.this) {
onSliderProgressChanged(); @Override
protected Void doInBackground() throws Throwable {
getDownloadService().seekTo(getProgressBar().getProgress());
return null;
}
@Override
protected void done(Void result) {
onSliderProgressChanged();
}
}.execute();
} }
@Override @Override
@ -280,11 +347,22 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() { playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
getDownloadService().play(position);
onCurrentChanged(); new SilentBackgroundTask<Void>(DownloadActivity.this) {
onSliderProgressChanged(); @Override
protected Void doInBackground() throws Throwable {
getDownloadService().play(position);
return null;
}
@Override
protected void done(Void result) {
onCurrentChanged();
onSliderProgressChanged();
}
}.execute();
} }
}); });
@ -381,7 +459,6 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
DownloadService downloadService = getDownloadService(); DownloadService downloadService = getDownloadService();
if (downloadService == null || downloadService.getCurrentPlaying() == null) { if (downloadService == null || downloadService.getCurrentPlaying() == null) {
playlistFlipper.setDisplayedChild(1); playlistFlipper.setDisplayedChild(1);
buttonBarFlipper.setDisplayedChild(1);
} }
onDownloadListChanged(); onDownloadListChanged();
@ -461,11 +538,11 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
return super.onCreateDialog(id); return super.onCreateDialog(id);
} }
} }
@Override @Override
protected void onPrepareDialog(int id, Dialog dialog) { protected void onPrepareDialog(int id, Dialog dialog) {
if (id == DIALOG_SAVE_PLAYLIST) { if (id == DIALOG_SAVE_PLAYLIST) {
String playlistName = getDownloadService().getSuggestedPlaylistName(); String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null;
if (playlistName != null) { if (playlistName != null) {
playlistNameView.setText(playlistName); playlistNameView.setText(playlistName);
} else { } else {
@ -623,7 +700,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override @Override
protected Void doInBackground() throws Throwable { protected Void doInBackground() throws Throwable {
List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>(); List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>();
for (DownloadFile downloadFile : getDownloadService().getDownloads()) { for (DownloadFile downloadFile : getDownloadService().getSongs()) {
entries.add(downloadFile.getSong()); entries.add(downloadFile.getSong());
} }
MusicService musicService = MusicServiceFactory.getMusicService(DownloadActivity.this); MusicService musicService = MusicServiceFactory.getMusicService(DownloadActivity.this);
@ -650,25 +727,17 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in)); playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_in));
playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out)); playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_down_out));
playlistFlipper.setDisplayedChild(0); 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 { } else {
playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in)); playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_in));
playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out)); playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.push_up_out));
playlistFlipper.setDisplayedChild(1); 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() { private void start() {
DownloadService service = getDownloadService(); DownloadService service = getDownloadService();
PlayerState state = service.getPlayerState(); PlayerState state = service.getPlayerState();
if (state == PAUSED || state == COMPLETED) { if (state == PAUSED || state == COMPLETED || state == STOPPED) {
service.start(); service.start();
} else if (state == STOPPED || state == IDLE) { } else if (state == STOPPED || state == IDLE) {
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
@ -682,30 +751,41 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
} }
} }
private void onDownloadListChanged() { private void onDownloadListChanged() {
onDownloadListChanged(false);
}
private void onDownloadListChanged(boolean refresh) {
DownloadService downloadService = getDownloadService(); DownloadService downloadService = getDownloadService();
if (downloadService == null) { if (downloadService == null) {
return; return;
} }
List<DownloadFile> list = downloadService.getDownloads(); List<DownloadFile> list;
if(nowPlaying) {
playlistView.setAdapter(new SongListAdapter(list)); list = downloadService.getSongs();
}
else {
list = downloadService.getBackgroundDownloads();
}
emptyTextView.setText(R.string.download_empty);
playlistView.setAdapter(new SongListAdapter(list));
emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
currentRevision = downloadService.getDownloadListUpdateRevision(); currentRevision = downloadService.getDownloadListUpdateRevision();
switch (downloadService.getRepeatMode()) { switch (downloadService.getRepeatMode()) {
case OFF: case OFF:
repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_off)); repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_off));
break; break;
case ALL: case ALL:
repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_all)); repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_all));
break; break;
case SINGLE: case SINGLE:
repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_single)); repeatButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_repeat_single));
break; break;
default: default:
break; break;
} }
} }
@ -732,70 +812,128 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
} }
} }
private void onSliderProgressChanged() { private void onSliderProgressChanged() {
if (getDownloadService() == null) { if (getDownloadService() == null || onProgressChangedTask != null) {
return; return;
} }
if (currentPlaying != null) { onProgressChangedTask = new SilentBackgroundTask<Void>(this) {
int millisPlayed = Math.max(0, getDownloadService().getPlayerPosition()); DownloadService downloadService;
Integer duration = getDownloadService().getPlayerDuration(); boolean isJukeboxEnabled;
int millisTotal = duration == null ? 0 : duration; int millisPlayed;
Integer duration;
PlayerState playerState;
positionTextView.setText(Util.formatDuration(millisPlayed / 1000)); @Override
durationTextView.setText(Util.formatDuration(millisTotal / 1000)); protected Void doInBackground() throws Throwable {
progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug. downloadService = getDownloadService();
progressBar.setProgress(millisPlayed); isJukeboxEnabled = downloadService.isJukeboxEnabled();
progressBar.setEnabled(currentPlaying.isCompleteFileAvailable() || getDownloadService().isJukeboxEnabled()); millisPlayed = Math.max(0, downloadService.getPlayerPosition());
} else { duration = downloadService.getPlayerDuration();
positionTextView.setText(R.string.util_zero_time); playerState = getDownloadService().getPlayerState();
durationTextView.setText(R.string.util_no_time); return null;
progressBar.setProgress(0); }
progressBar.setMax(0);
progressBar.setEnabled(false);
}
PlayerState playerState = getDownloadService().getPlayerState(); @Override
protected void done(Void result) {
if (currentPlaying != null) {
int millisTotal = duration == null ? 0 : duration;
switch (playerState) { positionTextView.setText(Util.formatDuration(millisPlayed / 1000));
case DOWNLOADING: durationTextView.setText(Util.formatDuration(millisTotal / 1000));
long bytes = currentPlaying.getPartialFile().length(); progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug.
statusTextView.setText(getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, this))); progressBar.setProgress(millisPlayed);
break; progressBar.setEnabled(currentPlaying.isWorkDone() || isJukeboxEnabled);
case PREPARING: } else {
statusTextView.setText(R.string.download_playerstate_buffering); positionTextView.setText(R.string.util_zero_time);
break; durationTextView.setText(R.string.util_no_time);
case STARTED: progressBar.setProgress(0);
if (getDownloadService().isShufflePlayEnabled()) { progressBar.setMax(0);
statusTextView.setText(R.string.download_playerstate_playing_shuffle); progressBar.setEnabled(false);
} else { }
statusTextView.setText(null);
}
break;
default:
statusTextView.setText(null);
break;
}
switch (playerState) { switch (playerState) {
case STARTED: case DOWNLOADING:
pauseButton.setVisibility(View.VISIBLE); long bytes = currentPlaying.getPartialFile().length();
stopButton.setVisibility(View.GONE); statusTextView.setText(getResources().getString(
startButton.setVisibility(View.GONE); R.string.download_playerstate_downloading,
break; Util.formatLocalizedBytes(bytes,
case DOWNLOADING: DownloadActivity.this)));
case PREPARING: break;
pauseButton.setVisibility(View.GONE); case PREPARING:
stopButton.setVisibility(View.VISIBLE); statusTextView
startButton.setVisibility(View.GONE); .setText(R.string.download_playerstate_buffering);
break; break;
default: case STARTED:
pauseButton.setVisibility(View.GONE); if (getDownloadService().isShufflePlayEnabled()) {
stopButton.setVisibility(View.GONE); statusTextView
startButton.setVisibility(View.VISIBLE); .setText(R.string.download_playerstate_playing_shuffle);
break; } 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;
}
onProgressChangedTask = null;
}
};
onProgressChangedTask.execute();
}
private void changeProgress(final int ms) {
final DownloadService downloadService = getDownloadService();
if(downloadService == null) {
return;
}
new SilentBackgroundTask<Void>(this) {
int msPlayed;
Integer duration;
int seekTo;
@Override
protected Void doInBackground() throws Throwable {
msPlayed = Math.max(0, downloadService.getPlayerPosition());
duration = downloadService.getPlayerDuration();
int msTotal = duration == null ? 0 : duration;
if(msPlayed + ms > msTotal) {
seekTo = msTotal;
} else {
seekTo = msPlayed + ms;
}
downloadService.seekTo(seekTo);
return null;
}
@Override
protected void done(Void result) {
progressBar.setProgress(seekTo);
}
}.execute();
}
private class SongListAdapter extends ArrayAdapter<DownloadFile> { private class SongListAdapter extends ArrayAdapter<DownloadFile> {
public SongListAdapter(List<DownloadFile> entries) { public SongListAdapter(List<DownloadFile> entries) {

View File

@ -1,5 +0,0 @@
package com.thejoshwa.ultrasonic.androidapp.activity;
public class GenericActivity {
}

View File

@ -366,12 +366,6 @@ public class MainActivity extends SubsonicTabActivity {
} }
} }
private void restart() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
Util.startActivityWithoutTransition(this, intent);
}
private void exit() { private void exit() {
stopService(new Intent(this, DownloadServiceImpl.class)); stopService(new Intent(this, DownloadServiceImpl.class));
Util.unregisterMediaButtonEventReceiver(this); Util.unregisterMediaButtonEventReceiver(this);

View File

@ -90,7 +90,8 @@ public final class PlayVideoActivity extends Activity {
private String getVideoUrl() { private String getVideoUrl() {
String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID); String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
return MusicServiceFactory.getMusicService(this).getVideoUrl(this, id); int maxBitrate = Util.getMaxVideoBitrate(this);
return MusicServiceFactory.getMusicService(this).getVideoUrl(maxBitrate, this, id);
} }
@Override @Override

View File

@ -44,13 +44,13 @@ import com.thejoshwa.ultrasonic.androidapp.domain.SearchResult;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadService; import com.thejoshwa.ultrasonic.androidapp.service.DownloadService;
import com.thejoshwa.ultrasonic.androidapp.util.ArtistAdapter;
import com.thejoshwa.ultrasonic.androidapp.util.BackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.BackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Constants; import com.thejoshwa.ultrasonic.androidapp.util.Constants;
import com.thejoshwa.ultrasonic.androidapp.util.EntryAdapter;
import com.thejoshwa.ultrasonic.androidapp.util.MergeAdapter; import com.thejoshwa.ultrasonic.androidapp.util.MergeAdapter;
import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Util; import com.thejoshwa.ultrasonic.androidapp.util.Util;
import com.thejoshwa.ultrasonic.androidapp.view.ArtistAdapter;
import com.thejoshwa.ultrasonic.androidapp.view.EntryAdapter;
/** /**
* Performs searches and displays the matching artists, albums and songs. * Performs searches and displays the matching artists, albums and songs.
@ -190,16 +190,16 @@ public class SearchActivity extends SubsonicTabActivity {
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.album_menu_play_now: case R.id.album_menu_play_now:
downloadRecursively(id, false, false, true, false); downloadRecursively(id, false, false, true, false, false);
break; break;
case R.id.album_menu_play_next: case R.id.album_menu_play_next:
downloadRecursively(id, false, true, false, true); downloadRecursively(id, false, true, false, true, false);
break; break;
case R.id.album_menu_play_last: case R.id.album_menu_play_last:
downloadRecursively(id, false, true, false, false); downloadRecursively(id, false, true, false, false, false);
break; break;
case R.id.album_menu_pin: case R.id.album_menu_pin:
downloadRecursively(id, true, true, false, false); downloadRecursively(id, true, true, false, false, false);
break; break;
default: default:
return super.onContextItemSelected(menuItem); return super.onContextItemSelected(menuItem);
@ -330,7 +330,7 @@ public class SearchActivity extends SubsonicTabActivity {
if (!append) { if (!append) {
downloadService.clear(); downloadService.clear();
} }
downloadService.download(Arrays.asList(song), save, false, playNext); downloadService.download(Arrays.asList(song), save, false, playNext, false);
if (autoplay) { if (autoplay) {
downloadService.play(downloadService.size() - 1); downloadService.play(downloadService.size() - 1);
} }
@ -340,8 +340,10 @@ public class SearchActivity extends SubsonicTabActivity {
} }
private void onVideoSelected(MusicDirectory.Entry entry) { private void onVideoSelected(MusicDirectory.Entry entry) {
int maxBitrate = Util.getMaxVideoBitrate(this);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(this, entry.getId()))); intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(maxBitrate, this, entry.getId())));
startActivity(intent); startActivity(intent);
} }

View File

@ -42,11 +42,13 @@ import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.util.Constants; import com.thejoshwa.ultrasonic.androidapp.util.Constants;
import com.thejoshwa.ultrasonic.androidapp.util.EntryAdapter; import com.thejoshwa.ultrasonic.androidapp.util.FileUtil;
import com.thejoshwa.ultrasonic.androidapp.util.Pair; import com.thejoshwa.ultrasonic.androidapp.util.Pair;
import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Util; import com.thejoshwa.ultrasonic.androidapp.util.Util;
import com.thejoshwa.ultrasonic.androidapp.view.EntryAdapter;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -142,28 +144,26 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
playNowButton.setOnClickListener(new View.OnClickListener() { playNowButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
download(false, false, true, false); playNow(false, false);
selectAll(false, false);
} }
}); });
playNextButton.setOnClickListener(new View.OnClickListener() { playNextButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
download(true, false, false, true); download(true, false, false, true, false);
selectAll(false, false); selectAll(false, false);
} }
}); });
playLastButton.setOnClickListener(new View.OnClickListener() { playLastButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
download(true, false, false, false); playNow(false, true);
selectAll(false, false);
} }
}); });
pinButton.setOnClickListener(new View.OnClickListener() { pinButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
download(true, true, false, false); downloadBackground(true);
selectAll(false, false); selectAll(false, false);
} }
}); });
@ -233,8 +233,22 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
return true; return true;
} }
private void playNow(final boolean shuffle, final boolean append) {
if(getSelectedSongs().size() > 0) {
download(append, false, !append, false, shuffle);
selectAll(false, false);
}
else {
playAll(shuffle, append);
}
}
private void playAll() {
playAll(false, false);
}
private void playAll() { private void playAll(final boolean shuffle, final boolean append) {
boolean hasSubFolders = false; boolean hasSubFolders = false;
for (int i = 0; i < albumListView.getCount(); i++) { for (int i = 0; i < albumListView.getCount(); i++) {
MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i); MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
@ -246,10 +260,10 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID); String id = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID);
if (hasSubFolders && id != null) { if (hasSubFolders && id != null) {
downloadRecursively(id, false, false, true, false); downloadRecursively(id, false, append, !append, shuffle, false);
} else { } else {
selectAll(true, false); selectAll(true, false);
download(false, false, true, false); download(append, false, !append, false, shuffle);
selectAll(false, false); selectAll(false, false);
} }
} }
@ -282,16 +296,16 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(info.position)); songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(info.position));
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.album_menu_play_now: case R.id.album_menu_play_now:
downloadRecursively(entry.getId(), false, false, true, false); downloadRecursively(entry.getId(), false, false, true, false, false);
break; break;
case R.id.album_menu_play_next: case R.id.album_menu_play_next:
downloadRecursively(entry.getId(), false, false, true, true); downloadRecursively(entry.getId(), false, false, true, true, false);
break; break;
case R.id.album_menu_play_last: case R.id.album_menu_play_last:
downloadRecursively(entry.getId(), false, true, false, false); downloadRecursively(entry.getId(), false, true, false, false, false);
break; break;
case R.id.album_menu_pin: case R.id.album_menu_pin:
downloadRecursively(entry.getId(), true, true, false, false); downloadRecursively(entry.getId(), true, true, false, false, false);
break; break;
case R.id.select_album_play_all: case R.id.select_album_play_all:
playAll(); playAll();
@ -321,14 +335,14 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
return false; return false;
} }
private void getMusicDirectory(final String id, String name) { private void getMusicDirectory(final String id, final String name) {
getActionBar().setSubtitle(name); getActionBar().setSubtitle(name);
new LoadTask() { new LoadTask() {
@Override @Override
protected MusicDirectory load(MusicService service) throws Exception { protected MusicDirectory load(MusicService service) throws Exception {
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false); boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
return service.getMusicDirectory(id, refresh, SelectAlbumActivity.this, this); return service.getMusicDirectory(id, name, refresh, SelectAlbumActivity.this, this);
} }
}.execute(); }.execute();
} }
@ -393,13 +407,13 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
}.execute(); }.execute();
} }
private void getPlaylist(final String playlistId, String playlistName) { private void getPlaylist(final String playlistId, final String playlistName) {
getActionBar().setSubtitle(playlistName); getActionBar().setSubtitle(playlistName);
new LoadTask() { new LoadTask() {
@Override @Override
protected MusicDirectory load(MusicService service) throws Exception { protected MusicDirectory load(MusicService service) throws Exception {
return service.getPlaylist(playlistId, SelectAlbumActivity.this, this); return service.getPlaylist(playlistId, playlistName, SelectAlbumActivity.this, this);
} }
}.execute(); }.execute();
} }
@ -527,7 +541,7 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
return songs; return songs;
} }
private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext) { private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext, final boolean shuffle) {
if (getDownloadService() == null) { if (getDownloadService() == null) {
return; return;
} }
@ -541,15 +555,16 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
} }
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
getDownloadService().download(songs, save, autoplay, playNext); getDownloadService().download(songs, save, autoplay, playNext, shuffle);
String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME); String playlistName = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
if (playlistName != null) { if (playlistName != null) {
getDownloadService().setSuggestedPlaylistName(playlistName); getDownloadService().setSuggestedPlaylistName(playlistName);
} }
if (autoplay) { if (autoplay) {
Util.startActivityWithoutTransition(SelectAlbumActivity.this, DownloadActivity.class); if (Util.getShouldTransitionOnPlaybackPreference(SelectAlbumActivity.this)) {
Util.startActivityWithoutTransition(SelectAlbumActivity.this, DownloadActivity.class);
}
} else if (save) { } else if (save) {
Util.toast(SelectAlbumActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size())); Util.toast(SelectAlbumActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
} else if (playNext) { } else if (playNext) {
@ -562,10 +577,42 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
checkLicenseAndTrialPeriod(onValid); checkLicenseAndTrialPeriod(onValid);
} }
private void downloadBackground(final boolean save) {
List<MusicDirectory.Entry> songs = getSelectedSongs();
if(songs.isEmpty()) {
selectAll(true, false);
songs = getSelectedSongs();
}
downloadBackground(save, songs);
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) {
if (getDownloadService() == null) {
return;
}
private void delete() { Runnable onValid = new Runnable() {
@Override
public void run() {
warnIfNetworkOrStorageUnavailable();
getDownloadService().downloadBackground(songs, save);
Util.toast(SelectAlbumActivity.this,
getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
}
};
checkLicenseAndTrialPeriod(onValid);
}
private void delete() {
List<MusicDirectory.Entry> songs = getSelectedSongs();
if(songs.isEmpty()) {
selectAll(true, false);
songs = getSelectedSongs();
}
if (getDownloadService() != null) { if (getDownloadService() != null) {
getDownloadService().delete(getSelectedSongs()); getDownloadService().delete(songs);
} }
} }
@ -578,11 +625,21 @@ public class SelectAlbumActivity extends SubsonicTabActivity {
} }
private void playVideo(MusicDirectory.Entry entry) { private void playVideo(MusicDirectory.Entry entry) {
int maxBitrate = Util.getMaxVideoBitrate(this);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(this, entry.getId()))); intent.setData(Uri.parse(MusicServiceFactory.getMusicService(this).getVideoUrl(maxBitrate, this, entry.getId())));
startActivity(intent); startActivity(intent);
} }
public void deleteRecursively(MusicDirectory.Entry album) {
File dir = FileUtil.getAlbumDirectory(this, album);
Util.recursiveDelete(dir);
if(Util.isOffline(this)) {
refresh();
}
}
private void checkLicenseAndTrialPeriod(Runnable onValid) { private void checkLicenseAndTrialPeriod(Runnable onValid) {
if (licenseValid) { if (licenseValid) {

View File

@ -41,11 +41,11 @@ import com.thejoshwa.ultrasonic.androidapp.domain.Indexes;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicFolder; import com.thejoshwa.ultrasonic.androidapp.domain.MusicFolder;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.util.ArtistAdapter;
import com.thejoshwa.ultrasonic.androidapp.util.BackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.BackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Constants; import com.thejoshwa.ultrasonic.androidapp.util.Constants;
import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Util; import com.thejoshwa.ultrasonic.androidapp.util.Util;
import com.thejoshwa.ultrasonic.androidapp.view.ArtistAdapter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -129,7 +129,7 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false); boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this); MusicService musicService = MusicServiceFactory.getMusicService(SelectArtistActivity.this);
if (!Util.isOffline(SelectArtistActivity.this)) { if (!Util.isOffline(SelectArtistActivity.this)) {
musicFolders = musicService.getMusicFolders(SelectArtistActivity.this, this); musicFolders = musicService.getMusicFolders(refresh, SelectArtistActivity.this, this);
} }
String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this); String musicFolderId = Util.getSelectedMusicFolderId(SelectArtistActivity.this);
return musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this); return musicService.getIndexes(musicFolderId, refresh, SelectArtistActivity.this, this);
@ -214,16 +214,16 @@ public class SelectArtistActivity extends SubsonicTabActivity implements Adapter
if (artist != null) { if (artist != null) {
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.artist_menu_play_now: case R.id.artist_menu_play_now:
downloadRecursively(artist.getId(), false, false, true, false); downloadRecursively(artist.getId(), false, false, true, false, false);
break; break;
case R.id.artist_menu_play_next: case R.id.artist_menu_play_next:
downloadRecursively(artist.getId(), false, false, true, true); downloadRecursively(artist.getId(), false, false, true, true, false);
break; break;
case R.id.artist_menu_play_last: case R.id.artist_menu_play_last:
downloadRecursively(artist.getId(), false, true, false, false); downloadRecursively(artist.getId(), false, true, false, false, false);
break; break;
case R.id.artist_menu_pin: case R.id.artist_menu_pin:
downloadRecursively(artist.getId(), true, true, false, false); downloadRecursively(artist.getId(), true, true, false, false, false);
break; break;
default: default:
return super.onContextItemSelected(menuItem); return super.onContextItemSelected(menuItem);

View File

@ -19,15 +19,20 @@
package com.thejoshwa.ultrasonic.androidapp.activity; package com.thejoshwa.ultrasonic.androidapp.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshBase;
@ -37,20 +42,24 @@ import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.Playlist; import com.thejoshwa.ultrasonic.androidapp.domain.Playlist;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.OfflineException;
import com.thejoshwa.ultrasonic.androidapp.service.ServerTooOldException;
import com.thejoshwa.ultrasonic.androidapp.util.BackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.BackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.CacheCleaner;
import com.thejoshwa.ultrasonic.androidapp.util.Constants; import com.thejoshwa.ultrasonic.androidapp.util.Constants;
import com.thejoshwa.ultrasonic.androidapp.util.LoadingTask;
import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.TabActivityBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Util; import com.thejoshwa.ultrasonic.androidapp.util.Util;
import com.thejoshwa.ultrasonic.androidapp.view.PlaylistAdapter;
import java.util.List; import java.util.List;
public class SelectPlaylistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener { public class SelectPlaylistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener {
private static final int MENU_ITEM_PLAY_ALL = 1;
private PullToRefreshListView refreshPlaylistsListView; private PullToRefreshListView refreshPlaylistsListView;
private ListView playlistsListView; private ListView playlistsListView;
private View emptyTextView; private View emptyTextView;
private PlaylistAdapter playlistAdapter;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -99,12 +108,16 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
protected List<Playlist> doInBackground() throws Throwable { protected List<Playlist> doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this); MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false); boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
return musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this); List<Playlist> playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
if(!Util.isOffline(SelectPlaylistActivity.this))
new CacheCleaner(SelectPlaylistActivity.this, getDownloadService()).cleanPlaylists(playlists);
return playlists;
} }
@Override @Override
protected void done(List<Playlist> result) { protected void done(List<Playlist> result) {
playlistsListView.setAdapter(new PlaylistAdapter(result)); playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(SelectPlaylistActivity.this, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE); emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
} }
}; };
@ -114,7 +127,12 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, view, menuInfo); super.onCreateContextMenu(menu, view, menuInfo);
menu.add(Menu.NONE, MENU_ITEM_PLAY_ALL, MENU_ITEM_PLAY_ALL, R.string.common_play_now);
MenuInflater inflater = getMenuInflater();
if (Util.isOffline(this))
inflater.inflate(R.menu.select_playlist_context_offline, menu);
else
inflater.inflate(R.menu.select_playlist_context, menu);
} }
@Override @Override
@ -122,14 +140,35 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position); Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
Intent intent;
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case MENU_ITEM_PLAY_ALL: case R.id.playlist_menu_pin:
Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class); downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
break;
case R.id.playlist_menu_play_now:
intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId()); 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_PLAYLIST_NAME, playlist.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent); Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
break; break;
case R.id.playlist_menu_play_shuffled:
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);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
break;
case R.id.playlist_menu_delete:
deletePlaylist(playlist);
break;
case R.id.playlist_info:
displayPlaylistInfo(playlist);
break;
case R.id.playlist_update_info:
updatePlaylistInfo(playlist);
break;
default: default:
return super.onContextItemSelected(menuItem); return super.onContextItemSelected(menuItem);
} }
@ -157,17 +196,121 @@ public class SelectPlaylistActivity extends SubsonicTabActivity implements Adapt
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName()); intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent); Util.startActivityWithoutTransition(SelectPlaylistActivity.this, intent);
} }
private class PlaylistAdapter extends ArrayAdapter<Playlist> {
public PlaylistAdapter(List<Playlist> playlists) {
super(SelectPlaylistActivity.this, R.layout.playlist_list_item, playlists);
}
}
private class GetDataTask extends AsyncTask<Void, Void, String[]> { private void deletePlaylist(final Playlist playlist) {
new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.common_confirm)
.setMessage(getResources().getString(R.string.delete_playlist, playlist.getName()))
.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new LoadingTask<Void>(SelectPlaylistActivity.this, false) {
@Override
protected Void doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
musicService.deletePlaylist(playlist.getId(), SelectPlaylistActivity.this, null);
return null;
}
@Override
protected void done(Void result) {
playlistAdapter.remove(playlist);
playlistAdapter.notifyDataSetChanged();
Util.toast(SelectPlaylistActivity.this, getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
}
@Override
protected void error(Throwable error) {
String msg;
if (error instanceof OfflineException || error instanceof ServerTooOldException) {
msg = getErrorMessage(error);
} else {
msg = getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
}
Util.toast(SelectPlaylistActivity.this, msg, false);
}
}.execute();
}
})
.setNegativeButton(R.string.common_cancel, null)
.show();
}
private void displayPlaylistInfo(final Playlist playlist) {
String message = "Owner: " + playlist.getOwner() + "\nComments: " +
((playlist.getComment() == null) ? "" : playlist.getComment()) +
"\nSong Count: " + playlist.getSongCount() +
((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) +
((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' '))));
new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(playlist.getName())
.setMessage(message)
.show();
}
private void updatePlaylistInfo(final Playlist playlist) {
View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
final EditText nameBox = (EditText)dialogView.findViewById(R.id.get_playlist_name);
final EditText commentBox = (EditText)dialogView.findViewById(R.id.get_playlist_comment);
final CheckBox publicBox = (CheckBox)dialogView.findViewById(R.id.get_playlist_public);
nameBox.setText(playlist.getName());
commentBox.setText(playlist.getComment());
Boolean pub = playlist.getPublic();
if(pub == null) {
publicBox.setEnabled(false);
} else {
publicBox.setChecked(pub);
}
new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.playlist_update_info)
.setView(dialogView)
.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new LoadingTask<Void>(SelectPlaylistActivity.this, false) {
@Override
protected Void doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
musicService.updatePlaylist(playlist.getId(), nameBox.getText().toString(), commentBox.getText().toString(), publicBox.isChecked(), SelectPlaylistActivity.this, null);
return null;
}
@Override
protected void done(Void result) {
refresh();
Util.toast(SelectPlaylistActivity.this, getResources().getString(R.string.playlist_updated_info, playlist.getName()));
}
@Override
protected void error(Throwable error) {
String msg;
if (error instanceof OfflineException || error instanceof ServerTooOldException) {
msg = getErrorMessage(error);
} else {
msg = getResources().getString(R.string.playlist_updated_info_error, playlist.getName()) + " " + getErrorMessage(error);
}
Util.toast(SelectPlaylistActivity.this, msg, false);
}
}.execute();
}
})
.setNegativeButton(R.string.common_cancel, null)
.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]> {
@Override @Override
protected void onPostExecute(String[] result) { protected void onPostExecute(String[] result) {
refreshPlaylistsListView.onRefreshComplete(); refreshPlaylistsListView.onRefreshComplete();
super.onPostExecute(result); super.onPostExecute(result);
} }

View File

@ -20,6 +20,7 @@ package com.thejoshwa.ultrasonic.androidapp.activity;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.CheckBoxPreference; import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
@ -77,6 +78,7 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
private ListPreference defaultArtists; private ListPreference defaultArtists;
private CheckBoxPreference mediaButtonsEnabled; private CheckBoxPreference mediaButtonsEnabled;
private CheckBoxPreference lockScreenEnabled; private CheckBoxPreference lockScreenEnabled;
private CheckBoxPreference gaplessPlaybackEnabled;
private int maxServerCount = 10; private int maxServerCount = 10;
private int minServerCount = 0; private int minServerCount = 0;
@ -148,6 +150,7 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
defaultAlbums = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS); defaultAlbums = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS);
mediaButtonsEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS); mediaButtonsEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS);
lockScreenEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS); lockScreenEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS);
gaplessPlaybackEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK);
findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { findPreference(Constants.PREFERENCES_KEY_CLEAR_SEARCH_HISTORY).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override @Override
@ -159,6 +162,11 @@ public class SettingsActivity extends PreferenceActivity implements SharedPrefer
} }
}); });
if (Build.VERSION.SDK_INT < 14) {
gaplessPlaybackEnabled.setChecked(false);
gaplessPlaybackEnabled.setEnabled(false);
}
settings = PreferenceManager.getDefaultSharedPreferences(this ); settings = PreferenceManager.getDefaultSharedPreferences(this );
activeServers = settings.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 3); activeServers = settings.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 3);

View File

@ -20,14 +20,18 @@ package com.thejoshwa.ultrasonic.androidapp.activity;
import java.io.File; import java.io.File;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -35,27 +39,32 @@ import android.os.Environment;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.View.OnTouchListener; import android.view.View.OnTouchListener;
import android.view.Window;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.thejoshwa.ultrasonic.androidapp.R; import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory.Entry; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory.Entry;
import com.thejoshwa.ultrasonic.androidapp.domain.PlayerState; import com.thejoshwa.ultrasonic.androidapp.domain.PlayerState;
import com.thejoshwa.ultrasonic.androidapp.domain.Playlist;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile; import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadService; import com.thejoshwa.ultrasonic.androidapp.service.DownloadService;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadServiceImpl; import com.thejoshwa.ultrasonic.androidapp.service.DownloadServiceImpl;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.service.OfflineException;
import com.thejoshwa.ultrasonic.androidapp.service.ServerTooOldException;
import com.thejoshwa.ultrasonic.androidapp.util.Constants; import com.thejoshwa.ultrasonic.androidapp.util.Constants;
import com.thejoshwa.ultrasonic.androidapp.util.FileUtil; import com.thejoshwa.ultrasonic.androidapp.util.FileUtil;
import com.thejoshwa.ultrasonic.androidapp.util.ImageLoader; import com.thejoshwa.ultrasonic.androidapp.util.ImageLoader;
import com.thejoshwa.ultrasonic.androidapp.util.LoadingTask;
import com.thejoshwa.ultrasonic.androidapp.util.ModalBackgroundTask; import com.thejoshwa.ultrasonic.androidapp.util.ModalBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.SilentBackgroundTask;
import com.thejoshwa.ultrasonic.androidapp.util.Util; import com.thejoshwa.ultrasonic.androidapp.util.Util;
import net.simonvt.menudrawer.MenuDrawer; import net.simonvt.menudrawer.MenuDrawer;
@ -67,6 +76,7 @@ import net.simonvt.menudrawer.Position;
public class SubsonicTabActivity extends Activity implements OnClickListener{ public class SubsonicTabActivity extends Activity implements OnClickListener{
private static final String TAG = SubsonicTabActivity.class.getSimpleName(); private static final String TAG = SubsonicTabActivity.class.getSimpleName();
private static ImageLoader IMAGE_LOADER; private static ImageLoader IMAGE_LOADER;
protected static String theme;
private static SubsonicTabActivity instance; private static SubsonicTabActivity instance;
private boolean destroyed; private boolean destroyed;
@ -78,7 +88,7 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
public MenuDrawer menuDrawer; public MenuDrawer menuDrawer;
private int activePosition = 1; private int activePosition = 1;
private int menuActiveViewId; private int menuActiveViewId;
private View nowPlaying = null; private View nowPlayingView = null;
View searchMenuItem = null; View searchMenuItem = null;
View playlistsMenuItem = null; View playlistsMenuItem = null;
View menuMain = null; View menuMain = null;
@ -93,7 +103,7 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
startService(new Intent(this, DownloadServiceImpl.class)); startService(new Intent(this, DownloadServiceImpl.class));
setVolumeControlStream(AudioManager.STREAM_MUSIC); setVolumeControlStream(AudioManager.STREAM_MUSIC);
if (bundle != null) { if (bundle != null) {
activePosition = bundle.getInt(STATE_ACTIVE_POSITION); activePosition = bundle.getInt(STATE_ACTIVE_POSITION);
menuActiveViewId = bundle.getInt(STATE_ACTIVE_VIEW_ID); menuActiveViewId = bundle.getInt(STATE_ACTIVE_VIEW_ID);
} }
@ -120,23 +130,12 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
if (activeView != null) { if (activeView != null) {
menuDrawer.setActiveView(activeView); menuDrawer.setActiveView(activeView);
} }
}
instance = this;
}
@Override @Override
protected void onPostCreate(Bundle bundle) { protected void onPostCreate(Bundle bundle) {
super.onPostCreate(bundle); super.onPostCreate(bundle);
instance = this;
if (!nowPlayingHidden) {
showNowPlaying();
} else {
hideNowPlaying();
}
int visibility = Util.isOffline(this) ? View.GONE : View.VISIBLE;
searchMenuItem.setVisibility(visibility);
playlistsMenuItem.setVisibility(visibility);
} }
@Override @Override
@ -144,14 +143,19 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
super.onResume(); super.onResume();
applyTheme(); applyTheme();
instance = this; instance = this;
Util.registerMediaButtonEventReceiver(this);
// Make sure to update theme
if (theme != null && !theme.equals(Util.getTheme(this))) {
restart();
}
if (!nowPlayingHidden) { if (!nowPlayingHidden) {
showNowPlaying(); showNowPlaying();
} else { } else {
hideNowPlaying(); hideNowPlaying();
} }
Util.registerMediaButtonEventReceiver(this);
} }
@Override @Override
@ -187,6 +191,13 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
return super.onKeyDown(keyCode, event); 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 @Override
public void finish() { public void finish() {
@ -194,18 +205,22 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
Util.disablePendingTransition(this); Util.disablePendingTransition(this);
} }
private void showNowPlaying() public boolean isDestroyed() {
return destroyed;
}
public void showNowPlaying()
{ {
nowPlaying = findViewById(R.id.now_playing); nowPlayingView = findViewById(R.id.now_playing);
if (!Util.getShowNowPlayingPreference(this)) { if (!Util.getShowNowPlayingPreference(this)) {
if (nowPlaying != null) { if (nowPlayingView != null) {
nowPlaying.setVisibility(View.GONE); nowPlayingView.setVisibility(View.GONE);
} }
return; return;
} }
if (nowPlaying != null) { if (nowPlayingView != null) {
final DownloadService downloadService = DownloadServiceImpl.getInstance(); final DownloadService downloadService = DownloadServiceImpl.getInstance();
if (downloadService != null) { if (downloadService != null) {
@ -222,12 +237,12 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
hideNowPlaying(); hideNowPlaying();
} }
ImageView nowPlayingControlPlay = (ImageView) nowPlaying.findViewById(R.id.now_playing_control_play); ImageView nowPlayingControlPlay = (ImageView) nowPlayingView.findViewById(R.id.now_playing_control_play);
SwipeDetector swipeDetector = SwipeDetector.Create(SubsonicTabActivity.this, downloadService); SwipeDetector swipeDetector = SwipeDetector.Create(SubsonicTabActivity.this, downloadService);
nowPlaying.setOnTouchListener(swipeDetector); nowPlayingView.setOnTouchListener(swipeDetector);
nowPlaying.setOnClickListener(new View.OnClickListener() { nowPlayingView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
} }
@ -253,85 +268,95 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
} }
} }
public void showNowPlaying(final Context context, final DownloadServiceImpl downloadService, final MusicDirectory.Entry song, PlayerState playerState) { private void showNowPlaying(final Context context, final DownloadServiceImpl downloadService, final MusicDirectory.Entry song, final PlayerState playerState) {
nowPlaying = findViewById(R.id.now_playing); this.runOnUiThread( new Runnable() {
@Override
if (!Util.getShowNowPlayingPreference(this)) { public void run() {
if (nowPlaying != null) { nowPlayingView = findViewById(R.id.now_playing);
nowPlaying.setVisibility(View.GONE);
} if (!Util.getShowNowPlayingPreference(SubsonicTabActivity.this)) {
return; if (nowPlayingView != null) {
} nowPlayingView.setVisibility(View.GONE);
}
if (nowPlaying != null) { return;
nowPlaying.setVisibility(View.VISIBLE); }
nowPlayingHidden = false;
if (nowPlayingView != null) {
String title = song.getTitle(); nowPlayingView.setVisibility(View.VISIBLE);
String artist = song.getArtist(); nowPlayingHidden = false;
try { String title = song.getTitle();
ImageView nowPlayingImage = (ImageView) nowPlaying.findViewById(R.id.now_playing_image); String artist = song.getArtist();
TextView nowPlayingTrack = (TextView) nowPlaying.findViewById(R.id.now_playing_trackname);
TextView nowPlayingArtist = (TextView) nowPlaying.findViewById(R.id.now_playing_artist); try {
ImageView nowPlayingImage = (ImageView) nowPlayingView.findViewById(R.id.now_playing_image);
TextView nowPlayingTrack = (TextView) nowPlayingView.findViewById(R.id.now_playing_trackname);
TextView nowPlayingArtist = (TextView) nowPlayingView.findViewById(R.id.now_playing_artist);
DisplayMetrics metrics = context.getResources().getDisplayMetrics(); DisplayMetrics metrics = context.getResources().getDisplayMetrics();
int imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); int imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
int size = 64; int size = 64;
if (imageSizeLarge <= 480) { if (imageSizeLarge <= 480) {
size = 64; size = 64;
} else if (imageSizeLarge <= 768) { } else if (imageSizeLarge <= 768) {
size = 128; size = 128;
} else if (imageSizeLarge <= 1024) { } else if (imageSizeLarge <= 1024) {
size = 256; size = 256;
} else if (imageSizeLarge <= 1080) { } else if (imageSizeLarge <= 1080) {
size = imageSizeLarge; size = imageSizeLarge;
} }
Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size); Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size);
if (bitmap == null) { if (bitmap == null) {
// set default album art // set default album art
nowPlayingImage.setImageResource(R.drawable.unknown_album); nowPlayingImage.setImageResource(R.drawable.unknown_album);
} else { } else {
nowPlayingImage.setImageBitmap(bitmap); nowPlayingImage.setImageBitmap(bitmap);
} }
nowPlayingImage.setOnClickListener(new View.OnClickListener() { nowPlayingImage.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
Intent intent = new Intent(SubsonicTabActivity.this, SelectAlbumActivity.class); Intent intent = new Intent(SubsonicTabActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, song.getParent()); intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, song.getParent());
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum()); intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum());
Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent); Util.startActivityWithoutTransition(SubsonicTabActivity.this, intent);
} }
}); });
nowPlayingTrack.setText(title); nowPlayingTrack.setText(title);
nowPlayingArtist.setText(artist); nowPlayingArtist.setText(artist);
} catch (Exception x) { } catch (Exception x) {
Log.w(TAG, "Failed to get notification cover art", x); Log.w(TAG, "Failed to get notification cover art", x);
} }
ImageView playButton = (ImageView) nowPlaying.findViewById(R.id.now_playing_control_play); ImageView playButton = (ImageView) nowPlayingView.findViewById(R.id.now_playing_control_play);
if (playerState == PlayerState.PAUSED) { if (playerState == PlayerState.PAUSED) {
playButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_play)); playButton.setImageDrawable(Util.getDrawableFromAttribute(SubsonicTabActivity.this, R.attr.media_play));
} else if (playerState == PlayerState.STARTED) { } else if (playerState == PlayerState.STARTED) {
playButton.setImageDrawable(Util.getDrawableFromAttribute(this, R.attr.media_pause)); playButton.setImageDrawable(Util.getDrawableFromAttribute(SubsonicTabActivity.this, R.attr.media_pause));
} }
} }
}
});
} }
public void hideNowPlaying() { public void hideNowPlaying() {
nowPlaying = findViewById(R.id.now_playing); this.runOnUiThread( new Runnable() {
@Override
public void run() {
nowPlayingView = findViewById(R.id.now_playing);
if (nowPlaying != null) { if (nowPlayingView != null) {
nowPlaying.setVisibility(View.GONE); nowPlayingView.setVisibility(View.GONE);
} }
}
});
} }
public static SubsonicTabActivity getInstance() { public static SubsonicTabActivity getInstance() {
@ -384,16 +409,32 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
} }
return IMAGE_LOADER; return IMAGE_LOADER;
} }
public synchronized static ImageLoader getStaticImageLoader(Context context) {
if (IMAGE_LOADER == null) {
IMAGE_LOADER = new ImageLoader(context);
}
return IMAGE_LOADER;
}
protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean playNext) { protected void downloadRecursively(final String id, 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) { 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; private static final int MAX_SONGS = 500;
@Override @Override
protected List<MusicDirectory.Entry> doInBackground() throws Throwable { protected List<MusicDirectory.Entry> doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this); MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
MusicDirectory root = musicService.getMusicDirectory(id, false, SubsonicTabActivity.this, this); MusicDirectory root;
if(isDirectory)
root = musicService.getMusicDirectory(id, name, false, SubsonicTabActivity.this, this);
else
root = musicService.getPlaylist(id, name, SubsonicTabActivity.this, this);
List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>(); List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>();
getSongsRecursively(root, songs); getSongsRecursively(root, songs);
return songs; return songs;
@ -411,7 +452,7 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
} }
for (MusicDirectory.Entry dir : parent.getChildren(true, false)) { for (MusicDirectory.Entry dir : parent.getChildren(true, false)) {
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this); MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
getSongsRecursively(musicService.getMusicDirectory(dir.getId(), false, SubsonicTabActivity.this, this), songs); getSongsRecursively(musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, SubsonicTabActivity.this, this), songs);
} }
} }
@ -419,18 +460,98 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
protected void done(List<MusicDirectory.Entry> songs) { protected void done(List<MusicDirectory.Entry> songs) {
DownloadService downloadService = getDownloadService(); DownloadService downloadService = getDownloadService();
if (!songs.isEmpty() && downloadService != null) { if (!songs.isEmpty() && downloadService != null) {
if (!append && !playNext) { if (!append) {
downloadService.clear(); downloadService.clear();
} }
warnIfNetworkOrStorageUnavailable(); warnIfNetworkOrStorageUnavailable();
downloadService.download(songs, save, autoplay, playNext); if(!background) {
Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class); downloadService.download(songs, save, autoplay, false, shuffle);
if (!append && Util.getShouldTransitionOnPlaybackPreference(SubsonicTabActivity.this)) {
Util.startActivityWithoutTransition(SubsonicTabActivity.this, DownloadActivity.class);
}
}
else {
downloadService.downloadBackground(songs, save);
}
} }
} }
}; };
task.execute(); task.execute();
} }
protected void addToPlaylist(final List<MusicDirectory.Entry> songs) {
if(songs.isEmpty()) {
Util.toast(this, "No songs selected");
return;
}
new LoadingTask<List<Playlist>>(this, true) {
@Override
protected List<Playlist> doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
return musicService.getPlaylists(false, SubsonicTabActivity.this, this);
}
@Override
protected void done(final List<Playlist> playlists) {
List<String> names = new ArrayList<String>();
for(Playlist playlist: playlists) {
names.add(playlist.getName());
}
AlertDialog.Builder builder = new AlertDialog.Builder(SubsonicTabActivity.this);
builder.setTitle("Add to Playlist")
.setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
addToPlaylist(playlists.get(which), songs);
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
@Override
protected void error(Throwable error) {
String msg;
if (error instanceof OfflineException || error instanceof ServerTooOldException) {
msg = getErrorMessage(error);
} else {
msg = getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
}
Util.toast(SubsonicTabActivity.this, msg, false);
}
}.execute();
}
private void addToPlaylist(final Playlist playlist, final List<MusicDirectory.Entry> songs) {
new SilentBackgroundTask<Void>(this) {
@Override
protected Void doInBackground() throws Throwable {
MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this);
musicService.addToPlaylist(playlist.getId(), songs, SubsonicTabActivity.this, null);
return null;
}
@Override
protected void done(Void result) {
Util.toast(SubsonicTabActivity.this, getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName()));
}
@Override
protected void error(Throwable error) {
String msg;
if (error instanceof OfflineException || error instanceof ServerTooOldException) {
msg = getErrorMessage(error);
} else {
msg = getResources().getString(R.string.updated_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
}
Util.toast(SubsonicTabActivity.this, msg, false);
}
}.execute();
}
private void setUncaughtExceptionHandler() { private void setUncaughtExceptionHandler() {
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
@ -481,7 +602,6 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
@Override @Override
public void onClick(View v) { public void onClick(View v) {
//menuDrawer.setActiveView(v);
menuActiveViewId = v.getId(); menuActiveViewId = v.getId();
Intent intent; Intent intent;
@ -619,5 +739,4 @@ public class SubsonicTabActivity extends Activity implements OnClickListener{
return false; return false;
} }
} }
} }

View File

@ -38,6 +38,8 @@ public class EqualizerController {
private final Context context; private final Context context;
private Equalizer equalizer; private Equalizer equalizer;
private boolean released = false;
private int audioSessionId = 0;
// Class initialization fails when this throws an exception. // Class initialization fails when this throws an exception.
static { static {
@ -58,7 +60,8 @@ public class EqualizerController {
public EqualizerController(Context context, MediaPlayer mediaPlayer) { public EqualizerController(Context context, MediaPlayer mediaPlayer) {
this.context = context; this.context = context;
try { try {
equalizer = new Equalizer(0, mediaPlayer.getAudioSessionId()); audioSessionId = mediaPlayer.getAudioSessionId();
equalizer = new Equalizer(0, audioSessionId);
} catch (Throwable x) { } catch (Throwable x) {
Log.w(TAG, "Failed to create equalizer.", x); Log.w(TAG, "Failed to create equalizer.", x);
} }
@ -97,11 +100,21 @@ public class EqualizerController {
public void release() { public void release() {
if (isAvailable()) { if (isAvailable()) {
released = true;
equalizer.release(); equalizer.release();
} }
} }
public Equalizer getEqualizer() { public Equalizer getEqualizer() {
if(released) {
released = false;
try {
equalizer = new Equalizer(0, audioSessionId);
} catch (Throwable x) {
equalizer = null;
Log.w(TAG, "Failed to create equalizer.", x);
}
}
return equalizer; return equalizer;
} }
@ -110,7 +123,7 @@ public class EqualizerController {
/** /**
* *
*/ */
private static final long serialVersionUID = 1477565229521401809L; private static final long serialVersionUID = 626565082425206061L;
private final short[] bandLevels; private final short[] bandLevels;
private short preset; private short preset;
private final boolean enabled; private final boolean enabled;
@ -139,4 +152,3 @@ public class EqualizerController {
} }
} }
} }

View File

@ -35,6 +35,8 @@ public class VisualizerController {
private static final int PREFERRED_CAPTURE_SIZE = 128; // Must be a power of two. private static final int PREFERRED_CAPTURE_SIZE = 128; // Must be a power of two.
private Visualizer visualizer; private Visualizer visualizer;
private boolean released = false;
private int audioSessionId = 0;
// Class initialization fails when this throws an exception. // Class initialization fails when this throws an exception.
static { static {
@ -54,7 +56,8 @@ public class VisualizerController {
public VisualizerController(Context context, MediaPlayer mediaPlayer) { public VisualizerController(Context context, MediaPlayer mediaPlayer) {
try { try {
visualizer = new Visualizer(mediaPlayer.getAudioSessionId()); audioSessionId = mediaPlayer.getAudioSessionId();
visualizer = new Visualizer(audioSessionId);
} catch (Throwable x) { } catch (Throwable x) {
Log.w(TAG, "Failed to create visualizer.", x); Log.w(TAG, "Failed to create visualizer.", x);
} }
@ -78,11 +81,21 @@ public class VisualizerController {
public void release() { public void release() {
if (isAvailable()) { if (isAvailable()) {
visualizer.release(); visualizer.release();
released = true;
} }
} }
public Visualizer getVisualizer() { public Visualizer getVisualizer() {
if(released) {
released = false;
try {
visualizer = new Visualizer(audioSessionId);
} catch (Throwable x) {
visualizer = null;
Log.w(TAG, "Failed to create visualizer.", x);
}
}
return visualizer; return visualizer;
} }
} }

View File

@ -32,6 +32,7 @@ public class Artist implements Serializable {
private String id; private String id;
private String name; private String name;
private String index; private String index;
private int closeness;
public String getId() { public String getId() {
return id; return id;
@ -56,6 +57,14 @@ public class Artist implements Serializable {
public void setIndex(String index) { public void setIndex(String index) {
this.index = index; this.index = index;
} }
public int getCloseness() {
return closeness;
}
public void setCloseness(int closeness) {
this.closeness = closeness;
}
@Override @Override
public String toString() { public String toString() {

View File

@ -18,6 +18,7 @@
*/ */
package com.thejoshwa.ultrasonic.androidapp.domain; package com.thejoshwa.ultrasonic.androidapp.domain;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.io.Serializable; import java.io.Serializable;
@ -29,26 +30,26 @@ public class Indexes implements Serializable {
/** /**
* *
*/ */
private static final long serialVersionUID = -9186563899711652646L; private static final long serialVersionUID = 8156117238598414701L;
private final long lastModified; private final long lastModified;
private final List<Artist> shortcuts; private final List<com.thejoshwa.ultrasonic.androidapp.domain.Artist> shortcuts;
private final List<Artist> artists; private final List<com.thejoshwa.ultrasonic.androidapp.domain.Artist> artists;
public Indexes(long lastModified, List<Artist> shortcuts, List<Artist> artists) { public Indexes(long lastModified, List<com.thejoshwa.ultrasonic.androidapp.domain.Artist> shortcuts, List<com.thejoshwa.ultrasonic.androidapp.domain.Artist> artists) {
this.lastModified = lastModified; this.lastModified = lastModified;
this.shortcuts = shortcuts; this.shortcuts = shortcuts;
this.artists = artists; this.artists = artists;
} }
public long getLastModified() { public long getLastModified() {
return lastModified; return lastModified;
} }
public List<Artist> getShortcuts() { public List<com.thejoshwa.ultrasonic.androidapp.domain.Artist> getShortcuts() {
return shortcuts; return shortcuts;
} }
public List<Artist> getArtists() { public List<com.thejoshwa.ultrasonic.androidapp.domain.Artist> getArtists() {
return artists; return artists;
} }
} }

View File

@ -64,6 +64,10 @@ public class MusicDirectory {
/** /**
* *
*/ */
private static final long serialVersionUID = -3339106650010798108L;
/**
*
*/
private String id; private String id;
private String parent; private String parent;
private boolean directory; private boolean directory;
@ -85,6 +89,7 @@ public class MusicDirectory {
private boolean video; private boolean video;
private boolean starred; private boolean starred;
private Integer discNumber; private Integer discNumber;
private int closeness;
public Integer getDiscNumber() { public Integer getDiscNumber() {
return discNumber; return discNumber;
@ -257,6 +262,14 @@ public class MusicDirectory {
public void setVideo(boolean video) { public void setVideo(boolean video) {
this.video = video; this.video = video;
} }
public int getCloseness() {
return closeness;
}
public void setCloseness(int closeness) {
this.closeness = closeness;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {

View File

@ -28,14 +28,28 @@ public class Playlist implements Serializable {
/** /**
* *
*/ */
private static final long serialVersionUID = -6658599528577231609L; private static final long serialVersionUID = -4160515427075433798L;
private String id; private String id;
private String name; private String name;
private String owner;
private String comment;
private String songCount;
private String created;
private Boolean pub;
public Playlist(String id, String name) { public Playlist(String id, String name) {
this.id = id; this.id = id;
this.name = name; this.name = name;
} }
public Playlist(String id, String name, String owner, String comment, String songCount, String created, String pub) {
this.id = id;
this.name = name;
this.owner = (owner == null) ? "" : owner;
this.comment = (comment == null) ? "" : comment;
this.songCount = (songCount == null) ? "" : songCount;
this.created = (created == null) ? "" : created;
this.pub = (pub == null) ? null : (pub.equals("true"));
}
public String getId() { public String getId() {
return id; return id;
@ -52,6 +66,45 @@ public class Playlist implements Serializable {
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public String getOwner() {
return this.owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getComment() {
return this.comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getSongCount() {
return this.songCount;
}
public void setSongCount(String songCount) {
this.songCount = songCount;
}
public String getCreated() {
return this.created;
}
public void setCreated(String created) {
this.created = created;
}
public Boolean getPublic() {
return this.pub;
}
public void setPublic(Boolean pub) {
this.pub = pub;
}
@Override @Override
public String toString() { public String toString() {

View File

@ -54,7 +54,7 @@ public class CachedMusicService implements MusicService {
private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedMusicDirectories; private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedMusicDirectories;
private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS); private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS);
private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS); private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS);
private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<List<Playlist>>(60, TimeUnit.SECONDS); private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<List<Playlist>>(3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<List<MusicFolder>>(10 * 3600, TimeUnit.SECONDS); private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<List<MusicFolder>>(10 * 3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS); private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS);
private String restUrl; private String restUrl;
@ -82,11 +82,14 @@ public class CachedMusicService implements MusicService {
} }
@Override @Override
public List<MusicFolder> getMusicFolders(Context context, ProgressListener progressListener) throws Exception { public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context); checkSettingsChanged(context);
if (refresh) {
cachedMusicFolders.clear();
}
List<MusicFolder> result = cachedMusicFolders.get(); List<MusicFolder> result = cachedMusicFolders.get();
if (result == null) { if (result == null) {
result = musicService.getMusicFolders(context, progressListener); result = musicService.getMusicFolders(refresh, context, progressListener);
cachedMusicFolders.set(result); cachedMusicFolders.set(result);
} }
return result; return result;
@ -109,12 +112,12 @@ public class CachedMusicService implements MusicService {
} }
@Override @Override
public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context); checkSettingsChanged(context);
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(id); TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(id);
MusicDirectory dir = cache == null ? null : cache.get(); MusicDirectory dir = cache == null ? null : cache.get();
if (dir == null) { if (dir == null) {
dir = musicService.getMusicDirectory(id, refresh, context, progressListener); dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener);
cache = new TimeLimitedCache<MusicDirectory>(TTL_MUSIC_DIR, TimeUnit.SECONDS); cache = new TimeLimitedCache<MusicDirectory>(TTL_MUSIC_DIR, TimeUnit.SECONDS);
cache.set(dir); cache.set(dir);
cachedMusicDirectories.put(id, cache); cachedMusicDirectories.put(id, cache);
@ -128,8 +131,8 @@ public class CachedMusicService implements MusicService {
} }
@Override @Override
public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
return musicService.getPlaylist(id, context, progressListener); return musicService.getPlaylist(id, name, context, progressListener);
} }
@Override @Override
@ -145,9 +148,30 @@ public class CachedMusicService implements MusicService {
@Override @Override
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception { public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
cachedPlaylists.clear();
musicService.createPlaylist(id, name, entries, context, progressListener); musicService.createPlaylist(id, name, entries, context, progressListener);
} }
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
musicService.deletePlaylist(id, context, progressListener);
}
@Override
public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
musicService.addToPlaylist(id, toAdd, context, progressListener);
}
@Override
public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
musicService.removeFromPlaylist(id, toRemove, context, progressListener);
}
@Override
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
musicService.updatePlaylist(id, name, comment, pub, context, progressListener);
}
@Override @Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
return musicService.getLyrics(artist, title, context, progressListener); return musicService.getLyrics(artist, title, context, progressListener);
@ -165,7 +189,7 @@ public class CachedMusicService implements MusicService {
@Override @Override
public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception {
return musicService.getRandomSongs(size, context, progressListener); return musicService.getRandomSongs(size, context, progressListener);
} }
@Override @Override
@ -194,8 +218,13 @@ public class CachedMusicService implements MusicService {
} }
@Override @Override
public String getVideoUrl(Context context, String id) { public String getVideoUrl(int maxBitrate, Context context, String id) {
return musicService.getVideoUrl(context, id); return musicService.getVideoUrl(maxBitrate, context, id);
}
@Override
public String getVideoStreamUrl(int maxBitrate, Context context, String id) {
return musicService.getVideoStreamUrl(maxBitrate, context, id);
} }
@Override @Override

View File

@ -25,6 +25,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import android.content.Context; import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager; import android.os.PowerManager;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
@ -56,6 +57,9 @@ public class DownloadFile {
private boolean save; private boolean save;
private boolean failed; private boolean failed;
private int bitRate; private int bitRate;
private boolean isPlaying = false;
private boolean saveWhenDone = false;
private boolean completeWhenDone = false;
public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) { public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
this.context = context; this.context = context;
@ -63,40 +67,36 @@ public class DownloadFile {
this.save = save; this.save = save;
saveFile = FileUtil.getSongFile(context, song); saveFile = FileUtil.getSongFile(context, song);
bitRate = Util.getMaxBitrate(context); bitRate = Util.getMaxBitrate(context);
partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + "." + bitRate + ".partial." + FileUtil.getExtension(saveFile.getName())); partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + ".complete." + FileUtil.getExtension(saveFile.getName())); ".partial." + FileUtil.getExtension(saveFile.getName()));
completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
".complete." + FileUtil.getExtension(saveFile.getName()));
mediaStoreService = new MediaStoreService(context); mediaStoreService = new MediaStoreService(context);
} }
public MusicDirectory.Entry getSong() { public MusicDirectory.Entry getSong() {
return song; return song;
} }
public boolean isOffline() {
return Util.isOffline(context);
}
public boolean isStreamProxyEnabled() {
return Util.isStreamProxyEnabled(context);
}
/** /**
* Returns the effective bit rate. * Returns the effective bit rate.
*/ */
public int getBitRate() { public int getBitRate() {
if(!partialFile.exists()) {
bitRate = Util.getMaxBitrate(context);
}
if (bitRate > 0) { if (bitRate > 0) {
return bitRate; return bitRate;
} }
return song.getBitRate() == null ? 160 : song.getBitRate(); return song.getBitRate() == null ? 160 : song.getBitRate();
} }
public int getBufferLength() {
return Util.getBufferLength(this.context);
}
public synchronized void download() { public synchronized void download() {
FileUtil.createDirectoryForParent(saveFile); FileUtil.createDirectoryForParent(saveFile);
failed = false; failed = false;
if(!partialFile.exists()) {
bitRate = Util.getMaxBitrate(context);
}
downloadTask = new DownloadTask(); downloadTask = new DownloadTask();
downloadTask.start(); downloadTask.start();
} }
@ -132,7 +132,7 @@ public class DownloadFile {
} }
public synchronized boolean isWorkDone() { public synchronized boolean isWorkDone() {
return saveFile.exists() || (completeFile.exists() && !save); return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
} }
public synchronized boolean isDownloading() { public synchronized boolean isDownloading() {
@ -191,6 +191,30 @@ public class DownloadFile {
} }
} }
} }
public void setPlaying(boolean isPlaying) {
try {
if(saveWhenDone && isPlaying == false) {
Util.renameFile(completeFile, saveFile);
saveWhenDone = false;
} else if(completeWhenDone && isPlaying == false) {
if(save) {
Util.renameFile(partialFile, saveFile);
mediaStoreService.saveInMediaStore(DownloadFile.this);
} else {
Util.renameFile(partialFile, completeFile);
}
completeWhenDone = false;
}
} catch(IOException ex) {
Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile);
}
this.isPlaying = isPlaying;
}
public boolean getPlaying() {
return isPlaying;
}
@Override @Override
public String toString() { public String toString() {
@ -205,7 +229,7 @@ public class DownloadFile {
InputStream in = null; InputStream in = null;
FileOutputStream out = null; FileOutputStream out = null;
PowerManager.WakeLock wakeLock = null; PowerManager.WakeLock wakeLock = null;
WifiManager.WifiLock wifiLock = null;
try { try {
if (Util.isScreenLitOnDownload(context)) { if (Util.isScreenLitOnDownload(context)) {
@ -214,52 +238,68 @@ public class DownloadFile {
wakeLock.acquire(); wakeLock.acquire();
Log.i(TAG, "Acquired wake lock " + wakeLock); Log.i(TAG, "Acquired wake lock " + wakeLock);
} }
wifiLock = Util.createWifiLock(context, toString());
wifiLock.acquire();
if (saveFile.exists() && saveFile.length() == song.getSize()) { if (saveFile.exists()) {
Log.i(TAG, saveFile + " already exists. Skipping."); Log.i(TAG, saveFile + " already exists. Skipping.");
return; return;
} }
if (completeFile.exists() && completeFile.length() == song.getSize()) { if (completeFile.exists()) {
if (save) { if (save) {
Util.atomicCopy(completeFile, saveFile); if(isPlaying) {
saveWhenDone = true;
} else {
Util.renameFile(completeFile, saveFile);
}
} else { } else {
Log.i(TAG, completeFile + " already exists. Skipping."); Log.i(TAG, completeFile + " already exists. Skipping.");
} }
return; return;
} }
if (isOffline()) {
Log.i(TAG, "We are offline. Skipping.");
return;
}
MusicService musicService = MusicServiceFactory.getMusicService(context); MusicService musicService = MusicServiceFactory.getMusicService(context);
// Attempt partial HTTP GET, appending to the file if it exists. // Some devices seem to throw error on partial file which doesn't exist
HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this); boolean compare;
in = response.getEntity().getContent(); try {
boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT; compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
if (partial) { } catch(Exception e) {
Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes"); compare = true;
} }
if(compare) {
// 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); out = new FileOutputStream(partialFile, partial);
long n = copy(in, out); long n = copy(in, out);
Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile); Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
out.flush(); out.flush();
out.close(); out.close();
if (isCancelled()) { if (isCancelled()) {
throw new Exception("Download of '" + song + "' was cancelled"); throw new Exception("Download of '" + song + "' was cancelled");
} }
downloadAndSaveCoverArt(musicService); downloadAndSaveCoverArt(musicService);
}
if (save) { if(isPlaying) {
Util.atomicCopy(partialFile, saveFile); completeWhenDone = true;
mediaStoreService.saveInMediaStore(DownloadFile.this); } else {
} else { if(save) {
Util.atomicCopy(partialFile, completeFile); Util.renameFile(partialFile, saveFile);
} mediaStoreService.saveInMediaStore(DownloadFile.this);
} else {
Util.renameFile(partialFile, completeFile);
}
}
} catch (Exception x) { } catch (Exception x) {
Util.close(out); Util.close(out);
@ -277,7 +317,13 @@ public class DownloadFile {
wakeLock.release(); wakeLock.release();
Log.i(TAG, "Released wake lock " + wakeLock); Log.i(TAG, "Released wake lock " + wakeLock);
} }
new CacheCleaner(context, DownloadServiceImpl.getInstance()).clean(); if (wifiLock != null) {
wifiLock.release();
}
new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace();
if(DownloadServiceImpl.getInstance() != null) {
((DownloadServiceImpl)DownloadServiceImpl.getInstance()).checkDownloads();
}
} }
} }
@ -336,4 +382,4 @@ public class DownloadFile {
return count; return count;
} }
} }
} }

View File

@ -32,7 +32,9 @@ import com.thejoshwa.ultrasonic.androidapp.domain.RepeatMode;
*/ */
public interface DownloadService { public interface DownloadService {
void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext); void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle);
void downloadBackground(List<MusicDirectory.Entry> songs, boolean save);
void setShufflePlayEnabled(boolean enabled); void setShufflePlayEnabled(boolean enabled);
@ -49,18 +51,30 @@ public interface DownloadService {
void setKeepScreenOn(boolean screenOn); void setKeepScreenOn(boolean screenOn);
boolean getShowVisualization(); boolean getShowVisualization();
boolean getEqualizerAvailable();
boolean getVisualizerAvailable();
void setShowVisualization(boolean showVisualization); void setShowVisualization(boolean showVisualization);
void clear(); void clear();
void clearBackground();
void clearIncomplete(); void clearIncomplete();
int size(); int size();
void remove(int which);
void remove(DownloadFile downloadFile); void remove(DownloadFile downloadFile);
List<DownloadFile> getSongs();
List<DownloadFile> getDownloads(); List<DownloadFile> getDownloads();
List<DownloadFile> getBackgroundDownloads();
int getCurrentPlayingIndex(); int getCurrentPlayingIndex();
@ -113,4 +127,8 @@ public interface DownloadService {
void adjustJukeboxVolume(boolean up); void adjustJukeboxVolume(boolean up);
void togglePlayPause(); void togglePlayPause();
void setVolume(float volume);
void swap(boolean mainList, int from, int to);
} }

View File

@ -24,11 +24,14 @@ import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.RemoteControlClient;
import android.os.AsyncTask;
import android.telephony.PhoneStateListener; import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
@ -53,6 +56,7 @@ public class DownloadServiceLifecycleSupport {
private BroadcastReceiver ejectEventReceiver; private BroadcastReceiver ejectEventReceiver;
private PhoneStateListener phoneStateListener; private PhoneStateListener phoneStateListener;
private boolean externalStorageAvailable= true; private boolean externalStorageAvailable= true;
private ReentrantLock lock = new ReentrantLock();
/** /**
* This receiver manages the intent that could come from other applications. * This receiver manages the intent that could come from other applications.
@ -73,7 +77,7 @@ public class DownloadServiceLifecycleSupport {
} else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) { } else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) {
downloadService.pause(); downloadService.pause();
} else if (DownloadServiceImpl.CMD_STOP.equals(action)) { } else if (DownloadServiceImpl.CMD_STOP.equals(action)) {
downloadService.stop(); downloadService.pause();
downloadService.seekTo(0); downloadService.seekTo(0);
} }
} }
@ -96,7 +100,7 @@ public class DownloadServiceLifecycleSupport {
} }
}; };
executorService = Executors.newScheduledThreadPool(2); executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS);
// Pause when headset is unplugged. // Pause when headset is unplugged.
@ -105,7 +109,9 @@ public class DownloadServiceLifecycleSupport {
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Headset event for: " + intent.getExtras().get("name")); Log.i(TAG, "Headset event for: " + intent.getExtras().get("name"));
if (intent.getExtras().getInt("state") == 0) { if (intent.getExtras().getInt("state") == 0) {
downloadService.pause(); if(!downloadService.isJukeboxEnabled()) {
downloadService.pause();
}
} }
} }
}; };
@ -163,12 +169,11 @@ public class DownloadServiceLifecycleSupport {
public void onDestroy() { public void onDestroy() {
executorService.shutdown(); executorService.shutdown();
serializeDownloadQueue(); serializeDownloadQueueNow();
downloadService.clear(false); downloadService.clear(false);
downloadService.unregisterReceiver(ejectEventReceiver); downloadService.unregisterReceiver(ejectEventReceiver);
downloadService.unregisterReceiver(headsetEventReceiver); downloadService.unregisterReceiver(headsetEventReceiver);
downloadService.unregisterReceiver(intentReceiver); downloadService.unregisterReceiver(intentReceiver);
Util.unregisterMediaButtonEventReceiver(downloadService);
TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
@ -179,18 +184,26 @@ public class DownloadServiceLifecycleSupport {
} }
public void serializeDownloadQueue() { public void serializeDownloadQueue() {
State state = new State(); new SerializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
for (DownloadFile downloadFile : downloadService.getDownloads()) { }
state.songs.add(downloadFile.getSong());
} public void serializeDownloadQueueNow() {
state.currentPlayingIndex = downloadService.getCurrentPlayingIndex(); List<DownloadFile> songs = new ArrayList<DownloadFile>(downloadService.getSongs());
state.currentPlayingPosition = downloadService.getPlayerPosition(); State state = new State();
for (DownloadFile downloadFile : songs) {
state.songs.add(downloadFile.getSong());
}
state.currentPlayingIndex = downloadService.getCurrentPlayingIndex();
state.currentPlayingPosition = downloadService.getPlayerPosition();
Log.i(TAG, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); Log.i(TAG, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition);
FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER);
} }
private void deserializeDownloadQueue() { private void deserializeDownloadQueue() {
new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void deserializeDownloadQueueNow() {
State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER); State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER);
if (state == null) { if (state == null) {
return; return;
@ -200,33 +213,37 @@ public class DownloadServiceLifecycleSupport {
// Work-around: Serialize again, as the restore() method creates a serialization without current playing info. // Work-around: Serialize again, as the restore() method creates a serialization without current playing info.
serializeDownloadQueue(); serializeDownloadQueue();
downloadService.setPlayerState(PlayerState.STOPPED);
} }
private void handleKeyEvent(KeyEvent event) { private void handleKeyEvent(KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) { if (event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() > 0) {
return; return;
} }
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PAUSE: downloadService.togglePlayPause();
case KeyEvent.KEYCODE_HEADSETHOOK: break;
downloadService.togglePlayPause(); case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
break; downloadService.previous();
case KeyEvent.KEYCODE_MEDIA_PREVIOUS: break;
downloadService.previous(); case KeyEvent.KEYCODE_MEDIA_NEXT:
break; if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) {
case KeyEvent.KEYCODE_MEDIA_NEXT: downloadService.next();
if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) { }
downloadService.next(); break;
} case KeyEvent.KEYCODE_MEDIA_STOP:
break; downloadService.stop();
case KeyEvent.KEYCODE_MEDIA_STOP: break;
downloadService.reset(); case KeyEvent.KEYCODE_MEDIA_PLAY:
break; if(downloadService.getPlayerState() != PlayerState.STARTED) {
downloadService.start();
}
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
downloadService.pause();
break;
default: default:
break; break;
} }
@ -244,7 +261,7 @@ public class DownloadServiceLifecycleSupport {
switch (state) { switch (state) {
case TelephonyManager.CALL_STATE_RINGING: case TelephonyManager.CALL_STATE_RINGING:
case TelephonyManager.CALL_STATE_OFFHOOK: case TelephonyManager.CALL_STATE_OFFHOOK:
if (downloadService.getPlayerState() == PlayerState.STARTED) { if (downloadService.getPlayerState() == PlayerState.STARTED && !downloadService.isJukeboxEnabled()) {
resumeAfterCall = true; resumeAfterCall = true;
downloadService.pause(); downloadService.pause();
} }
@ -268,4 +285,30 @@ public class DownloadServiceLifecycleSupport {
private int currentPlayingIndex; private int currentPlayingIndex;
private int currentPlayingPosition; private int currentPlayingPosition;
} }
private class SerializeTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if(lock.tryLock()) {
try {
serializeDownloadQueueNow();
} finally {
lock.unlock();
}
}
return null;
}
}
private class DeserializeTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
try {
lock.lock();
deserializeDownloadQueueNow();
} finally {
lock.unlock();
}
return null;
}
}
} }

View File

@ -47,26 +47,34 @@ public interface MusicService {
boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception; boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception;
List<MusicFolder> getMusicFolders(Context context, ProgressListener progressListener) throws Exception;
List<Genre> getGenres(Context context, ProgressListener progressListener) throws Exception; List<Genre> getGenres(Context context, ProgressListener progressListener) throws Exception;
void star(String id, Context context, ProgressListener progressListener) throws Exception; void star(String id, Context context, ProgressListener progressListener) throws Exception;
void unstar(String id, Context context, ProgressListener progressListener) throws Exception; void unstar(String id, Context context, ProgressListener progressListener) throws Exception;
List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
Indexes getIndexes(String musicFolderId, 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; MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception; SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception; MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception;
List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception; List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception; void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception;
void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception;
void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception;
void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception;
void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception;
Lyrics getLyrics(String artist, String title, 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; void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception;
@ -87,7 +95,9 @@ public interface MusicService {
Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception; Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception;
String getVideoUrl(Context context, String id); String getVideoUrl(int maxBitrate, Context context, String id);
String getVideoStreamUrl(int Bitrate, Context context, String id);
JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception; JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception;

View File

@ -18,11 +18,15 @@
*/ */
package com.thejoshwa.ultrasonic.androidapp.service; package com.thejoshwa.ultrasonic.androidapp.service;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -54,7 +58,8 @@ import com.thejoshwa.ultrasonic.androidapp.util.Util;
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class OfflineMusicService extends RESTMusicService { public class OfflineMusicService extends RESTMusicService {
private static final String TAG = OfflineMusicService.class.getSimpleName();
@Override @Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
return true; return true;
@ -86,14 +91,14 @@ public class OfflineMusicService extends RESTMusicService {
} }
@Override @Override
public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
File dir = new File(id); File dir = new File(id);
MusicDirectory result = new MusicDirectory(); MusicDirectory result = new MusicDirectory();
result.setName(dir.getName()); result.setName(dir.getName());
Set<String> names = new HashSet<String>(); Set<String> names = new HashSet<String>();
for (File file : FileUtil.listMusicFiles(dir)) { for (File file : FileUtil.listMediaFiles(dir)) {
String name = getName(file); String name = getName(file);
if (name != null & !names.contains(name)) { if (name != null & !names.contains(name)) {
names.add(name); names.add(name);
@ -253,15 +258,11 @@ public class OfflineMusicService extends RESTMusicService {
@Override @Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception { public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception {
InputStream in = new FileInputStream(entry.getCoverArt()); try {
try { return FileUtil.getAlbumArtBitmap(context, entry, size);
byte[] bytes = Util.toByteArray(in); } catch(Exception e) {
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); return null;
Log.i("getCoverArt", "getCoverArt"); }
return Util.scaleBitmap(bitmap, size);
} finally {
Util.close(in);
}
} }
@Override @Override
@ -275,29 +276,210 @@ public class OfflineMusicService extends RESTMusicService {
} }
@Override @Override
public List<MusicFolder> getMusicFolders(Context context, ProgressListener progressListener) throws Exception { public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Music folders not available in offline mode"); throw new OfflineException("Music folders not available in offline mode");
} }
@Override @Override
public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Search not available in offline mode"); List<Artist> artists = new ArrayList<Artist>();
List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
File root = FileUtil.getMusicDirectory(context);
int closeness = 0;
for (File artistFile : FileUtil.listFiles(root)) {
String artistName = artistFile.getName();
if (artistFile.isDirectory()) {
if((closeness = matchCriteria(criteria, artistName)) > 0) {
Artist artist = new Artist();
artist.setId(artistFile.getPath());
artist.setIndex(artistFile.getName().substring(0, 1));
artist.setName(artistName);
artist.setCloseness(closeness);
artists.add(artist);
}
recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs);
}
}
Collections.sort(artists, new Comparator<Artist>() {
public int compare(Artist lhs, Artist rhs) {
if(lhs.getCloseness() == rhs.getCloseness()) {
return 0;
}
else if(lhs.getCloseness() > rhs.getCloseness()) {
return -1;
}
else {
return 1;
}
}
});
Collections.sort(albums, new Comparator<MusicDirectory.Entry>() {
public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) {
if(lhs.getCloseness() == rhs.getCloseness()) {
return 0;
}
else if(lhs.getCloseness() > rhs.getCloseness()) {
return -1;
}
else {
return 1;
}
}
});
Collections.sort(songs, new Comparator<MusicDirectory.Entry>() {
public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) {
if(lhs.getCloseness() == rhs.getCloseness()) {
return 0;
}
else if(lhs.getCloseness() > rhs.getCloseness()) {
return -1;
}
else {
return 1;
}
}
});
return new SearchResult(artists, albums, songs);
} }
private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) {
int closeness;
for(File albumFile : FileUtil.listMediaFiles(file)) {
if(albumFile.isDirectory()) {
String albumName = getName(albumFile);
if((closeness = matchCriteria(criteria, albumName)) > 0) {
MusicDirectory.Entry album = createEntry(context, albumFile, albumName);
album.setArtist(artistName);
album.setCloseness(closeness);
albums.add(album);
}
for(File songFile : FileUtil.listMediaFiles(albumFile)) {
String songName = getName(songFile);
if(songFile.isDirectory()) {
recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs);
}
else if((closeness = matchCriteria(criteria, songName)) > 0){
MusicDirectory.Entry song = createEntry(context, albumFile, songName);
song.setArtist(artistName);
song.setAlbum(albumName);
song.setCloseness(closeness);
songs.add(song);
}
}
}
else {
String songName = getName(albumFile);
if((closeness = matchCriteria(criteria, songName)) > 0) {
MusicDirectory.Entry song = createEntry(context, albumFile, songName);
song.setArtist(artistName);
song.setAlbum(songName);
song.setCloseness(closeness);
songs.add(song);
}
}
}
}
private int matchCriteria(SearchCritera criteria, String name) {
String query = criteria.getQuery().toLowerCase();
String[] queryParts = query.split(" ");
String[] nameParts = name.toLowerCase().split(" ");
int closeness = 0;
for(String queryPart : queryParts) {
for(String namePart : nameParts) {
if(namePart.equals(queryPart)) {
closeness++;
}
}
}
return closeness;
}
@Override @Override
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Playlists not available in offline mode"); List<Playlist> playlists = new ArrayList<Playlist>();
File root = FileUtil.getPlaylistDirectory();
for (File file : FileUtil.listFiles(root)) {
if(FileUtil.isPlaylistFile(file)) {
String id = file.getName();
String filename = FileUtil.getBaseName(id);
Playlist playlist = new Playlist(id, filename);
playlists.add(playlist);
} else {
// Delete legacy playlist files
try {
file.delete();
} catch(Exception e) {
Log.w(TAG, "Failed to delete old playlist file: " + file.getName());
}
}
}
return playlists;
} }
@Override @Override
public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Playlists not available in offline mode"); DownloadService downloadService = DownloadServiceImpl.getInstance();
if (downloadService == null) {
return new MusicDirectory();
}
Reader reader = null;
BufferedReader buffer = null;
try {
File playlistFile = FileUtil.getPlaylistFile(name);
reader = new FileReader(playlistFile);
buffer = new BufferedReader(reader);
MusicDirectory playlist = new MusicDirectory();
String line = buffer.readLine();
if(!"#EXTM3U".equals(line)) return playlist;
while( (line = buffer.readLine()) != null ){
File entryFile = new File(line);
String entryName = getName(entryFile);
if(entryFile.exists() && entryName != null){
playlist.addChild(createEntry(context, entryFile, entryName));
}
}
return playlist;
} finally {
Util.close(buffer);
Util.close(reader);
}
} }
@Override @Override
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception { public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Playlists not available in offline mode"); throw new OfflineException("Playlists not available in offline mode");
} }
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Playlists not available in offline mode");
}
@Override
public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Updating playlist not available in offline mode");
}
@Override
public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Removing from playlist not available in offline mode");
}
@Override
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException("Updating playlist not available in offline mode");
}
@Override @Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
@ -315,7 +497,12 @@ public class OfflineMusicService extends RESTMusicService {
} }
@Override @Override
public String getVideoUrl(Context context, String id) { public String getVideoUrl(int maxBitrate, Context context, String id) {
return null;
}
@Override
public String getVideoStreamUrl(int maxBitrate, Context context, String id) {
return null; return null;
} }
@ -384,7 +571,7 @@ public class OfflineMusicService extends RESTMusicService {
} }
private void listFilesRecursively(File parent, List<File> children) { private void listFilesRecursively(File parent, List<File> children) {
for (File file : FileUtil.listMusicFiles(parent)) { for (File file : FileUtil.listMediaFiles(parent)) {
if (file.isFile()) { if (file.isFile()) {
children.add(file); children.add(file);
} else { } else {

View File

@ -18,7 +18,10 @@
*/ */
package com.thejoshwa.ultrasonic.androidapp.service; package com.thejoshwa.ultrasonic.androidapp.service;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -174,13 +177,6 @@ public class RESTMusicService implements MusicService {
Reader reader = getReader(context, progressListener, "ping", null); Reader reader = getReader(context, progressListener, "ping", null);
try { try {
new ErrorParser(context).parse(reader); new ErrorParser(context).parse(reader);
XmlPullParser parser;
parser = Xml.newPullParser();
parser.setInput(reader);
String version = parser.getAttributeValue(null, "version");
if (version != null) {
Util.setServerRestVersion(context, new Version(version));
}
} finally { } finally {
Util.close(reader); Util.close(reader);
} }
@ -197,15 +193,91 @@ public class RESTMusicService implements MusicService {
} }
} }
public List<MusicFolder> getMusicFolders(Context context, ProgressListener progressListener) throws Exception { public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
List<MusicFolder> cachedMusicFolders = readCachedMusicFolders(context);
if (cachedMusicFolders != null && !refresh) {
return cachedMusicFolders;
}
Reader reader = getReader(context, progressListener, "getMusicFolders", null); Reader reader = getReader(context, progressListener, "getMusicFolders", null);
try { try {
return new MusicFoldersParser(context).parse(reader, progressListener); List<MusicFolder> musicFolders = new MusicFoldersParser(context).parse(reader, progressListener);
writeCachedMusicFolders(context, musicFolders);
return musicFolders;
} finally { } finally {
Util.close(reader); 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 || refresh) ? 0L : cachedIndexes.getLastModified();
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
if(lastModified != 0L) {
parameterNames.add("ifModifiedSince");
parameterValues.add(lastModified);
}
if (musicFolderId != null) {
parameterNames.add("musicFolderId");
parameterValues.add(musicFolderId);
}
Reader reader = getReader(context, progressListener, "getIndexes", null, parameterNames, parameterValues);
try {
Indexes indexes = new IndexesParser(context).parse(reader, progressListener);
if (indexes != null) {
writeCachedIndexes(context, indexes, musicFolderId);
return indexes;
}
if(cachedIndexes != null) {
return cachedIndexes;
} else {
return new Indexes(0, new ArrayList<com.thejoshwa.ultrasonic.androidapp.domain.Artist>(), new ArrayList<com.thejoshwa.ultrasonic.androidapp.domain.Artist>());
}
} finally {
Util.close(reader);
}
}
private Indexes readCachedIndexes(Context context, String musicFolderId) {
String filename = getCachedIndexesFilename(context, musicFolderId);
return FileUtil.deserialize(context, filename);
}
private void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) {
String filename = getCachedIndexesFilename(context, musicFolderId);
FileUtil.serialize(context, indexes, filename);
}
private String getCachedIndexesFilename(Context context, String musicFolderId) {
String s = Util.getRestUrl(context, null) + musicFolderId;
return "indexes-" + Math.abs(s.hashCode()) + ".ser";
}
private ArrayList<MusicFolder> readCachedMusicFolders(Context context) {
String filename = getCachedMusicFoldersFilename(context);
return FileUtil.deserialize(context, filename);
}
private void writeCachedMusicFolders(Context context, List<MusicFolder> musicFolders) {
String filename = getCachedMusicFoldersFilename(context);
FileUtil.serialize(context, new ArrayList<MusicFolder>(musicFolders), filename);
}
private String getCachedMusicFoldersFilename(Context context) {
String s = Util.getRestUrl(context, null);
return "musicFolders-" + Math.abs(s.hashCode()) + ".ser";
}
public void star(String id, Context context, ProgressListener progressListener) throws Exception { public void star(String id, Context context, ProgressListener progressListener) throws Exception {
Reader reader = getReader(context, progressListener, "star", null, "id", id); Reader reader = getReader(context, progressListener, "star", null, "id", id);
try { try {
@ -225,54 +297,10 @@ public class RESTMusicService implements MusicService {
} }
@Override @Override
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
Indexes cachedIndexes = readCachedIndexes(context, musicFolderId);
long lastModified = cachedIndexes == null ? 0L : cachedIndexes.getLastModified();
List<String> parameterNames = new ArrayList<String>();
List<Object> parameterValues = new ArrayList<Object>();
parameterNames.add("ifModifiedSince");
parameterValues.add(lastModified);
if (musicFolderId != null) {
parameterNames.add("musicFolderId");
parameterValues.add(musicFolderId);
}
Reader reader = getReader(context, progressListener, "getIndexes", null, parameterNames, parameterValues);
try {
Indexes indexes = new IndexesParser(context).parse(reader, progressListener);
if (indexes != null) {
writeCachedIndexes(context, indexes, musicFolderId);
return indexes;
}
return cachedIndexes;
} finally {
Util.close(reader);
}
}
private Indexes readCachedIndexes(Context context, String musicFolderId) {
String filename = getCachedIndexesFilename(context, musicFolderId);
return FileUtil.deserialize(context, filename);
}
private void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) {
String filename = getCachedIndexesFilename(context, musicFolderId);
FileUtil.serialize(context, indexes, filename);
}
private String getCachedIndexesFilename(Context context, String musicFolderId) {
String s = Util.getRestUrl(context, null) + musicFolderId;
return "indexes-" + Math.abs(s.hashCode()) + ".ser";
}
@Override
public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id); Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id);
try { try {
return new MusicDirectoryParser(context).parse(reader, progressListener); return new MusicDirectoryParser(context).parse(name, reader, progressListener);
} finally { } finally {
Util.close(reader); Util.close(reader);
} }
@ -320,18 +348,41 @@ public class RESTMusicService implements MusicService {
} }
@Override @Override
public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception {
HttpParams params = new BasicHttpParams(); HttpParams params = new BasicHttpParams();
HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST); HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST);
Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id); Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id);
try { try {
return new PlaylistParser(context).parse(reader, progressListener); MusicDirectory playlist = new PlaylistParser(context).parse(reader, progressListener);
File playlistFile = FileUtil.getPlaylistFile(name);
FileWriter fw = new FileWriter(playlistFile);
BufferedWriter bw = new BufferedWriter(fw);
try {
fw.write("#EXTM3U\n");
for (MusicDirectory.Entry e : playlist.getChildren()) {
String filePath = FileUtil.getSongFile(context, e).getAbsolutePath();
if(! new File(filePath).exists()){
String ext = FileUtil.getExtension(filePath);
String base = FileUtil.getBaseName(filePath);
filePath = base + ".complete." + ext;
}
fw.write(filePath + "\n");
}
} catch(Exception e) {
Log.w(TAG, "Failed to save playlist: " + name);
} finally {
bw.close();
fw.close();
}
return playlist;
} finally { } finally {
Util.close(reader); Util.close(reader);
} }
} }
@Override @Override
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
Reader reader = getReader(context, progressListener, "getPlaylists", null); Reader reader = getReader(context, progressListener, "getPlaylists", null);
@ -367,6 +418,65 @@ public class RESTMusicService implements MusicService {
Util.close(reader); Util.close(reader);
} }
} }
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id);
try {
new ErrorParser(context).parse(reader);
} finally {
Util.close(reader);
}
}
@Override
public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.8", "Updating playlists is not supported.");
List<String> names = new ArrayList<String>();
List<Object> values = new ArrayList<Object>();
names.add("playlistId");
values.add(id);
for(MusicDirectory.Entry song: toAdd) {
names.add("songIdToAdd");
values.add(song.getId());
}
Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
try {
new ErrorParser(context).parse(reader);
} finally {
Util.close(reader);
}
}
@Override
public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.8", "Updating playlists is not supported.");
List<String> names = new ArrayList<String>();
List<Object> values = new ArrayList<Object>();
names.add("playlistId");
values.add(id);
for(Integer song: toRemove) {
names.add("songIndexToRemove");
values.add(song);
}
Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values);
try {
new ErrorParser(context).parse(reader);
} finally {
Util.close(reader);
}
}
@Override
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
checkServerVersion(context, "1.8", "Updating playlists is not supported.");
Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub));
try {
new ErrorParser(context).parse(reader);
} finally {
Util.close(reader);
}
}
@Override @Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
@ -404,8 +514,14 @@ public class RESTMusicService implements MusicService {
public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception {
HttpParams params = new BasicHttpParams(); HttpParams params = new BasicHttpParams();
HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS);
List<String> names = new ArrayList<String>();
List<Object> values = new ArrayList<Object>();
Reader reader = getReader(context, progressListener, "getRandomSongs", params, "size", size); names.add("size");
values.add(size);
Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values);
try { try {
return new RandomSongsParser(context).parse(reader, progressListener); return new RandomSongsParser(context).parse(reader, progressListener);
} finally { } finally {
@ -487,6 +603,17 @@ public class RESTMusicService implements MusicService {
byte[] bytes = Util.toByteArray(in); byte[] bytes = Util.toByteArray(in);
File albumDir = FileUtil.getAlbumDirectory(context, entry);
if (albumDir.exists()) {
OutputStream out = null;
try {
out = new FileOutputStream(FileUtil.getAlbumArtFile(albumDir));
out.write(bytes);
} finally {
Util.close(out);
}
}
// If we aren't allowing server-side scaling, always save the file to disk because it will be unmodified // If we aren't allowing server-side scaling, always save the file to disk because it will be unmodified
if (!serverScaling || saveToFile) { if (!serverScaling || saveToFile) {
OutputStream out = null; OutputStream out = null;
@ -545,16 +672,27 @@ public class RESTMusicService implements MusicService {
} }
@Override @Override
public String getVideoUrl(Context context, String id) { public String getVideoUrl(int maxBitrate, Context context, String id) {
StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "videoPlayer")); StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "videoPlayer"));
builder.append("&id=").append(id); builder.append("&id=").append(id);
builder.append("&maxBitRate=500"); builder.append("&maxBitRate=").append(maxBitrate);
builder.append("&autoplay=true"); builder.append("&autoplay=true");
String url = rewriteUrlWithRedirect(context, builder.toString()); String url = rewriteUrlWithRedirect(context, builder.toString());
Log.i(TAG, "Using video URL: " + url); Log.i(TAG, "Using video URL: " + url);
return url; return url;
} }
@Override
public String getVideoStreamUrl(int maxBitrate, Context context, String id) {
StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "stream"));
builder.append("&id=").append(id);
builder.append("&maxBitRate=").append(maxBitrate);
String url = rewriteUrlWithRedirect(context, builder.toString());
Log.i(TAG, "Using video URL: " + url);
return url;
}
@Override @Override
public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception { public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception {
@ -598,7 +736,6 @@ public class RESTMusicService implements MusicService {
List<String> parameterNames = Arrays.asList("action", "gain"); List<String> parameterNames = Arrays.asList("action", "gain");
List<Object> parameterValues = Arrays.<Object>asList("setGain", gain); List<Object> parameterValues = Arrays.<Object>asList("setGain", gain);
return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues);
} }
private JukeboxStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception { private JukeboxStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception {
@ -631,116 +768,124 @@ public class RESTMusicService implements MusicService {
return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener); return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
} }
private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener) throws Exception {
List<Object> parameterValues, ProgressListener progressListener) throws Exception { HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener);
HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener); if (entity == null) {
if (entity == null) { throw new RuntimeException("No entity received for URL " + url);
throw new RuntimeException("No entity received for URL " + url); }
}
InputStream in = entity.getContent(); InputStream in = entity.getContent();
return new InputStreamReader(in, Constants.UTF_8); return new InputStreamReader(in, Constants.UTF_8);
} }
private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener) throws Exception {
List<Object> parameterValues, ProgressListener progressListener) throws Exception { return getResponseForURL(context, url, requestParams, parameterNames,
return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, null).getEntity(); parameterValues, null, progressListener, null).getEntity();
} }
private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams, private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, List<Header> headers, ProgressListener progressListener, CancellableTask task) throws Exception {
List<String> parameterNames, List<Object> parameterValues, Log.d(TAG, "Connections in pool: " + connManager.getConnectionsInPool());
List<Header> headers, ProgressListener progressListener, CancellableTask task) throws Exception {
Log.d(TAG, "Connections in pool: " + connManager.getConnectionsInPool());
// If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being // If not too many parameters, extract them to the URL rather than
// received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus // relying on the HTTP POST request being
// loosing its entity. // received intact. Remember, HTTP POST requests are converted to GET
if (parameterNames != null && parameterNames.size() < 10) { // requests during HTTP redirects, thus
StringBuilder builder = new StringBuilder(url); // loosing its entity.
for (int i = 0; i < parameterNames.size(); i++) { if (parameterNames != null && parameterNames.size() < 10) {
builder.append("&").append(parameterNames.get(i)).append("="); StringBuilder builder = new StringBuilder(url);
builder.append(URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8")); for (int i = 0; i < parameterNames.size(); i++) {
} builder.append("&").append(parameterNames.get(i)).append("=");
url = builder.toString(); builder.append(URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"));
parameterNames = null; }
parameterValues = null; url = builder.toString();
} parameterNames = null;
parameterValues = null;
}
String rewrittenUrl = rewriteUrlWithRedirect(context, url); String rewrittenUrl = rewriteUrlWithRedirect(context, url);
return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task); return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task);
} }
private HttpResponse executeWithRetry(Context context, String url, String originalUrl, HttpParams requestParams, private HttpResponse executeWithRetry(Context context, String url, String originalUrl, HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues, List<Header> headers, ProgressListener progressListener, CancellableTask task) throws IOException {
List<String> parameterNames, List<Object> parameterValues, Log.i(TAG, "Using URL " + url);
List<Header> headers, ProgressListener progressListener, CancellableTask task) throws IOException {
Log.i(TAG, "Using URL " + url);
final AtomicReference<Boolean> cancelled = new AtomicReference<Boolean>(false); SharedPreferences prefs = Util.getPreferences(context);
int attempts = 0; int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000"));
while (true) { HttpParams newParams = httpClient.getParams();
attempts++; HttpConnectionParams.setSoTimeout(newParams, networkTimeout);
HttpContext httpContext = new BasicHttpContext(); httpClient.setParams(newParams);
final HttpPost request = new HttpPost(url);
if (task != null) { final AtomicReference<Boolean> cancelled = new AtomicReference<Boolean>(false);
// Attempt to abort the HTTP request if the task is cancelled. int attempts = 0;
task.setOnCancelListener(new CancellableTask.OnCancelListener() { while (true) {
@Override attempts++;
public void onCancel() { HttpContext httpContext = new BasicHttpContext();
cancelled.set(true); final HttpPost request = new HttpPost(url);
request.abort();
}
});
}
if (parameterNames != null) { if (task != null) {
List<NameValuePair> params = new ArrayList<NameValuePair>(); // Attempt to abort the HTTP request if the task is cancelled.
for (int i = 0; i < parameterNames.size(); i++) { task.setOnCancelListener(new CancellableTask.OnCancelListener() {
params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i)))); @Override
} public void onCancel() {
request.setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8)); new Thread(new Runnable() {
} public void run() {
try {
cancelled.set(true);
request.abort();
} catch(Exception e) {
Log.e(TAG, "Failed to stop http task");
}
}
}).start();
}
});
}
if (requestParams != null) { if (parameterNames != null) {
request.setParams(requestParams); List<NameValuePair> params = new ArrayList<NameValuePair>();
Log.d(TAG, "Socket read timeout: " + HttpConnectionParams.getSoTimeout(requestParams) + " ms."); for (int i = 0; i < parameterNames.size(); i++) {
} params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i))));
}
request.setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8));
}
if (headers != null) { if (requestParams != null) {
for (Header header : headers) { request.setParams(requestParams);
request.addHeader(header); Log.d(TAG, "Socket read timeout: " + HttpConnectionParams.getSoTimeout(requestParams) + " ms.");
} }
}
// Set credentials to get through apache proxies that require authentication. if (headers != null) {
SharedPreferences prefs = Util.getPreferences(context); for (Header header : headers) {
int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); request.addHeader(header);
String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); }
String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); }
httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
new UsernamePasswordCredentials(username, password));
try { // Set credentials to get through apache proxies that require authentication.
httpClient.getParams().setParameter("http.socket.timeout", Util.getNetworkTimeout(context)); int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
HttpResponse response = httpClient.execute(request, httpContext); String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
detectRedirect(originalUrl, context, httpContext); httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
return response; new UsernamePasswordCredentials(username, password));
} catch (IOException x) {
request.abort(); try {
if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || cancelled.get()) { HttpResponse response = httpClient.execute(request, httpContext);
throw x; detectRedirect(originalUrl, context, httpContext);
} return response;
if (progressListener != null) { } catch (IOException x) {
String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1); request.abort();
progressListener.updateProgress(msg); if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || cancelled.get()) {
} throw x;
Log.w(TAG, "Got IOException (" + attempts + "), will retry", x); }
increaseTimeouts(requestParams); if (progressListener != null) {
Util.sleepQuietly(2000L); String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1);
} progressListener.updateProgress(msg);
} }
} Log.w(TAG, "Got IOException (" + attempts + "), will retry", x);
increaseTimeouts(requestParams);
Util.sleepQuietly(2000L);
}
}
}
private void increaseTimeouts(HttpParams requestParams) { private void increaseTimeouts(HttpParams requestParams) {
if (requestParams != null) { if (requestParams != null) {
@ -758,7 +903,7 @@ public class RESTMusicService implements MusicService {
private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) { private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) {
HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST); HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST);
HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST); HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
// Sometimes the request doesn't contain the "http://host" part // Sometimes the request doesn't contain the "http://host" part
String redirectedUrl; String redirectedUrl;
if (request.getURI().getScheme() == null) { if (request.getURI().getScheme() == null) {
@ -766,7 +911,7 @@ public class RESTMusicService implements MusicService {
} else { } else {
redirectedUrl = request.getURI().toString(); redirectedUrl = request.getURI().toString();
} }
redirectFrom = originalUrl.substring(0, originalUrl.indexOf("/rest/")); redirectFrom = originalUrl.substring(0, originalUrl.indexOf("/rest/"));
redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/")); redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/"));

View File

@ -47,7 +47,7 @@ public class AlbumListParser extends MusicDirectoryEntryParser {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
String name = getElementName(); String name = getElementName();
if ("album".equals(name)) { if ("album".equals(name)) {
dir.addChild(parseEntry()); dir.addChild(parseEntry(""));
} else if ("error".equals(name)) { } else if ("error".equals(name)) {
handleError(); handleError();
} }

View File

@ -30,7 +30,7 @@ public class MusicDirectoryEntryParser extends AbstractParser {
super(context); super(context);
} }
protected MusicDirectory.Entry parseEntry() { protected MusicDirectory.Entry parseEntry(String artist) {
MusicDirectory.Entry entry = new MusicDirectory.Entry(); MusicDirectory.Entry entry = new MusicDirectory.Entry();
entry.setId(get("id")); entry.setId(get("id"));
entry.setParent(get("parent")); entry.setParent(get("parent"));
@ -55,7 +55,9 @@ public class MusicDirectoryEntryParser extends AbstractParser {
entry.setPath(get("path")); entry.setPath(get("path"));
entry.setVideo(getBoolean("isVideo")); entry.setVideo(getBoolean("isVideo"));
entry.setDiscNumber(getInteger("discNumber")); entry.setDiscNumber(getInteger("discNumber"));
} } else if(!"".equals(artist)) {
entry.setPath(artist + "/" + entry.getTitle());
}
return entry; return entry;
} }

View File

@ -24,7 +24,6 @@ import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
import com.thejoshwa.ultrasonic.androidapp.util.ProgressListener; import com.thejoshwa.ultrasonic.androidapp.util.ProgressListener;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import java.io.Reader; import java.io.Reader;
/** /**
@ -38,7 +37,7 @@ public class MusicDirectoryParser extends MusicDirectoryEntryParser {
super(context); super(context);
} }
public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { public MusicDirectory parse(String artist, Reader reader, ProgressListener progressListener) throws Exception {
long t0 = System.currentTimeMillis(); long t0 = System.currentTimeMillis();
updateProgress(progressListener, R.string.parser_reading); updateProgress(progressListener, R.string.parser_reading);
@ -51,7 +50,7 @@ public class MusicDirectoryParser extends MusicDirectoryEntryParser {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
String name = getElementName(); String name = getElementName();
if ("child".equals(name)) { if ("child".equals(name)) {
dir.addChild(parseEntry()); dir.addChild(parseEntry(artist));
} else if ("directory".equals(name)) { } else if ("directory".equals(name)) {
dir.setName(get("name")); dir.setName(get("name"));
} else if ("error".equals(name)) { } else if ("error".equals(name)) {

View File

@ -46,7 +46,7 @@ public class PlaylistParser extends MusicDirectoryEntryParser {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
String name = getElementName(); String name = getElementName();
if ("entry".equals(name)) { if ("entry".equals(name)) {
dir.addChild(parseEntry()); dir.addChild(parseEntry(""));
} else if ("error".equals(name)) { } else if ("error".equals(name)) {
handleError(); handleError();
} }

View File

@ -22,6 +22,8 @@ import android.content.Context;
import com.thejoshwa.ultrasonic.androidapp.R; import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.Playlist; import com.thejoshwa.ultrasonic.androidapp.domain.Playlist;
import com.thejoshwa.ultrasonic.androidapp.util.ProgressListener; import com.thejoshwa.ultrasonic.androidapp.util.ProgressListener;
import com.thejoshwa.ultrasonic.androidapp.view.PlaylistAdapter;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import java.io.Reader; import java.io.Reader;
@ -51,7 +53,12 @@ public class PlaylistsParser extends AbstractParser {
if ("playlist".equals(tag)) { if ("playlist".equals(tag)) {
String id = get("id"); String id = get("id");
String name = get("name"); String name = get("name");
result.add(new Playlist(id, name)); String owner = get("owner");
String comment = get("comment");
String songCount = get("songCount");
String created = get("created");
String pub = get("public");
result.add(new Playlist(id, name, owner, comment, songCount, created, pub));
} else if ("error".equals(tag)) { } else if ("error".equals(tag)) {
handleError(); handleError();
} }
@ -61,7 +68,7 @@ public class PlaylistsParser extends AbstractParser {
validate(); validate();
updateProgress(progressListener, R.string.parser_reading_done); updateProgress(progressListener, R.string.parser_reading_done);
return result; return PlaylistAdapter.PlaylistComparator.sort(result);
} }
} }

View File

@ -46,7 +46,7 @@ public class RandomSongsParser extends MusicDirectoryEntryParser {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
String name = getElementName(); String name = getElementName();
if ("song".equals(name)) { if ("song".equals(name)) {
dir.addChild(parseEntry()); dir.addChild(parseEntry(""));
} else if ("error".equals(name)) { } else if ("error".equals(name)) {
handleError(); handleError();
} }

View File

@ -57,9 +57,9 @@ public class SearchResult2Parser extends MusicDirectoryEntryParser {
artist.setName(get("name")); artist.setName(get("name"));
artists.add(artist); artists.add(artist);
} else if ("album".equals(name)) { } else if ("album".equals(name)) {
albums.add(parseEntry()); albums.add(parseEntry(""));
} else if ("song".equals(name)) { } else if ("song".equals(name)) {
songs.add(parseEntry()); songs.add(parseEntry(""));
} else if ("error".equals(name)) { } else if ("error".equals(name)) {
handleError(); handleError();
} }

View File

@ -51,7 +51,7 @@ public class SearchResultParser extends MusicDirectoryEntryParser {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
String name = getElementName(); String name = getElementName();
if ("match".equals(name)) { if ("match".equals(name)) {
songs.add(parseEntry()); songs.add(parseEntry(""));
} else if ("error".equals(name)) { } else if ("error".equals(name)) {
handleError(); handleError();
} }

View File

@ -1,19 +1,20 @@
package com.thejoshwa.ultrasonic.androidapp.util; package com.thejoshwa.ultrasonic.androidapp.util;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.SortedSet;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import android.os.AsyncTask;
import android.os.StatFs; import android.os.StatFs;
import com.thejoshwa.ultrasonic.androidapp.domain.Playlist;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile; import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadService; import com.thejoshwa.ultrasonic.androidapp.service.DownloadService;
@ -24,7 +25,7 @@ import com.thejoshwa.ultrasonic.androidapp.service.DownloadService;
public class CacheCleaner { public class CacheCleaner {
private static final String TAG = CacheCleaner.class.getSimpleName(); private static final String TAG = CacheCleaner.class.getSimpleName();
private static final double MAX_FILE_SYSTEM_USAGE = 0.95; private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
private final Context context; private final Context context;
private final DownloadService downloadService; private final DownloadService downloadService;
@ -35,35 +36,17 @@ public class CacheCleaner {
} }
public void clean() { public void clean() {
new Thread(new Runnable() { new BackgroundCleanup().execute();
public void run() {
Log.i(TAG, "Starting cache cleaning.");
if (downloadService == null) {
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
return;
}
try {
List<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
sortByAscendingModificationTime(files);
Set<File> 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);
}
}
}).start();
} }
public void cleanSpace() {
new BackgroundSpaceCleanup().execute();
}
public void cleanPlaylists(List<Playlist> playlists) {
new BackgroundPlaylistsCleanup().execute(playlists);
}
private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) { private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
for (File dir : dirs) { for (File dir : dirs) {
if (undeletable.contains(dir)) { if (undeletable.contains(dir)) {
@ -71,59 +54,62 @@ public class CacheCleaner {
} }
File[] children = dir.listFiles(); File[] children = dir.listFiles();
// No songs left in the folder
if(children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) {
Util.delete(FileUtil.getAlbumArtFile(dir));
children = dir.listFiles();
}
// Delete empty directory and associated album artwork. // Delete empty directory
if (children != null && children.length == 0) { if (children.length == 0) {
Util.delete(dir); Util.delete(dir);
Util.delete(FileUtil.getAlbumArtFile(dir));
} }
} }
} }
private long getMinimumDelete(List<File> files) {
if(files.size() == 0) {
return 0L;
}
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 = bytesTotalFs - MIN_FREE_SPACE;
private void deleteFiles(List<File> files, Set<File> undeletable) { 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));
return bytesToDelete;
}
private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) {
if (files.isEmpty()) { if (files.isEmpty()) {
return; return;
} }
long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
long bytesUsedByUltraSonic = 0L;
for (File file : files) {
bytesUsedByUltraSonic += file.length();
}
long bytesToDelete = 0;
// Ensure that file system is not more than 95% full.
try
{
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(bytesUsedByUltraSonic - cacheSizeBytes, 0L);
long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
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(bytesUsedByUltraSonic));
Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
} catch (Exception x) {
//
}
long bytesDeleted = 0L; long bytesDeleted = 0L;
for (File file : files) { for (File file : files) {
if (file.getName().equals(Constants.ALBUM_ART_FILE)) { if(!deletePartials && bytesDeleted > bytesToDelete) break;
// Move artwork to new folder.
file.renameTo(FileUtil.getAlbumArtFile(file.getParentFile()));
} else if (bytesToDelete > bytesDeleted || file.getName().endsWith(".partial") || file.getName().contains(".partial.")) { if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) {
if (!undeletable.contains(file)) { if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) {
long size = file.length(); long size = file.length();
if (Util.delete(file)) { if (Util.delete(file)) {
bytesDeleted += size; bytesDeleted += size;
@ -133,15 +119,13 @@ public class CacheCleaner {
} }
Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted)); Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
Log.i(TAG, "Cache size after : " + Util.formatBytes(bytesUsedByUltraSonic - bytesDeleted));
} }
private void findCandidatesForDeletion(File file, List<File> files, List<File> dirs) { private void findCandidatesForDeletion(File file, List<File> files, List<File> dirs) {
if (file.isFile()) { if (file.isFile()) {
String name = file.getName(); String name = file.getName();
boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete."); boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
boolean isAlbumArtFile = name.equals(Constants.ALBUM_ART_FILE); if (isCacheFile) {
if (isCacheFile || isAlbumArtFile) {
files.add(file); files.add(file);
} }
} else { } else {
@ -179,4 +163,79 @@ public class CacheCleaner {
undeletable.add(FileUtil.getMusicDirectory(context)); undeletable.add(FileUtil.getMusicDirectory(context));
return undeletable; return undeletable;
} }
}
private class BackgroundCleanup extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if (downloadService == null) {
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
return null;
}
try {
List<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
sortByAscendingModificationTime(files);
Set<File> undeletable = findUndeletableFiles();
deleteFiles(files, undeletable, getMinimumDelete(files), true);
deleteEmptyDirs(dirs, undeletable);
} catch (RuntimeException x) {
Log.e(TAG, "Error in cache cleaning.", x);
}
return null;
}
}
private class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if (downloadService == null) {
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
return null;
}
try {
List<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
long bytesToDelete = getMinimumDelete(files);
if(bytesToDelete > 0L) {
sortByAscendingModificationTime(files);
Set<File> undeletable = findUndeletableFiles();
deleteFiles(files, undeletable, bytesToDelete, false);
}
} catch (RuntimeException x) {
Log.e(TAG, "Error in cache cleaning.", x);
}
return null;
}
}
private class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void> {
@Override
protected Void doInBackground(List<Playlist>... params) {
try {
SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory());
List<Playlist> playlists = params[0];
for (Playlist playlist : playlists) {
playlistFiles.remove(FileUtil.getPlaylistFile(playlist.getName()));
}
for(File playlist : playlistFiles) {
playlist.delete();
}
} catch (RuntimeException x) {
Log.e(TAG, "Error in playlist cache cleaning.", x);
}
return null;
}
}
}

View File

@ -37,7 +37,7 @@ public abstract class CancellableTask {
private final AtomicReference<OnCancelListener> cancelListener = new AtomicReference<OnCancelListener>(); private final AtomicReference<OnCancelListener> cancelListener = new AtomicReference<OnCancelListener>();
public void cancel() { public void cancel() {
Log.d(TAG, "Cancelling " + CancellableTask.this); Log.i(TAG, "Cancelling " + CancellableTask.this);
cancelled.set(true); cancelled.set(true);
OnCancelListener listener = cancelListener.get(); OnCancelListener listener = cancelListener.get();
@ -69,12 +69,12 @@ public abstract class CancellableTask {
@Override @Override
public void run() { public void run() {
running.set(true); running.set(true);
Log.d(TAG, "Starting thread for " + CancellableTask.this); Log.i(TAG, "Starting thread for " + CancellableTask.this);
try { try {
execute(); execute();
} finally { } finally {
running.set(false); running.set(false);
Log.d(TAG, "Stopping thread for " + CancellableTask.this); Log.i(TAG, "Stopping thread for " + CancellableTask.this);
} }
} }
}); });
@ -84,4 +84,4 @@ public abstract class CancellableTask {
public static interface OnCancelListener { public static interface OnCancelListener {
void onCancel(); void onCancel();
} }
} }

View File

@ -78,6 +78,8 @@ public final class Constants {
public static final String PREFERENCES_KEY_SHOW_TRACK_NUMBER = "showTrackNumber"; public static final String PREFERENCES_KEY_SHOW_TRACK_NUMBER = "showTrackNumber";
public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi"; 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_MAX_BITRATE_MOBILE = "maxBitrateMobile";
public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI = "maxVideoBitrateWifi";
public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE = "maxVideoBitrateMobile";
public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize"; public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation"; public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
public static final String PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount"; public static final String PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount";
@ -88,6 +90,9 @@ public final class Constants {
public static final String PREFERENCES_KEY_SERVER_SCALING = "serverScaling"; public static final String PREFERENCES_KEY_SERVER_SCALING = "serverScaling";
public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode"; public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload"; public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
public static final String PREFERENCES_KEY_SHUFFLE_START_YEAR = "startYear";
public static final String PREFERENCES_KEY_SHUFFLE_END_YEAR = "endYear";
public static final String PREFERENCES_KEY_SHUFFLE_GENRE = "genre";
public static final String PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength"; public static final String PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength";
public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout"; public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout";
public static final String PREFERENCES_KEY_SHOW_NOTIFICATION = "showNotification"; public static final String PREFERENCES_KEY_SHOW_NOTIFICATION = "showNotification";
@ -101,8 +106,12 @@ public final class Constants {
public static final String PREFERENCES_KEY_DEFAULT_ARTISTS = "defaultArtists"; public static final String PREFERENCES_KEY_DEFAULT_ARTISTS = "defaultArtists";
public static final String PREFERENCES_KEY_USE_STREAM_PROXY = "useStreamProxy"; public static final String PREFERENCES_KEY_USE_STREAM_PROXY = "useStreamProxy";
public static final String PREFERENCES_KEY_SHOW_NOW_PLAYING = "showNowPlaying"; public static final String PREFERENCES_KEY_SHOW_NOW_PLAYING = "showNowPlaying";
public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback";
public static final String PREFERENCES_KEY_CLEAR_SEARCH_HISTORY = "clearSearchHistory"; public static final String PREFERENCES_KEY_CLEAR_SEARCH_HISTORY = "clearSearchHistory";
public static final String PREFERENCES_KEY_TEST_CONNECTION = "testConnection"; public static final String PREFERENCES_KEY_TEST_CONNECTION = "testConnection";
public static final String PREFERENCES_EQUALIZER_ON = "equalizerOn";
public static final String PREFERENCES_EQUALIZER_SETTINGS = "equalizerSettings";
public static final String PREFERENCES_KEY_DOWNLOAD_TRANSITION = "transitionToDownloadOnPlay";
// Name of the preferences file. // Name of the preferences file.
public static final String PREFERENCES_FILE_NAME = "com.thejoshwa.ultrasonic.androidapp_preferences"; public static final String PREFERENCES_FILE_NAME = "com.thejoshwa.ultrasonic.androidapp_preferences";

View File

@ -36,6 +36,8 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Environment; import android.os.Environment;
import android.util.Log; import android.util.Log;
import com.thejoshwa.ultrasonic.androidapp.domain.Artist;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
/** /**
@ -44,11 +46,32 @@ import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
public class FileUtil { public class FileUtil {
private static final String TAG = FileUtil.class.getSimpleName(); private static final String TAG = FileUtil.class.getSimpleName();
private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">"}; private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">"}; private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma"); private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
private static final List<String> VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv");
private static final List<String> PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u");
private static final File DEFAULT_MUSIC_DIR = createDirectory("music"); private static final File DEFAULT_MUSIC_DIR = createDirectory("music");
public static File getAnySong(Context context) {
File dir = getMusicDirectory(context);
return getAnySong(context, dir);
}
private static File getAnySong(Context context, File dir) {
for(File file: dir.listFiles()) {
if(file.isDirectory()) {
return getAnySong(context, file);
}
String extension = getExtension(file.getName());
if(MUSIC_FILE_EXTENSIONS.contains(extension)) {
return file;
}
}
return null;
}
public static File getSongFile(Context context, MusicDirectory.Entry song) { public static File getSongFile(Context context, MusicDirectory.Entry song) {
File dir = getAlbumDirectory(context, song); File dir = getAlbumDirectory(context, song);
@ -72,14 +95,30 @@ public class FileUtil {
return new File(dir, fileName.toString()); return new File(dir, fileName.toString());
} }
public static File getPlaylistFile(String name) {
File playlistDir = getPlaylistDirectory();
return new File(playlistDir, fileSystemSafe(name) + ".m3u");
}
public static File getOldPlaylistFile(String name) {
File playlistDir = getPlaylistDirectory();
return new File(playlistDir, name);
}
public static File getPlaylistDirectory() {
File playlistDir = new File(getUltraSonicDirectory(), "playlists");
ensureDirectoryExistsAndIsReadWritable(playlistDir);
return playlistDir;
}
public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) { public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
File albumDir = getAlbumDirectory(context, entry); File albumDir = getAlbumDirectory(context, entry);
return getAlbumArtFile(albumDir); return getAlbumArtFile(albumDir);
} }
public static File getAlbumArtFile(File albumDir) { public static File getAlbumArtFile(File albumDir) {
File albumArtDir = getAlbumArtDirectory(); File albumArtDir = getAlbumArtDirectory();
return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg"); return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg");
} }
public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) { public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
@ -102,6 +141,11 @@ public class FileUtil {
return null; return null;
} }
public static File getArtistDirectory(Context context, Artist artist) {
File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getName()));
return dir;
}
public static File getAlbumArtDirectory() { public static File getAlbumArtDirectory() {
File albumArtDir = new File(getUltraSonicDirectory(), "artwork"); File albumArtDir = new File(getUltraSonicDirectory(), "artwork");
@ -110,17 +154,17 @@ public class FileUtil {
return albumArtDir; return albumArtDir;
} }
private static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) { public static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
File dir; File dir;
if (entry.getPath() != null) { if (entry.getPath() != null) {
File f = new File(fileSystemSafeDir(entry.getPath())); File f = new File(fileSystemSafeDir(entry.getPath()));
dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent())); dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
} else { } else {
String artist = fileSystemSafe(entry.getArtist()); String artist = fileSystemSafe(entry.getArtist());
String album = fileSystemSafe(entry.getAlbum()); String album = fileSystemSafe(entry.getAlbum());
if (album == "unnamed") { if("unnamed".equals(album)) {
album = fileSystemSafe(entry.getTitle()); album = fileSystemSafe(entry.getTitle());
} }
dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album); dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
} }
return dir; return dir;
@ -238,23 +282,38 @@ public class FileUtil {
return new TreeSet<File>(Arrays.asList(files)); return new TreeSet<File>(Arrays.asList(files));
} }
public static SortedSet<File> listMusicFiles(File dir) { public static SortedSet<File> listMediaFiles(File dir) {
SortedSet<File> files = listFiles(dir); SortedSet<File> files = listFiles(dir);
Iterator<File> iterator = files.iterator(); Iterator<File> iterator = files.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
File file = iterator.next(); File file = iterator.next();
if (!file.isDirectory() && !isMusicFile(file)) { if (!file.isDirectory() && !isMediaFile(file)) {
iterator.remove(); iterator.remove();
} }
} }
return files; return files;
} }
private static boolean isMusicFile(File file) { private static boolean isMediaFile(File file) {
String extension = getExtension(file.getName()); String extension = getExtension(file.getName());
return MUSIC_FILE_EXTENSIONS.contains(extension); return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension);
} }
public static boolean isMusicFile(File file) {
String extension = getExtension(file.getName());
return MUSIC_FILE_EXTENSIONS.contains(extension);
}
public static boolean isVideoFile(File file) {
String extension = getExtension(file.getName());
return VIDEO_FILE_EXTENSIONS.contains(extension);
}
public static boolean isPlaylistFile(File file) {
String extension = getExtension(file.getName());
return PLAYLIST_FILE_EXTENSIONS.contains(extension);
}
/** /**
* Returns the extension (the substring after the last dot) of the given file. The dot * Returns the extension (the substring after the last dot) of the given file. The dot
* is not included in the returned extension. * is not included in the returned extension.
@ -264,7 +323,7 @@ public class FileUtil {
*/ */
public static String getExtension(String name) { public static String getExtension(String name) {
int index = name.lastIndexOf('.'); int index = name.lastIndexOf('.');
return index == -1 ? "" : name.substring(index + 1).toLowerCase(Locale.getDefault()); return index == -1 ? "" : name.substring(index + 1).toLowerCase();
} }
/** /**
@ -278,6 +337,19 @@ public class FileUtil {
int index = name.lastIndexOf('.'); int index = name.lastIndexOf('.');
return index == -1 ? name : name.substring(0, index); return index == -1 ? name : name.substring(0, index);
} }
public static long getUsedSize(Context context, File file) {
long size = 0L;
if(file.isFile()) {
return file.length();
} else {
for (File child : FileUtil.listFiles(file)) {
size += getUsedSize(context, child);
}
return size;
}
}
public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) { public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
File file = new File(context.getCacheDir(), fileName); File file = new File(context.getCacheDir(), fileName);
@ -304,7 +376,7 @@ public class FileUtil {
ObjectInputStream in = null; ObjectInputStream in = null;
try { try {
in = new ObjectInputStream(new FileInputStream(file)); in = new ObjectInputStream(new FileInputStream(file));
T result = (T)in.readObject(); T result = (T) in.readObject();
Log.i(TAG, "Deserialized object from " + file); Log.i(TAG, "Deserialized object from " + file);
return result; return result;
} catch (Throwable x) { } catch (Throwable x) {

View File

@ -22,6 +22,8 @@ import java.lang.ref.SoftReference;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import android.graphics.drawable.Drawable;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
*/ */
@ -99,4 +101,15 @@ public class LRUCache<K,V>{
} }
} }
protected int sizeOf(String key, Drawable drawable) {
// TODO Auto-generated method stub
return 0;
}
protected void entryRemoved(boolean evicted, String key,
Drawable oldDrawable, Drawable newDrawable) {
// TODO Auto-generated method stub
}
} }

View File

@ -0,0 +1,86 @@
package com.thejoshwa.ultrasonic.androidapp.util;
import com.thejoshwa.ultrasonic.androidapp.activity.SubsonicTabActivity;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Build;
/**
* @author Sindre Mehus
* @version $Id$
*/
public abstract class LoadingTask<T> extends BackgroundTask<T> {
private final SubsonicTabActivity tabActivity;
private final boolean cancellable;
private boolean cancelled = false;
public LoadingTask(SubsonicTabActivity activity, final boolean cancellable) {
super(activity);
tabActivity = activity;
this.cancellable = cancellable;
}
@Override
public void execute() {
final ProgressDialog loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
cancelled = true;
}
});
new Thread() {
@Override
public void run() {
try {
final T result = doInBackground();
if (isCancelled()) {
return;
}
getHandler().post(new Runnable() {
@Override
public void run() {
loading.cancel();
done(result);
}
});
} catch (final Throwable t) {
if (isCancelled()) {
return;
}
getHandler().post(new Runnable() {
@Override
public void run() {
loading.cancel();
error(t);
}
});
}
}
}.start();
}
@SuppressLint("NewApi")
private boolean isCancelled() {
if (Build.VERSION.SDK_INT >= 17) {
return tabActivity.isDestroyed() || cancelled;
} else {
return cancelled;
}
}
@Override
public void updateProgress(final String message) {
getHandler().post(new Runnable() {
@Override
public void run() {
}
});
}
}

View File

@ -44,9 +44,9 @@ public class ShufflePlayBuffer {
private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>(); private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
private Context context; private Context context;
private int currentServer; private int currentServer;
public ShufflePlayBuffer(Context context) { public ShufflePlayBuffer(Context context) {
this.context = context; this.context = context;
executorService = Executors.newSingleThreadScheduledExecutor(); executorService = Executors.newSingleThreadScheduledExecutor();
Runnable runnable = new Runnable() { Runnable runnable = new Runnable() {
@Override @Override
@ -99,11 +99,10 @@ public class ShufflePlayBuffer {
private void clearBufferIfnecessary() { private void clearBufferIfnecessary() {
synchronized (buffer) { synchronized (buffer) {
if (currentServer != Util.getActiveServer(context)) { if (currentServer != Util.getActiveServer(context)) {
currentServer = Util.getActiveServer(context); currentServer = Util.getActiveServer(context);
buffer.clear(); buffer.clear();
} }
} }
} }
}
}

View File

@ -18,12 +18,13 @@ import java.net.URLDecoder;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import org.apache.http.Header;
import org.apache.http.HttpRequest; import org.apache.http.HttpRequest;
import org.apache.http.message.BasicHttpRequest; import org.apache.http.message.BasicHttpRequest;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile; import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadService; import com.thejoshwa.ultrasonic.androidapp.service.DownloadService;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
@ -35,25 +36,21 @@ public class StreamProxy implements Runnable {
private boolean isRunning; private boolean isRunning;
private ServerSocket socket; private ServerSocket socket;
private int port; private int port;
private DownloadFile downloadFile; private DownloadService downloadService;
public StreamProxy() { public StreamProxy(DownloadService downloadService) {
// Create listening socket // Create listening socket
try { try {
socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 })); socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
socket.setSoTimeout(5000); socket.setSoTimeout(5000);
port = socket.getLocalPort(); port = socket.getLocalPort();
this.downloadService = downloadService;
} catch (UnknownHostException e) { // impossible } catch (UnknownHostException e) { // impossible
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "IOException initializing server", e); Log.e(TAG, "IOException initializing server", e);
} }
} }
public void setDownloadFile(DownloadFile downloadFile) {
this.downloadFile = downloadFile;
}
public int getPort() { public int getPort() {
return port; return port;
@ -67,52 +64,43 @@ public class StreamProxy implements Runnable {
public void stop() { public void stop() {
isRunning = false; isRunning = false;
thread.interrupt(); thread.interrupt();
try {
thread.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
@Override @Override
public void run() { public void run() {
Looper.prepare();
isRunning = true; isRunning = true;
while (isRunning) { while (isRunning) {
try { try {
Socket client = socket.accept(); Socket client = socket.accept();
if (client == null) { if (client == null) {
continue; continue;
} }
Log.i(TAG, "client connected");
Log.d(TAG, "client connected");
StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client); StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
if (task.processRequest()) { if (task.processRequest()) {
task.execute(); new Thread(task).start();
} }
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
// Do nothing // Do nothing
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error connecting to client", e); Log.e(TAG, "Error connecting to client", e);
} }
} }
Log.i(TAG, "Proxy interrupted. Shutting down.");
Log.d(TAG, "Proxy interrupted. Shutting down.");
} }
private class StreamToMediaPlayerTask extends AsyncTask<String, Void, Integer> { private class StreamToMediaPlayerTask implements Runnable {
File thisFile;
String localPath;
Socket client; Socket client;
int cbSkip; int cbSkip;
public StreamToMediaPlayerTask(Socket client) { public StreamToMediaPlayerTask(Socket client) {
this.client = client; this.client = client;
} }
private HttpRequest readRequest() { private HttpRequest readRequest() {
HttpRequest request = null; HttpRequest request = null;
InputStream is; InputStream is;
@ -134,122 +122,121 @@ public class StreamProxy implements Runnable {
StringTokenizer st = new StringTokenizer(firstLine); StringTokenizer st = new StringTokenizer(firstLine);
String method = st.nextToken(); String method = st.nextToken();
String uri = st.nextToken(); String uri = st.nextToken();
Log.d(TAG, uri);
String realUri = uri.substring(1); String realUri = uri.substring(1);
Log.d(TAG, realUri); Log.i(TAG, realUri);
request = new BasicHttpRequest(method, realUri); request = new BasicHttpRequest(method, realUri);
return request; return request;
} }
public boolean processRequest() { public boolean processRequest() {
HttpRequest request = readRequest(); HttpRequest request = readRequest();
if (request == null) { if (request == null) {
return false; return false;
} }
// Read HTTP headers
Log.i(TAG, "Processing request");
Log.d(TAG, "Processing request"); try {
localPath = URLDecoder.decode(request.getRequestLine().getUri(), Constants.UTF_8);
thisFile = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); } catch (UnsupportedEncodingException e) {
Log.e(TAG, "Unsupported encoding", e);
if (!thisFile.exists()) {
Log.e(TAG, "File " + thisFile.getPath() + " does not exist");
return false; return false;
} }
Log.i(TAG, "Processing request for file " + localPath);
File file = new File(localPath);
if (!file.exists()) {
Log.e(TAG, "File " + localPath + " does not exist");
return false;
}
return true; return true;
} }
@Override @Override
protected Integer doInBackground(String... params) { public void run() {
long fileSize = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile().length() : downloadFile.getSong().getSize(); Log.i(TAG, "Streaming song in background");
DownloadFile downloadFile = downloadService.getCurrentPlaying();
MusicDirectory.Entry song = downloadFile.getSong();
long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8;
Log.i(TAG, "Streaming fileSize: " + fileSize);
// Create HTTP header // Create HTTP header
String headers = "HTTP/1.1 200 OK\r\n"; String headers = "HTTP/1.0 200 OK\r\n";
headers += "Content-Type: " + "application/octet-stream" + "\r\n";
headers += "Connection: close\r\n";
headers += "\r\n";
if (fileSize > 0) { long cbToSend = fileSize - cbSkip;
headers += "Content-Length: " + fileSize + "\r\n"; OutputStream output = null;
} byte[] buff = new byte[64 * 1024];
try {
headers += "Content-Type: " + "application/octet-stream" + "\r\n"; output = new BufferedOutputStream(client.getOutputStream(), 32*1024);
headers += "Connection: close\r\n"; output.write(headers.getBytes());
headers += "\r\n";
long cbToSend = fileSize - cbSkip; if(!downloadFile.isWorkDone()) {
long totalBytesSent = 0; // Loop as long as there's stuff to send
OutputStream output = null; while (isRunning && !client.isClosed()) {
byte[] buff = new byte[64 * 1024];
try {
output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024);
output.write(headers.getBytes());
// Loop as long as there's stuff to send
while (isRunning && cbToSend > 0 && !client.isClosed()) {
// See if there's more to send
int cbSentThisBatch = 0;
FileInputStream input = new FileInputStream(thisFile); // See if there's more to send
input.skip(cbSkip); File file = new File(localPath);
int cbToSendThisBatch = input.available(); int cbSentThisBatch = 0;
if (file.exists()) {
FileInputStream input = new FileInputStream(file);
input.skip(cbSkip);
int cbToSendThisBatch = input.available();
while (cbToSendThisBatch > 0) {
int cbToRead = Math.min(cbToSendThisBatch, buff.length);
int cbRead = input.read(buff, 0, cbToRead);
if (cbRead == -1) {
break;
}
cbToSendThisBatch -= cbRead;
cbToSend -= cbRead;
output.write(buff, 0, cbRead);
output.flush();
cbSkip += cbRead;
cbSentThisBatch += cbRead;
}
input.close();
}
while (cbToSendThisBatch > 0) { // Done regardless of whether or not it thinks it is
int cbToRead = Math.min(cbToSendThisBatch, buff.length); if(downloadFile.isWorkDone() && cbSkip >= file.length()) {
int cbRead = input.read(buff, 0, cbToRead);
if (cbRead == -1) {
break; break;
} }
cbToSendThisBatch -= cbRead; // If we did nothing this batch, block for a second
cbToSend -= cbRead; if (cbSentThisBatch == 0) {
Log.d(TAG, "Blocking until more data appears (" + cbToSend + ")");
output.write(buff, 0, cbRead); Thread.sleep(1000);
output.flush();
cbSkip += cbRead;
cbSentThisBatch += cbRead;
totalBytesSent += cbRead;
}
input.close();
if (!downloadFile.isDownloading()) {
if (downloadFile.isCompleteFileAvailable()) {
if (downloadFile.getCompleteFile().length() == totalBytesSent) {
Log.d(TAG, "Track is no longer being downloaded, sent " + totalBytesSent + " / " + fileSize);
break;
}
} }
} }
} else {
// If we did nothing this batch, block for a second Log.w(TAG, "Requesting data for completely downloaded file");
if (cbSentThisBatch == 0) {
Log.d(TAG, "Blocking until more data appears");
Thread.sleep(500);
}
} }
} catch (SocketException socketException) { }
Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly"); catch (SocketException socketException) {
} catch (Exception e) { Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
Log.e(TAG, "Exception thrown from streaming task:"); }
Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage()); catch (Exception e) {
e.printStackTrace(); Log.e(TAG, "Exception thrown from streaming task:");
} Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
}
// Cleanup // Cleanup
try { try {
if (output != null) { if (output != null) {
output.close(); output.close();
} }
client.close(); client.close();
} catch (IOException e) { }
Log.e(TAG, "IOException while cleaning up streaming task:"); catch (IOException e) {
Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage()); Log.e(TAG, "IOException while cleaning up streaming task:");
e.printStackTrace(); Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
} }
}
return 1;
}
} }
} }

View File

@ -36,6 +36,8 @@ import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.os.Parcelable; import android.os.Parcelable;
@ -400,6 +402,14 @@ public class Util extends DownloadActivity {
delete(tmp); delete(tmp);
} }
} }
public static void renameFile(File from, File to) throws IOException {
if(from.renameTo(to)) {
Log.i(TAG, "Renamed " + from + " to " + to);
} else {
atomicCopy(from, to);
}
}
public static void close(Closeable closeable) { public static void close(Closeable closeable) {
try { try {
@ -421,6 +431,24 @@ public class Util extends DownloadActivity {
} }
return true; return true;
} }
public static boolean recursiveDelete(File dir) {
if (dir != null && dir.exists()) {
for(File file: dir.listFiles()) {
if(file.isDirectory()) {
if(!recursiveDelete(file)) {
return false;
}
} else if(file.exists()) {
if(!file.delete()) {
return false;
}
}
}
return dir.delete();
}
return false;
}
public static void toast(Context context, int messageId) { public static void toast(Context context, int messageId) {
toast(context, messageId, true); toast(context, messageId, true);
@ -796,6 +824,11 @@ public class Util extends DownloadActivity {
} }
} }
public static WifiManager.WifiLock createWifiLock(Context context, String tag) {
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
return wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, tag);
}
public static Bitmap scaleBitmap(Bitmap bitmap, int size) { public static Bitmap scaleBitmap(Bitmap bitmap, int size) {
// Try to keep correct aspect ratio of the original image, do not force a square // Try to keep correct aspect ratio of the original image, do not force a square
double aspectRatio = (double)bitmap.getHeight() / (double)bitmap.getWidth(); double aspectRatio = (double)bitmap.getHeight() / (double)bitmap.getWidth();
@ -1088,6 +1121,18 @@ public class Util extends DownloadActivity {
views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); views.setOnClickPendingIntent(R.id.control_stop, pendingIntent);
} }
public static int getMaxVideoBitrate(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_VIDEO_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE, "0"));
}
public static int getNetworkTimeout(Context context) { public static int getNetworkTimeout(Context context) {
SharedPreferences prefs = getPreferences(context); SharedPreferences prefs = getPreferences(context);
return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000")); return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000"));
@ -1137,4 +1182,14 @@ public class Util extends DownloadActivity {
SharedPreferences prefs = getPreferences(context); SharedPreferences prefs = getPreferences(context);
return prefs.getBoolean(Constants.PREFERENCES_KEY_SHOW_NOW_PLAYING, true); return prefs.getBoolean(Constants.PREFERENCES_KEY_SHOW_NOW_PLAYING, true);
} }
public static boolean getGaplessPlaybackPreference(Context context) {
SharedPreferences prefs = getPreferences(context);
return prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true);
}
public static boolean getShouldTransitionOnPlaybackPreference(Context context) {
SharedPreferences prefs = getPreferences(context);
return prefs.getBoolean(Constants.PREFERENCES_KEY_DOWNLOAD_TRANSITION, true);
}
} }

View File

@ -16,26 +16,27 @@
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package com.thejoshwa.ultrasonic.androidapp.util; package com.thejoshwa.ultrasonic.androidapp.view;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import com.thejoshwa.ultrasonic.androidapp.R; import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.util.ImageLoader;
import com.thejoshwa.ultrasonic.androidapp.util.Util;
/** /**
* Used to display albums in a {@code ListView}. * Used to display albums in a {@code ListView}.
* *
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class AlbumView extends LinearLayout { public class AlbumView extends UpdateView {
private static final String TAG = AlbumView.class.getSimpleName(); private static final String TAG = AlbumView.class.getSimpleName();
private TextView titleView; private TextView titleView;

View File

@ -16,7 +16,7 @@
Copyright 2010 (C) Sindre Mehus Copyright 2010 (C) Sindre Mehus
*/ */
package com.thejoshwa.ultrasonic.androidapp.util; package com.thejoshwa.ultrasonic.androidapp.view;
import com.thejoshwa.ultrasonic.androidapp.domain.Artist; import com.thejoshwa.ultrasonic.androidapp.domain.Artist;
import com.thejoshwa.ultrasonic.androidapp.R; import com.thejoshwa.ultrasonic.androidapp.R;

View File

@ -0,0 +1,86 @@
package com.thejoshwa.ultrasonic.androidapp.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
public class AutoRepeatButton extends ImageView {
private long initialRepeatDelay = 1000;
private long repeatIntervalInMilliseconds = 300;
private boolean doClick = true;
private Runnable repeatEvent = null;
private Runnable repeatClickWhileButtonHeldRunnable = new Runnable() {
@Override
public void run() {
doClick = false;
//Perform the present repetition of the click action provided by the user
// in setOnClickListener().
if(repeatEvent != null)
repeatEvent.run();
//Schedule the next repetitions of the click action, using a faster repeat
// interval than the initial repeat delay interval.
postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalInMilliseconds);
}
};
private void commonConstructorCode() {
this.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN)
{
doClick = true;
//Just to be sure that we removed all callbacks,
// which should have occurred in the ACTION_UP
removeCallbacks(repeatClickWhileButtonHeldRunnable);
//Schedule the start of repetitions after a one half second delay.
postDelayed(repeatClickWhileButtonHeldRunnable, initialRepeatDelay);
setPressed(true);
}
else if(action == MotionEvent.ACTION_UP) {
//Cancel any repetition in progress.
removeCallbacks(repeatClickWhileButtonHeldRunnable);
if(doClick || repeatEvent == null) {
performClick();
}
setPressed(false);
}
//Returning true here prevents performClick() from getting called
// in the usual manner, which would be redundant, given that we are
// already calling it above.
return true;
}
});
}
public void setOnRepeatListener(Runnable runnable) {
repeatEvent = runnable;
}
public AutoRepeatButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
commonConstructorCode();
}
public AutoRepeatButton(Context context, AttributeSet attrs) {
super(context, attrs);
commonConstructorCode();
}
public AutoRepeatButton(Context context) {
super(context);
commonConstructorCode();
}
}

View File

@ -16,7 +16,7 @@
Copyright 2010 (C) Sindre Mehus Copyright 2010 (C) Sindre Mehus
*/ */
package com.thejoshwa.ultrasonic.androidapp.util; package com.thejoshwa.ultrasonic.androidapp.view;
import java.util.List; import java.util.List;
@ -25,6 +25,7 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import com.thejoshwa.ultrasonic.androidapp.activity.SubsonicTabActivity; import com.thejoshwa.ultrasonic.androidapp.activity.SubsonicTabActivity;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
import com.thejoshwa.ultrasonic.androidapp.util.ImageLoader;
/** /**
* @author Sindre Mehus * @author Sindre Mehus

View File

@ -16,7 +16,7 @@
Copyright 2010 (C) Sindre Mehus Copyright 2010 (C) Sindre Mehus
*/ */
package com.thejoshwa.ultrasonic.androidapp.util; package com.thejoshwa.ultrasonic.androidapp.view;
import com.thejoshwa.ultrasonic.androidapp.domain.Genre; import com.thejoshwa.ultrasonic.androidapp.domain.Genre;
import com.thejoshwa.ultrasonic.androidapp.R; import com.thejoshwa.ultrasonic.androidapp.R;

View File

@ -0,0 +1,51 @@
package com.thejoshwa.ultrasonic.androidapp.view;
import java.util.List;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import java.util.Collections;
import java.util.Comparator;
import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.activity.SubsonicTabActivity;
import com.thejoshwa.ultrasonic.androidapp.domain.Playlist;
/**
* @author Sindre Mehus
*/
public class PlaylistAdapter extends ArrayAdapter<Playlist> {
private final SubsonicTabActivity activity;
public PlaylistAdapter(SubsonicTabActivity activity, List<Playlist> Playlists) {
super(activity, R.layout.playlist_list_item, Playlists);
this.activity = activity;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Playlist entry = getItem(position);
PlaylistView view;
if (convertView != null && convertView instanceof PlaylistView) {
view = (PlaylistView) convertView;
} else {
view = new PlaylistView(activity);
}
view.setPlaylist(entry);
return view;
}
public static class PlaylistComparator implements Comparator<Playlist> {
@Override
public int compare(Playlist playlist1, Playlist playlist2) {
return playlist1.getName().compareToIgnoreCase(playlist2.getName());
}
public static List<Playlist> sort(List<Playlist> playlists) {
Collections.sort(playlists, new PlaylistComparator());
return playlists;
}
}
}

View File

@ -0,0 +1,47 @@
/*
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 com.thejoshwa.ultrasonic.androidapp.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.TextView;
import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.Playlist;
/**
* Used to display albums in a {@code ListView}.
*
* @author Sindre Mehus
*/
public class PlaylistView extends UpdateView {
private static final String TAG = PlaylistView.class.getSimpleName();
private TextView titleView;
public PlaylistView(Context context) {
super(context);
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
titleView = (TextView) findViewById(R.id.playlist_name);
}
public void setPlaylist(Playlist playlist) {
titleView.setText(playlist.getName());
update();
}
}

View File

@ -16,18 +16,16 @@
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package com.thejoshwa.ultrasonic.androidapp.util; package com.thejoshwa.ultrasonic.androidapp.view;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Checkable; import android.widget.Checkable;
import android.widget.CheckedTextView; import android.widget.CheckedTextView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import com.thejoshwa.ultrasonic.androidapp.R; import com.thejoshwa.ultrasonic.androidapp.R;
import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory; import com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
@ -36,32 +34,30 @@ import com.thejoshwa.ultrasonic.androidapp.service.DownloadServiceImpl;
import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile; import com.thejoshwa.ultrasonic.androidapp.service.DownloadFile;
import com.thejoshwa.ultrasonic.androidapp.service.MusicService; import com.thejoshwa.ultrasonic.androidapp.service.MusicService;
import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory; import com.thejoshwa.ultrasonic.androidapp.service.MusicServiceFactory;
import com.thejoshwa.ultrasonic.androidapp.util.Util;
import java.io.File; import java.io.File;
import java.util.WeakHashMap;
/** /**
* Used to display songs in a {@code ListView}. * Used to display songs in a {@code ListView}.
* *
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class SongView extends LinearLayout implements Checkable { public class SongView extends UpdateView implements Checkable {
private static final String TAG = SongView.class.getSimpleName(); private static final String TAG = SongView.class.getSimpleName();
private static final WeakHashMap<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>();
private static Handler handler;
private CheckedTextView checkedTextView; private CheckedTextView checkedTextView;
private ImageView starImageView; private ImageView starImageView;
private TextView trackTextView; private TextView trackTextView;
private TextView discTextView;
private TextView titleTextView; private TextView titleTextView;
private TextView artistTextView; private TextView artistTextView;
private TextView durationTextView; private TextView durationTextView;
private TextView statusTextView; private TextView statusTextView;
private MusicDirectory.Entry song; private MusicDirectory.Entry song;
public SongView(Context context) { private DownloadService downloadService;
public SongView(Context context) {
super(context); super(context);
LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true);
@ -72,15 +68,6 @@ public class SongView extends LinearLayout implements Checkable {
artistTextView = (TextView) findViewById(R.id.song_artist); artistTextView = (TextView) findViewById(R.id.song_artist);
durationTextView = (TextView) findViewById(R.id.song_duration); durationTextView = (TextView) findViewById(R.id.song_duration);
statusTextView = (TextView) findViewById(R.id.song_status); 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(final MusicDirectory.Entry song, boolean checkable) { public void setSong(final MusicDirectory.Entry song, boolean checkable) {
@ -157,23 +144,38 @@ public class SongView extends LinearLayout implements Checkable {
} }
}); });
updateBackground();
update(); update();
} }
@Override
protected void updateBackground() {
if (downloadService == null) {
downloadService = DownloadServiceImpl.getInstance();
if(downloadService == null) {
return;
}
}
downloadService.forSong(song);
}
private void update() { @Override
DownloadService downloadService = DownloadServiceImpl.getInstance(); protected void update() {
if (downloadService == null) { if (downloadService == null) {
return; return;
} }
DownloadFile downloadFile = downloadService.forSong(song); DownloadFile downloadFile = downloadService.forSong(song);
File completeFile = downloadFile.getCompleteFile(); downloadFile.getCompleteFile();
File partialFile = downloadFile.getPartialFile(); File partialFile = downloadFile.getPartialFile();
Drawable leftImage = null; Drawable leftImage = null;
Drawable rightImage = null; Drawable rightImage = null;
if (completeFile.exists()) { if (downloadFile.isWorkDone()) {
leftImage = downloadFile.isSaved() ? Util.getDrawableFromAttribute(getContext(), R.attr.unpin) : Util.getDrawableFromAttribute(getContext(), R.attr.downloaded); leftImage = downloadFile.isSaved() ? Util.getDrawableFromAttribute(getContext(), R.attr.unpin) : Util.getDrawableFromAttribute(getContext(), R.attr.downloaded);
} }
@ -200,34 +202,6 @@ public class SongView extends LinearLayout implements Checkable {
} }
} }
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 @Override
public void setChecked(boolean b) { public void setChecked(boolean b) {
checkedTextView.setChecked(b); checkedTextView.setChecked(b);

View File

@ -0,0 +1,115 @@
package com.thejoshwa.ultrasonic.androidapp.view;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;
public class UpdateView extends LinearLayout {
private static final String TAG = UpdateView.class.getSimpleName();
private static final WeakHashMap<UpdateView, ?> INSTANCES = new WeakHashMap<UpdateView, Object>();
private static Handler backgroundHandler;
private static Handler uiHandler;
private static Runnable updateRunnable;
public UpdateView(Context context) {
super(context);
setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
INSTANCES.put(this, null);
int instanceCount = INSTANCES.size();
if (instanceCount > 50) {
Log.w(TAG, instanceCount + " live UpdateView instances");
}
startUpdater();
}
@Override
public void setPressed(boolean pressed) {
}
private static synchronized void startUpdater() {
if(uiHandler != null) {
return;
}
uiHandler = new Handler();
updateRunnable = new Runnable() {
@Override
public void run() {
updateAll();
}
};
new Thread(new Runnable() {
public void run() {
Looper.prepare();
backgroundHandler = new Handler(Looper.myLooper());
uiHandler.post(updateRunnable);
Looper.loop();
}
}).start();
}
private static void updateAll() {
try {
List<UpdateView> views = new ArrayList<UpdateView>();;
for (UpdateView view : INSTANCES.keySet()) {
if (view.isShown()) {
views.add(view);
}
}
updateAllLive(views);
} catch (Throwable x) {
Log.w(TAG, "Error when updating song views.", x);
}
}
private static void updateAllLive(final List<UpdateView> views) {
final Runnable runnable = new Runnable() {
@Override
public void run() {
try {
for(UpdateView view: views) {
view.update();
}
} catch (Throwable x) {
Log.w(TAG, "Error when updating song views.", x);
}
uiHandler.postDelayed(updateRunnable, 1000L);
}
};
backgroundHandler.post(new Runnable() {
@Override
public void run() {
try {
for(UpdateView view: views) {
view.updateBackground();
}
uiHandler.post(runnable);
} catch (Throwable x) {
Log.w(TAG, "Error when updating song views.", x);
}
}
});
}
protected void updateBackground() {
}
protected void update() {
}
}