diff options
76 files changed, 3767 insertions, 1220 deletions
@@ -7,4 +7,6 @@ nbandroid/* .idea
subsonic-android.iml
releases/
-proguard_logs/
\ No newline at end of file +proguard_logs/
+/gen/
+/out/
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5e0b77e3..07ec734d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="github.daneren2005.dsub"
android:installLocation="internalOnly"
- android:versionCode="130"
- android:versionName="4.8.2">
+ android:versionCode="140"
+ android:versionName="4.9 Beta 2">
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="github.daneren2005.dsub"
@@ -86,7 +86,7 @@ <service android:name=".service.DownloadService"
android:label="Subsonic Download Service"/>
- <service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl"/>
+ <service android:name="org.fourthline.cling.android.AndroidUpnpServiceImpl"/>
<service android:name="github.daneren2005.dsub.service.sync.AuthenticatorService">
<intent-filter>
diff --git a/libs/cling-core-1.0.5.jar b/libs/cling-core-1.0.5.jar Binary files differdeleted file mode 100644 index 8079f329..00000000 --- a/libs/cling-core-1.0.5.jar +++ /dev/null diff --git a/libs/cling-core-2.0.1.jar b/libs/cling-core-2.0.1.jar Binary files differnew file mode 100644 index 00000000..632d3038 --- /dev/null +++ b/libs/cling-core-2.0.1.jar diff --git a/libs/cling-support-1.0.5.jar b/libs/cling-support-1.0.5.jar Binary files differdeleted file mode 100644 index a0ca6363..00000000 --- a/libs/cling-support-1.0.5.jar +++ /dev/null diff --git a/libs/cling-support-2.0.1.jar b/libs/cling-support-2.0.1.jar Binary files differnew file mode 100644 index 00000000..7fa28604 --- /dev/null +++ b/libs/cling-support-2.0.1.jar diff --git a/libs/javax.servlet-3.0.0.v201112011016.jar b/libs/javax.servlet-3.0.0.v201112011016.jar Binary files differnew file mode 100644 index 00000000..b1354096 --- /dev/null +++ b/libs/javax.servlet-3.0.0.v201112011016.jar diff --git a/libs/jetty-all-8.1.16.v20140903.jar b/libs/jetty-all-8.1.16.v20140903.jar Binary files differnew file mode 100644 index 00000000..25b1d324 --- /dev/null +++ b/libs/jetty-all-8.1.16.v20140903.jar diff --git a/libs/seamless-http-1.1.0.jar b/libs/seamless-http-1.1.0.jar Binary files differnew file mode 100644 index 00000000..98ec884a --- /dev/null +++ b/libs/seamless-http-1.1.0.jar diff --git a/libs/seamless-util-1.1.0.jar b/libs/seamless-util-1.1.0.jar Binary files differnew file mode 100644 index 00000000..12026b7f --- /dev/null +++ b/libs/seamless-util-1.1.0.jar diff --git a/libs/seamless-xml-1.1.0.jar b/libs/seamless-xml-1.1.0.jar Binary files differnew file mode 100644 index 00000000..1e740877 --- /dev/null +++ b/libs/seamless-xml-1.1.0.jar diff --git a/libs/teleal-common-1.0.13.jar b/libs/teleal-common-1.0.13.jar Binary files differdeleted file mode 100644 index 2d6403ef..00000000 --- a/libs/teleal-common-1.0.13.jar +++ /dev/null diff --git a/proguard.cfg b/proguard.cfg index b7df6dcf..4a536bf1 100644 --- a/proguard.cfg +++ b/proguard.cfg @@ -45,4 +45,31 @@ -keep class android.support.v7.app.MediaRouteButton { *; }
--dontwarn android.support.**
\ No newline at end of file +-dontwarn android.support.**
+
+# DLNA, needs to be stripped down to only what we need
+-keep class org.fourthline.** { *; }
+-keep interface org.fourthline.** { *; }
+-keep class org.seamless.** { *;}
+-keep interface org.seamless.http.** { *;}
+-keep class org.eclipse.** { *; }
+-keep interface org.eclipse.** { *; }
+-keep class javax.** { *; }
+-keep interface javax.** { *; }
+-keep class javax.** { *; }
+-keep interface javax.** { *; }
+-keep class org.objectweb.** { *; }
+-keep interface org.objectweb.** { *; }
+-keep class org.slf4j.** { *; }
+-keep interface org.slf4j.** { *; }
+-keep class org.mortbay.** { *; }
+-keep interface org.mortbay.** { *; }
+-dontwarn javax.**
+-dontwarn org.objectweb.**
+-dontwarn org.slf4j.**
+-dontwarn org.mortbay.**
+-dontwarn org.fourthline.**
+-dontwarn org.seamless.**
+-dontwarn org.eclipse.**
+-dontwarn java.**
+-keepattributes *Annotation*, InnerClasses
\ No newline at end of file diff --git a/project.properties b/project.properties index 819e411a..cfa8bb97 100644 --- a/project.properties +++ b/project.properties @@ -8,7 +8,7 @@ # project structure. # Project target. -target=android-19 +target=android-21 android.library.reference.1=DragSortListView/library android.library.reference.2=../../../../Program Files (x86)/Android/android-sdk/extras/android/support/v7/appcompat android.library.reference.3=../../../../Program Files (x86)/Android/android-sdk/extras/android/support/v7/mediarouter diff --git a/res/drawable-hdpi/btn_check_buttonless_off.png b/res/drawable-hdpi/btn_check_buttonless_off.png Binary files differdeleted file mode 100644 index d705b420..00000000 --- a/res/drawable-hdpi/btn_check_buttonless_off.png +++ /dev/null diff --git a/res/drawable-hdpi/btn_check_buttonless_on.png b/res/drawable-hdpi/btn_check_buttonless_on.png Binary files differdeleted file mode 100644 index a2612d7d..00000000 --- a/res/drawable-hdpi/btn_check_buttonless_on.png +++ /dev/null diff --git a/res/drawable-hdpi/ic_drawer.png b/res/drawable-hdpi/ic_drawer.png Binary files differdeleted file mode 100644 index eb90af58..00000000 --- a/res/drawable-hdpi/ic_drawer.png +++ /dev/null diff --git a/res/drawable-mdpi/ic_drawer.png b/res/drawable-mdpi/ic_drawer.png Binary files differdeleted file mode 100644 index 1681d12c..00000000 --- a/res/drawable-mdpi/ic_drawer.png +++ /dev/null diff --git a/res/drawable-xhdpi/ic_drawer.png b/res/drawable-xhdpi/ic_drawer.png Binary files differdeleted file mode 100644 index daba1451..00000000 --- a/res/drawable-xhdpi/ic_drawer.png +++ /dev/null diff --git a/res/drawable-xxhdpi/ic_drawer.png b/res/drawable-xxhdpi/ic_drawer.png Binary files differdeleted file mode 100644 index 9c4685d6..00000000 --- a/res/drawable-xxhdpi/ic_drawer.png +++ /dev/null diff --git a/res/drawable/btn_check.xml b/res/drawable/btn_check.xml deleted file mode 100644 index f363a2d2..00000000 --- a/res/drawable/btn_check.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2008 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - - <item android:state_checked="true" - android:drawable="@drawable/btn_check_buttonless_on" /> - - <item android:state_checked="false" - android:drawable="@drawable/btn_check_buttonless_off" /> - - <item - android:drawable="@drawable/btn_check_buttonless_off" /> - -</selector> diff --git a/res/layout/abstract_fragment_activity.xml b/res/layout/abstract_fragment_activity.xml index 0702397f..0268ff87 100644 --- a/res/layout/abstract_fragment_activity.xml +++ b/res/layout/abstract_fragment_activity.xml @@ -23,8 +23,7 @@ android:layout_width="50dip"
android:layout_height="50dip"
android:layout_gravity="left|center"
- android:scaleType="fitStart"
- android:src="@drawable/unknown_album"/>
+ android:scaleType="fitStart"/>
<LinearLayout
android:layout_width="0dp"
diff --git a/res/layout/album_cell_item.xml b/res/layout/album_cell_item.xml index 3dd79477..c1c8aa56 100644 --- a/res/layout/album_cell_item.xml +++ b/res/layout/album_cell_item.xml @@ -12,8 +12,7 @@ <github.daneren2005.dsub.view.SquareImageView
android:id="@+id/album_coverart"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:src="@drawable/unknown_album"/>
+ android:layout_height="match_parent"/>
<RatingBar
android:id="@+id/album_rating"
diff --git a/res/layout/album_list_item.xml b/res/layout/album_list_item.xml index 27ab3c63..202843b6 100644 --- a/res/layout/album_list_item.xml +++ b/res/layout/album_list_item.xml @@ -13,8 +13,7 @@ android:id="@+id/album_coverart"
android:layout_width="@dimen/AlbumArt.Small"
android:layout_height="@dimen/AlbumArt.Small"
- android:layout_gravity="left|center_vertical"
- android:src="@drawable/unknown_album"/>
+ android:layout_gravity="left|center_vertical"/>
<RatingBar
android:id="@+id/album_rating"
@@ -71,6 +70,5 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="right|center_vertical"
- android:paddingRight="10dip"
- style="@style/BasicButton"/>
+ style="@style/MoreButton"/>
</LinearLayout>
diff --git a/res/layout/basic_list_item.xml b/res/layout/basic_list_item.xml index 84526324..f40aef2e 100644 --- a/res/layout/basic_list_item.xml +++ b/res/layout/basic_list_item.xml @@ -33,6 +33,5 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="right|center_vertical"
- android:paddingRight="10dip"
- style="@style/BasicButton"/>
+ style="@style/MoreButton"/>
</LinearLayout>
\ No newline at end of file diff --git a/res/layout/complex_list_item.xml b/res/layout/complex_list_item.xml index 421295f2..a36cb2f6 100644 --- a/res/layout/complex_list_item.xml +++ b/res/layout/complex_list_item.xml @@ -45,6 +45,5 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="right|center_vertical"
- android:paddingRight="10dip"
- style="@style/BasicButton"/>
+ style="@style/MoreButton"/>
</LinearLayout>
\ No newline at end of file diff --git a/res/layout/grid_view.xml b/res/layout/grid_view.xml index 40674c8d..7690d975 100644 --- a/res/layout/grid_view.xml +++ b/res/layout/grid_view.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?>
-<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+<github.daneren2005.dsub.view.HeaderGridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gridview"
android:layout_width="fill_parent"
android:layout_height="0dip"
diff --git a/res/layout/preferences.xml b/res/layout/preferences.xml new file mode 100644 index 00000000..5caaa804 --- /dev/null +++ b/res/layout/preferences.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<ListView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/list" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:drawSelectorOnTop="false" + android:scrollbarAlwaysDrawVerticalTrack="true" + android:paddingTop="6dp" + android:paddingLeft="12dp" + android:paddingRight="12dp"/>
\ No newline at end of file diff --git a/res/layout/select_album_header.xml b/res/layout/select_album_header.xml index a253aa31..abc16e58 100644 --- a/res/layout/select_album_header.xml +++ b/res/layout/select_album_header.xml @@ -6,7 +6,6 @@ <ImageView android:id="@+id/select_album_art" - android:src="@drawable/unknown_album" android:layout_width="@dimen/AlbumArt.Header" android:layout_height="@dimen/AlbumArt.Header" android:layout_alignParentTop="true" @@ -16,6 +15,7 @@ android:contentDescription="@null"/> <LinearLayout + android:id="@+id/select_album_text_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_toRightOf="@+id/select_album_art" diff --git a/res/layout/song_list_item.xml b/res/layout/song_list_item.xml index d433df69..67d460f1 100644 --- a/res/layout/song_list_item.xml +++ b/res/layout/song_list_item.xml @@ -10,7 +10,7 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical"
- android:checkMark="@drawable/btn_check"
+ android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:paddingLeft="3dip"/>
<LinearLayout android:orientation="vertical"
@@ -122,6 +122,5 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="right|center_vertical"
- android:paddingRight="10dip"
- style="@style/BasicButton"/>
+ style="@style/MoreButton"/>
</LinearLayout>
diff --git a/res/layout/user_list_item.xml b/res/layout/user_list_item.xml index c4092894..ac408295 100644 --- a/res/layout/user_list_item.xml +++ b/res/layout/user_list_item.xml @@ -40,6 +40,5 @@ android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="right|center_vertical"
- android:paddingRight="10dip"
- style="@style/BasicButton"/>
+ style="@style/MoreButton"/>
</LinearLayout>
\ No newline at end of file diff --git a/res/menu/select_album.xml b/res/menu/select_album.xml index fa887c28..39eb2206 100644 --- a/res/menu/select_album.xml +++ b/res/menu/select_album.xml @@ -18,6 +18,10 @@ android:title="@string/menu.top_tracks"/> <item + android:id="@+id/menu_similar_artists" + android:title="@string/menu.similar_artists"/> + + <item android:id="@+id/menu_show_all" android:title="@string/menu.show_all"/> diff --git a/res/menu/select_album_list.xml b/res/menu/select_album_list.xml new file mode 100644 index 00000000..3b86fbcd --- /dev/null +++ b/res/menu/select_album_list.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:compat="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/menu_play_now" + android:icon="?media_button_start" + android:title="@string/menu.play" + compat:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_shuffle" + android:icon="?attr/shuffle" + android:title="@string/menu.shuffle" + compat:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_exit" + android:title="@string/menu.exit"/> +</menu> diff --git a/res/menu/similar_artists.xml b/res/menu/similar_artists.xml new file mode 100644 index 00000000..bffa1837 --- /dev/null +++ b/res/menu/similar_artists.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:compat="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/menu_show_missing" + android:title="@string/menu.show_missing"/> +</menu>
\ No newline at end of file diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index e49d6197..777ef4fd 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -192,8 +192,6 @@ <string name="download.jukebox_server_too_old">Control remoto no soportado. Por favor, actualice su servidor Subsonic.</string> <string name="download.jukebox_offline">Control remoto no disponible en modo offline.</string> <string name="download.jukebox_not_authorized">Control remoto no permitido. Por favor, active el modo jukebox en <b>Users > Settings</b> en su servidor Subsonic.</string> - <string name="download.show_downloading">Mostrar descargas</string> - <string name="download.show_now_playing">Mostrar reproduciendo ahora</string> <string name="download.timer_length">Temporizador</string> <string name="download.start_timer">Iniciar temporizador</string> <string name="download.stop_timer">Detener temporizador</string> diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index 6e0ff106..4b82ac85 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -560,6 +560,11 @@ <string name="tasker.start_playing_shuffled">Lejátszás indítása kevert sorrendben</string>
<string name="tasker.start_playing_title">Tasker -> DSub indítása</string>
<string name="tasker.edit_shuffle_mode">Indítás kevert sorrendben: </string>
+ <string name="tasker.edit_shuffle_start_year">Kevert sorrend kezdő év:</string>
+ <string name="tasker.edit_shuffle_end_year">Kevert sorrend utolsó év:</string>
+ <string name="tasker.edit_shuffle_genre">Kevert sorrend műfaja:</string>
+ <string name="tasker.edit_server_offline">Offline kapcsoló: </string>
+ <string name="tasker.edit_do_nothing">Ne csináljon semmit</string>
<plurals name="select_album_n_songs">
<item quantity="zero">Nincsenek dalok</item>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 5a009228..03f10808 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -129,8 +129,6 @@ <string name="download.jukebox_server_too_old">Удаленное управление не поддерживается. Пожалуйста, обновите Ваш сервер Subsonic.</string>
<string name="download.jukebox_offline">Удаленное управление не поддерживается в оффлайн режиме.</string>
<string name="download.jukebox_not_authorized">Удаленное управление запрещено. Пожалуйста, активируйте режим jukebox в разделе <b>Настройки > Проигрыватели</b> на вашем сервере Subsonic.</string>
- <string name="download.show_downloading">Показать закачки</string>
- <string name="download.show_now_playing">Показать воспроизведение</string>
<string name="download.timer_length">Длительность</string>
<string name="download.start_timer">Запустить таймер</string>
<string name="download.stop_timer">Остановить таймер</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml index 33a40981..314fc9bf 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -104,6 +104,8 @@ <string name="menu.rescan">Rescan Server</string>
<string name="menu.rate">Set Rating</string>
<string name="menu.top_tracks">Last.FM Top Tracks</string>
+ <string name="menu.similar_artists">Similar Artists</string>
+ <string name="menu.show_missing">Show missing</string>
<string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Update Information</string>
@@ -256,7 +258,7 @@ <string name="error.label">Error</string>
- <string name="settings.title">DSub settings</string>
+ <string name="settings.title">Settings</string>
<string name="settings.test_connection_title">Test connection</string>
<string name="settings.servers_add">Add Server</string>
<string name="settings.servers_remove">Remove Server</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml index e32811fa..43271afd 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -4,6 +4,10 @@ <item name="android:background">@drawable/abc_item_background_holo_light</item> </style> + <style name="MoreButton" parent="BasicButton"> + <item name="android:paddingRight">14dip</item> + </style> + <style name="PlaybackControl" parent="@style/BasicButton"> <item name="android:scaleType">fitCenter</item> <item name="android:padding">6dip</item> diff --git a/res/values/themes.xml b/res/values/themes.xml index 129c0612..70f30e56 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -31,11 +31,12 @@ <item name="drawerItemsIcons">@array/drawerItemIconsLight</item> <item name="android:textViewStyle">@style/DSub.TextViewStyle</item> <item name="android:buttonStyle">@style/DSub.ButtonStyle.Light</item> + <item name="drawerArrowStyle">@style/DSub.DrawerArrow</item> + <item name="colorAccent">@color/cyan</item> </style> <style name="Theme.DSub.Dark" parent="@style/Theme.AppCompat"> <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Dark</item> <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Dark</item> - <item name="android:textColorSecondary">@color/cyan</item> <item name="offline_icon">@drawable/main_offline_dark</item> <item name="media_button_backward">@drawable/media_backward_dark</item> <item name="media_button_forward">@drawable/media_forward_dark</item> @@ -64,42 +65,14 @@ <item name="drawerItemsIcons">@array/drawerItemIconsDark</item> <item name="android:textViewStyle">@style/DSub.TextViewStyle</item> <item name="android:buttonStyle">@style/DSub.ButtonStyle.Dark</item> + <item name="drawerArrowStyle">@style/DSub.DrawerArrow</item> + <item name="colorAccent">@color/cyan</item> </style> <style name="Theme.DSub.Black" parent="Theme.DSub.Dark"> <item name="android:windowBackground">@android:color/black</item> </style> - <style name="Theme.DSub.Holo" parent="@style/Theme.AppCompat"> - <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Holo</item> - <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Holo</item> + <style name="Theme.DSub.Holo" parent="Theme.DSub.Dark"> <item name="android:windowBackground">@drawable/background</item> - <item name="offline_icon">@drawable/main_offline_dark</item> - <item name="media_button_backward">@drawable/media_backward_dark</item> - <item name="media_button_forward">@drawable/media_forward_dark</item> - <item name="media_button_pause">@drawable/media_pause_dark</item> - <item name="media_button_repeat_off">@drawable/media_repeat_off</item> - <item name="media_button_start">@drawable/media_start_dark</item> - <item name="media_button_stop">@drawable/media_stop_dark</item> - <item name="chat_send">@drawable/ic_menu_chat_send_dark</item> - <item name="add">@drawable/ic_action_add_dark</item> - <item name="download_none">@drawable/download_none_dark</item> - <item name="shuffle">@drawable/ic_menu_shuffle_dark</item> - <item name="refresh">@drawable/ic_menu_refresh_dark</item> - <item name="search">@drawable/ic_menu_search_dark</item> - <item name="remove">@drawable/ic_menu_remove_dark</item> - <item name="save">@drawable/ic_menu_save_dark</item> - <item name="volume">@drawable/ic_action_volume_dark</item> - <item name="toggle_list">@drawable/action_toggle_list_dark</item> - <item name="select_server">@drawable/main_select_server_dark</item> - <item name="downloading">@drawable/downloading_dark</item> - <item name="bookmark">@drawable/ic_menu_bookmark_dark</item> - <item name="share">@drawable/ic_menu_share_dark</item> - <item name="add_person">@drawable/ic_menu_add_person_dark</item> - <item name="password">@drawable/ic_menu_password_dark</item> - <item name="rating_bad">@drawable/ic_action_rating_bad_dark</item> - <item name="rating_good">@drawable/ic_action_rating_good_dark</item> - <item name="drawerItemsIcons">@array/drawerItemIconsDark</item> - <item name="android:textViewStyle">@style/DSub.TextViewStyle</item> - <item name="android:buttonStyle">@style/DSub.ButtonStyle.Dark</item> </style> <style name="Widget.DSub.ActionBarStyle.Light" parent="Widget.AppCompat.Light.ActionBar.Solid"> @@ -108,20 +81,13 @@ <item name="backgroundStacked">@android:color/transparent</item> <item name="android:backgroundStacked">@android:color/transparent</item> </style> - + <style name="Widget.DSub.ActionBarStyle.Dark" parent="Widget.AppCompat.ActionBar.Solid"> <item name="background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item> <item name="backgroundStacked">@android:color/transparent</item> <item name="android:backgroundStacked">@android:color/transparent</item> </style> - - <style name="Widget.DSub.ActionBarStyle.Holo" parent="Widget.AppCompat.ActionBar.Solid"> - <item name="background">@android:color/transparent</item> - <item name="android:background">@android:color/transparent</item> - <item name="backgroundStacked">@android:color/transparent</item> - <item name="android:backgroundStacked">@android:color/transparent</item> - </style> <style name="DSub.TextViewStyle" parent="android:Widget.TextView"> </style> @@ -134,4 +100,8 @@ </style> <style name="DSub.ButtonStyle.Light" parent="android:Widget.Holo.Light.Button"> </style> + + <style name="DSub.DrawerArrow" parent="Widget.AppCompat.DrawerArrowToggle"> + <item name="spinBars">true</item> + </style> </resources> diff --git a/res/xml/changelog.xml b/res/xml/changelog.xml index 9c3e5040..11385ca6 100644 --- a/res/xml/changelog.xml +++ b/res/xml/changelog.xml @@ -1,5 +1,45 @@ <?xml version="1.0" encoding="utf-8"?> <changelog> + <release version="4.9 Beta 2" versioncode="136" releasedate="1/15/2014"> + <change>Fixed issue of DLNA not showing up for some people</change> + <change>Add artist info header (Subsonic 5.1+)</change> + <change>Add Similar Artists option (Subsonic 5.1+)</change> + <change>View similar artists missing from Subsonic (Subsonic 5.1+)</change> + <change>Podcasts: clicking on description wraps around image to display everything</change> + <change>Delete artwork/avatars on Clean Cache</change> + <change>Fix sleep timer incrementing on it's own</change> + <change>Fixed various crashes</change> + </release> + <release version="4.9 Beta 1" versioncode="135" releasedate="12/27/2014"> + <change>Early DLNA support</change> + <change>I have only tested against XBMC. I need feedback on what does and doesn't work</change> + <change>I know no metadata shows up. I haven't been able to figure that part out yet.</change> + </release> + <release version="4.8.6" versioncode="134" releasedate="12/27/2014"> + <change>Play/shuffle quick album lists such as Recently Added or Random</change> + <change>Change download status to a percentage</change> + <change>Improved unknown album art</change> + <change>Allow any size cache to be set</change> + <change>Improved search sort order</change> + <change>Fix settings coloring on older versions of Android</change> + <change>Fix sleep timer not remembering last value</change> + <change>Fix caching not working while casting</change> + </release> + <release version="4.8.5" versioncode="133" releasedate="11/26/2014"> + <change>Fix crash on GB</change> + <change>Fix some theme issues</change> + </release> + <release version="4.8.4" versioncode="132" releasedate="11/22/2014"> + <change>Partial Material Theme update</change> + <change>Make playing notification public for Lolipop</change> + <change>Fix Lolipop connectivity issues for some users</change> + <change>Fix cache from playlist view downloading starred songs instead</change> + <change>Fix remove from playlist not showing up on MusicCabinet servers</change> + </release> + <release version="4.8.3" versioncode="131" releasedate="11/14/2014"> + <change>Fix color on Lolipop lockscreen notification</change> + <change>Various bug fixes</change> + </release> <release version="4.8.2" versioncode="130" releasedate="11/2/2014"> <change>Improve automatic bookmark logic</change> <change>Tasker: Toggle online/offline</change> diff --git a/res/xml/settings.xml b/res/xml/settings.xml index d0dcdc43..0f044476 100644 --- a/res/xml/settings.xml +++ b/res/xml/settings.xml @@ -4,7 +4,7 @@ xmlns:myns="http://schemas.android.com/apk/res/github.daneren2005.dsub" android:title="@string/settings.title"> - <PreferenceScreen + <PreferenceScreen android:title="@string/settings.servers_title"> <PreferenceCategory @@ -17,7 +17,7 @@ android:title="@string/settings.servers_add"/> </PreferenceCategory> - </PreferenceScreen> + </PreferenceScreen> <PreferenceScreen android:title="@string/settings.appearance_title"> @@ -31,7 +31,7 @@ android:defaultValue="light" android:entryValues="@array/themeValues" android:entries="@array/themeNames"/> - + <CheckBoxPreference android:title="@string/settings.theme_fullscreen" android:summary="@string/settings.theme_fullscreen_summary" @@ -212,13 +212,11 @@ <PreferenceCategory android:title="@string/settings.cache_title"> - <github.daneren2005.dsub.view.SeekBarPreference + <EditTextPreference android:title="@string/settings.cache_size" android:key="cacheSize" android:defaultValue="2000" - android:dialogLayout="@layout/seekbar_preference" - myns:max="20000" - myns:display="%.0f MB"/> + android:digits="0123456789"/> <EditTextPreference android:title="@string/settings.cache_location" diff --git a/src/github/daneren2005/dsub/activity/SettingsActivity.java b/src/github/daneren2005/dsub/activity/SettingsActivity.java index 0dd68fcb..d5ac60d3 100644 --- a/src/github/daneren2005/dsub/activity/SettingsActivity.java +++ b/src/github/daneren2005/dsub/activity/SettingsActivity.java @@ -25,7 +25,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -33,9 +32,9 @@ import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; +import android.support.v7.app.ActionBarActivity; import android.text.InputType; import android.util.Log; import android.view.MenuItem; @@ -43,8 +42,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; +import android.widget.FrameLayout; import github.daneren2005.dsub.R; +import github.daneren2005.dsub.fragments.PreferenceCompatFragment; +import github.daneren2005.dsub.fragments.SettingsFragment; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.MusicService; import github.daneren2005.dsub.service.MusicServiceFactory; @@ -59,660 +61,31 @@ import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; -import java.security.acl.Group; +import java.text.DecimalFormat; import java.util.LinkedHashMap; -import java.util.Locale; import java.util.Map; -public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { +public class SettingsActivity extends SubsonicActivity { private static final String TAG = SettingsActivity.class.getSimpleName(); - private final Map<String, ServerSettings> serverSettings = new LinkedHashMap<String, ServerSettings>(); - private boolean testingConnection; - private ListPreference theme; - private ListPreference maxBitrateWifi; - private ListPreference maxBitrateMobile; - private ListPreference maxVideoBitrateWifi; - private ListPreference maxVideoBitrateMobile; - private ListPreference networkTimeout; - private EditTextPreference cacheLocation; - private ListPreference preloadCountWifi; - private ListPreference preloadCountMobile; - private ListPreference tempLoss; - private ListPreference pauseDisconnect; - private Preference addServerPreference; - private PreferenceCategory serversCategory; - private ListPreference videoPlayer; - private ListPreference syncInterval; - private CheckBoxPreference syncEnabled; - private CheckBoxPreference syncWifi; - private CheckBoxPreference syncNotification; - private CheckBoxPreference syncStarred; - private CheckBoxPreference syncMostRecent; - private CheckBoxPreference replayGain; - private ListPreference replayGainType; - private Preference replayGainBump; - private Preference replayGainUntagged; - private String internalSSID; - private String internalSSIDDisplay; - - private int serverCount = 3; - private SharedPreferences settings; + private PreferenceCompatFragment fragment; @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) @Override public void onCreate(Bundle savedInstanceState) { - applyTheme(); super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.settings); + setContentView(R.layout.download_activity); - internalSSID = Util.getSSID(this); - if(internalSSID == null) { - internalSSID = ""; - } - internalSSIDDisplay = this.getResources().getString(R.string.settings_server_local_network_ssid_hint, internalSSID); - - theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME); - maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI); - maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE); - maxVideoBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI); - maxVideoBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE); - networkTimeout = (ListPreference) findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); - cacheLocation = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); - preloadCountWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI); - preloadCountMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE); - tempLoss = (ListPreference) findPreference(Constants.PREFERENCES_KEY_TEMP_LOSS); - pauseDisconnect = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PAUSE_DISCONNECT); - serversCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_SERVER_KEY); - addServerPreference = (Preference) findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); - videoPlayer = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER); - syncInterval = (ListPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL); - syncEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED); - syncWifi = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_WIFI); - syncNotification = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION); - syncStarred = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_STARRED); - syncMostRecent = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT); - replayGain = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN); - replayGainType = (ListPreference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE); - replayGainBump = (Preference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP); - replayGainUntagged = (Preference) findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED); - - settings = Util.getPreferences(this); - serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); - - findPreference("clearCache").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Util.confirmDialog(SettingsActivity.this, R.string.common_delete, R.string.common_confirm_message_cache, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - new LoadingTask<Void>(SettingsActivity.this, false) { - @Override - protected Void doInBackground() throws Throwable { - FileUtil.deleteMusicDirectory(SettingsActivity.this); - FileUtil.deleteSerializedCache(SettingsActivity.this); - return null; - } - - @Override - protected void done(Void result) { - Util.toast(SettingsActivity.this, R.string.settings_cache_clear_complete); - } - - @Override - protected void error(Throwable error) { - Util.toast(SettingsActivity.this, getErrorMessage(error), false); - } - }.execute(); - } - }); - return false; - } - }); - - addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - serverCount++; - String instance = String.valueOf(serverCount); - - Preference addServerPreference = findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); - serversCategory.addPreference(addServer(serverCount)); - - SharedPreferences.Editor editor = settings.edit(); - editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); - // Reset set folder ID - editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); - editor.commit(); - - serverSettings.put(instance, new ServerSettings(instance)); - - return true; - } - }); - - findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Boolean syncEnabled = (Boolean) newValue; - - Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); - ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled); - ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, syncEnabled); + if (savedInstanceState == null) { + fragment = new SettingsFragment(); + Bundle args = new Bundle(); + args.putInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, R.xml.settings); - return true; - } - }); - syncInterval.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Integer syncInterval = Integer.parseInt(((String) newValue)); + fragment.setArguments(args); + fragment.setRetainInstance(true); - Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); - ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval); - ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, new Bundle(), 60L * syncInterval); - - return true; - } - }); - - serversCategory.setOrderingAsAdded(false); - for (int i = 1; i <= serverCount; i++) { - String instance = String.valueOf(i); - serversCategory.addPreference(addServer(i)); - serverSettings.put(instance, new ServerSettings(instance)); - } - - SharedPreferences prefs = Util.getPreferences(this); - prefs.registerOnSharedPreferenceChangeListener(this); - - update(); - - if(Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - getActionBar().setDisplayHomeAsUpEnabled(true); - getActionBar().setHomeButtonEnabled(true); + currentFragment = fragment; + currentFragment.setPrimaryFragment(true); + getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, currentFragment, currentFragment.getSupportTag() + "").commit(); } } - - @Override - protected void onDestroy() { - super.onDestroy(); - - SharedPreferences prefs = Util.getPreferences(this); - prefs.unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if(item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - - return false; - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - // Random error I have no idea how to reproduce - if(sharedPreferences == null) { - return; - } - - update(); - - if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) { - setHideMedia(sharedPreferences.getBoolean(key, false)); - } - else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) { - setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true)); - } - else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) { - setCacheLocation(sharedPreferences.getString(key, "")); - } - else if (Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION.equals(key)){ - DownloadService downloadService = DownloadService.getInstance(); - downloadService.setSleepTimerDuration(Integer.parseInt(sharedPreferences.getString(key, "60"))); - } - else if(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT.equals(key)) { - SyncUtil.removeMostRecentSyncFiles(this); - } else if(Constants.PREFERENCES_KEY_REPLAY_GAIN.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED.equals(key)) { - DownloadService downloadService = DownloadService.getInstance(); - if(downloadService != null) { - downloadService.reapplyVolume(); - } - } - - scheduleBackup(); - } - - private void scheduleBackup() { - try { - Class managerClass = Class.forName("android.app.backup.BackupManager"); - Constructor managerConstructor = managerClass.getConstructor(Context.class); - Object manager = managerConstructor.newInstance(this); - Method m = managerClass.getMethod("dataChanged"); - m.invoke(manager); - Log.d(TAG, "Backup requested"); - } catch(ClassNotFoundException e) { - Log.d(TAG, "No backup manager found"); - } catch(Throwable t) { - Log.d(TAG, "Scheduling backup failed " + t); - t.printStackTrace(); - } - } - - private void update() { - if (testingConnection) { - return; - } - - theme.setSummary(theme.getEntry()); - maxBitrateWifi.setSummary(maxBitrateWifi.getEntry()); - maxBitrateMobile.setSummary(maxBitrateMobile.getEntry()); - maxVideoBitrateWifi.setSummary(maxVideoBitrateWifi.getEntry()); - maxVideoBitrateMobile.setSummary(maxVideoBitrateMobile.getEntry()); - networkTimeout.setSummary(networkTimeout.getEntry()); - cacheLocation.setSummary(cacheLocation.getText()); - preloadCountWifi.setSummary(preloadCountWifi.getEntry()); - preloadCountMobile.setSummary(preloadCountMobile.getEntry()); - tempLoss.setSummary(tempLoss.getEntry()); - pauseDisconnect.setSummary(pauseDisconnect.getEntry()); - videoPlayer.setSummary(videoPlayer.getEntry()); - syncInterval.setSummary(syncInterval.getEntry()); - if(syncEnabled.isChecked()) { - if(!syncInterval.isEnabled()) { - syncInterval.setEnabled(true); - syncWifi.setEnabled(true); - syncNotification.setEnabled(true); - syncStarred.setEnabled(true); - syncMostRecent.setEnabled(true); - } - } else { - if(syncInterval.isEnabled()) { - syncInterval.setEnabled(false); - syncWifi.setEnabled(false); - syncNotification.setEnabled(false); - syncStarred.setEnabled(false); - syncMostRecent.setEnabled(false); - } - } - if(replayGain.isChecked()) { - replayGainType.setEnabled(true); - replayGainBump.setEnabled(true); - replayGainUntagged.setEnabled(true); - } else { - replayGainType.setEnabled(false); - replayGainBump.setEnabled(false); - replayGainUntagged.setEnabled(false); - } - replayGainType.setSummary(replayGainType.getEntry()); - - for (ServerSettings ss : serverSettings.values()) { - ss.update(); - } - } - - private PreferenceScreen addServer(final int instance) { - final PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(this); - screen.setTitle(R.string.settings_server_unused); - screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance); - - final EditTextPreference serverNamePreference = new EditTextPreference(this); - serverNamePreference.setKey(Constants.PREFERENCES_KEY_SERVER_NAME + instance); - serverNamePreference.setDefaultValue(getResources().getString(R.string.settings_server_unused)); - serverNamePreference.setTitle(R.string.settings_server_name); - serverNamePreference.setDialogTitle(R.string.settings_server_name); - - if (serverNamePreference.getText() == null) { - serverNamePreference.setText(getResources().getString(R.string.settings_server_unused)); - } - - serverNamePreference.setSummary(serverNamePreference.getText()); - - final EditTextPreference serverUrlPreference = new EditTextPreference(this); - serverUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_URL + instance); - serverUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); - serverUrlPreference.setDefaultValue("http://yourhost"); - serverUrlPreference.setTitle(R.string.settings_server_address); - serverUrlPreference.setDialogTitle(R.string.settings_server_address); - - if (serverUrlPreference.getText() == null) { - serverUrlPreference.setText("http://yourhost"); - } - - serverUrlPreference.setSummary(serverUrlPreference.getText()); - screen.setSummary(serverUrlPreference.getText()); - - final EditTextPreference serverLocalNetworkSSIDPreference = new EditTextPreference(this) { - @Override - protected void onAddEditTextToDialogView(View dialogView, final EditText editText) { - super.onAddEditTextToDialogView(dialogView, editText); - ViewGroup root = (ViewGroup) ((ViewGroup) dialogView).getChildAt(0); - - Button defaultButton = new Button(getContext()); - defaultButton.setText(internalSSIDDisplay); - defaultButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - editText.setText(internalSSID); - } - }); - root.addView(defaultButton); - } - }; - serverLocalNetworkSSIDPreference.setKey(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance); - serverLocalNetworkSSIDPreference.setTitle(R.string.settings_server_local_network_ssid); - serverLocalNetworkSSIDPreference.setDialogTitle(R.string.settings_server_local_network_ssid); - - final EditTextPreference serverInternalUrlPreference = new EditTextPreference(this); - serverInternalUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance); - serverInternalUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); - serverInternalUrlPreference.setDefaultValue(""); - serverInternalUrlPreference.setTitle(R.string.settings_server_internal_address); - serverInternalUrlPreference.setDialogTitle(R.string.settings_server_internal_address); - serverInternalUrlPreference.setSummary(serverInternalUrlPreference.getText()); - - final EditTextPreference serverUsernamePreference = new EditTextPreference(this); - serverUsernamePreference.setKey(Constants.PREFERENCES_KEY_USERNAME + instance); - serverUsernamePreference.setTitle(R.string.settings_server_username); - serverUsernamePreference.setDialogTitle(R.string.settings_server_username); - - final EditTextPreference serverPasswordPreference = new EditTextPreference(this); - serverPasswordPreference.setKey(Constants.PREFERENCES_KEY_PASSWORD + instance); - serverPasswordPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - serverPasswordPreference.setSummary("***"); - serverPasswordPreference.setTitle(R.string.settings_server_password); - - final CheckBoxPreference serverTagPreference = new CheckBoxPreference(this); - serverTagPreference.setKey(Constants.PREFERENCES_KEY_BROWSE_TAGS + instance); - serverTagPreference.setChecked(Util.isTagBrowsing(this, instance)); - serverTagPreference.setSummary(R.string.settings_browse_by_tags_summary); - serverTagPreference.setTitle(R.string.settings_browse_by_tags); - serverPasswordPreference.setDialogTitle(R.string.settings_server_password); - - final CheckBoxPreference serverSyncPreference = new CheckBoxPreference(this); - serverSyncPreference.setKey(Constants.PREFERENCES_KEY_SERVER_SYNC + instance); - serverSyncPreference.setChecked(Util.isSyncEnabled(this, instance)); - serverSyncPreference.setSummary(R.string.settings_server_sync_summary); - serverSyncPreference.setTitle(R.string.settings_server_sync); - - final Preference serverOpenBrowser = new Preference(this); - serverOpenBrowser.setKey(Constants.PREFERENCES_KEY_OPEN_BROWSER); - serverOpenBrowser.setPersistent(false); - serverOpenBrowser.setTitle(R.string.settings_server_open_browser); - serverOpenBrowser.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - openInBrowser(instance); - return true; - } - }); - - Preference serverRemoveServerPreference = new Preference(this); - serverRemoveServerPreference.setKey(Constants.PREFERENCES_KEY_SERVER_REMOVE + instance); - serverRemoveServerPreference.setPersistent(false); - serverRemoveServerPreference.setTitle(R.string.settings_servers_remove); - - serverRemoveServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Util.confirmDialog(SettingsActivity.this, R.string.common_delete, screen.getTitle().toString(), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // Reset values to null so when we ask for them again they are new - serverNamePreference.setText(null); - serverUrlPreference.setText(null); - serverUsernamePreference.setText(null); - serverPasswordPreference.setText(null); - - int activeServer = Util.getActiveServer(SettingsActivity.this); - for (int i = instance; i <= serverCount; i++) { - Util.removeInstanceName(SettingsActivity.this, i, activeServer); - } - - serverCount--; - SharedPreferences.Editor editor = settings.edit(); - editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); - editor.commit(); - - serversCategory.removePreference(screen); - screen.getDialog().dismiss(); - } - }); - - return true; - } - }); - - Preference serverTestConnectionPreference = new Preference(this); - serverTestConnectionPreference.setKey(Constants.PREFERENCES_KEY_TEST_CONNECTION + instance); - serverTestConnectionPreference.setPersistent(false); - serverTestConnectionPreference.setTitle(R.string.settings_test_connection_title); - serverTestConnectionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - testConnection(instance); - return false; - } - }); - - screen.addPreference(serverNamePreference); - screen.addPreference(serverUrlPreference); - screen.addPreference(serverInternalUrlPreference); - screen.addPreference(serverLocalNetworkSSIDPreference); - screen.addPreference(serverUsernamePreference); - screen.addPreference(serverPasswordPreference); - screen.addPreference(serverTagPreference); - screen.addPreference(serverSyncPreference); - screen.addPreference(serverTestConnectionPreference); - screen.addPreference(serverOpenBrowser); - screen.addPreference(serverRemoveServerPreference); - - screen.setOrder(instance); - - return screen; - } - - private void applyTheme() { - String activeTheme = Util.getTheme(this); - Util.applyTheme(this, activeTheme); - } - - private void setHideMedia(boolean hide) { - File nomediaDir = new File(FileUtil.getSubsonicDirectory(this), ".nomedia"); - File musicNoMedia = new File(FileUtil.getMusicDirectory(this), ".nomedia"); - if (hide && !nomediaDir.exists()) { - try { - if (!nomediaDir.createNewFile()) { - Log.w(TAG, "Failed to create " + nomediaDir); - } - } catch(Exception e) { - Log.w(TAG, "Failed to create " + nomediaDir, e); - } - - try { - if(!musicNoMedia.createNewFile()) { - Log.w(TAG, "Failed to create " + musicNoMedia); - } - } catch(Exception e) { - Log.w(TAG, "Failed to create " + musicNoMedia, e); - } - } else if (nomediaDir.exists()) { - if (!nomediaDir.delete()) { - Log.w(TAG, "Failed to delete " + nomediaDir); - } - if(!musicNoMedia.delete()) { - Log.w(TAG, "Failed to delete " + musicNoMedia); - } - } - Util.toast(this, R.string.settings_hide_media_toast, false); - } - - private void setMediaButtonsEnabled(boolean enabled) { - if (enabled) { - Util.registerMediaButtonEventReceiver(this); - } else { - Util.unregisterMediaButtonEventReceiver(this); - } - } - - private void setCacheLocation(String path) { - File dir = new File(path); - if (!FileUtil.verifyCanWrite(dir)) { - Util.toast(this, R.string.settings_cache_location_error, false); - - // Reset it to the default. - String defaultPath = FileUtil.getDefaultMusicDirectory(this).getPath(); - if (!defaultPath.equals(path)) { - SharedPreferences prefs = Util.getPreferences(this); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); - editor.commit(); - cacheLocation.setSummary(defaultPath); - cacheLocation.setText(defaultPath); - } - - // Clear download queue. - DownloadService downloadService = DownloadService.getInstance(); - downloadService.clear(); - } - } - - private void testConnection(final int instance) { - LoadingTask<Boolean> task = new LoadingTask<Boolean>(this) { - private int previousInstance; - - @Override - protected Boolean doInBackground() throws Throwable { - updateProgress(R.string.settings_testing_connection); - - previousInstance = Util.getActiveServer(SettingsActivity.this); - testingConnection = true; - Util.setActiveServer(SettingsActivity.this, instance); - try { - MusicService musicService = MusicServiceFactory.getMusicService(SettingsActivity.this); - musicService.ping(SettingsActivity.this, this); - return musicService.isLicenseValid(SettingsActivity.this, null); - } finally { - Util.setActiveServer(SettingsActivity.this, previousInstance); - testingConnection = false; - } - } - - @Override - protected void done(Boolean licenseValid) { - if (licenseValid) { - Util.toast(SettingsActivity.this, R.string.settings_testing_ok); - } else { - Util.toast(SettingsActivity.this, R.string.settings_testing_unlicensed); - } - } - - @Override - public void cancel() { - super.cancel(); - Util.setActiveServer(SettingsActivity.this, previousInstance); - } - - @Override - protected void error(Throwable error) { - Log.w(TAG, error.toString(), error); - new ErrorDialog(SettingsActivity.this, getResources().getString(R.string.settings_connection_failure) + - " " + getErrorMessage(error), false); - } - }; - task.execute(); - } - - private void openInBrowser(final int instance) { - SharedPreferences prefs = Util.getPreferences(this); - String url = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); - if(url == null) { - new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false); - return; - } - Uri uriServer = Uri.parse(url); - - Intent browserIntent = new Intent(Intent.ACTION_VIEW, uriServer); - startActivity(browserIntent); - } - - private class ServerSettings { - private EditTextPreference serverName; - private EditTextPreference serverUrl; - private EditTextPreference serverLocalNetworkSSID; - private EditTextPreference serverInternalUrl; - private EditTextPreference username; - private PreferenceScreen screen; - - private ServerSettings(String instance) { - - screen = (PreferenceScreen) findPreference("server" + instance); - serverName = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance); - serverUrl = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance); - serverLocalNetworkSSID = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance); - serverInternalUrl = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance); - username = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_USERNAME + instance); - - serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - try { - String url = (String) value; - new URL(url); - if (url.contains(" ") || url.contains("@") || url.contains("_")) { - throw new Exception(); - } - } catch (Exception x) { - new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false); - return false; - } - return true; - } - }); - serverInternalUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - try { - String url = (String) value; - // Allow blank internal IP address - if("".equals(url) || url == null) { - return true; - } - - new URL(url); - if (url.contains(" ") || url.contains("@") || url.contains("_")) { - throw new Exception(); - } - } catch (Exception x) { - new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false); - return false; - } - return true; - } - }); - - username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - String username = (String) value; - if (username == null || !username.equals(username.trim())) { - new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_username, false); - return false; - } - return true; - } - }); - } - - public void update() { - serverName.setSummary(serverName.getText()); - serverUrl.setSummary(serverUrl.getText()); - serverLocalNetworkSSID.setSummary(serverLocalNetworkSSID.getText()); - serverInternalUrl.setSummary(serverInternalUrl.getText()); - username.setSummary(username.getText()); - screen.setSummary(serverUrl.getText()); - screen.setTitle(serverName.getText()); - } - } } diff --git a/src/github/daneren2005/dsub/activity/SubsonicActivity.java b/src/github/daneren2005/dsub/activity/SubsonicActivity.java index c3f8e4a7..f73bccb8 100644 --- a/src/github/daneren2005/dsub/activity/SubsonicActivity.java +++ b/src/github/daneren2005/dsub/activity/SubsonicActivity.java @@ -28,7 +28,7 @@ import android.media.AudioManager; import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
-import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
@@ -101,8 +101,8 @@ public class SubsonicActivity extends ActionBarActivity implements OnItemSelecte protected void onCreate(Bundle bundle) {
setUncaughtExceptionHandler();
applyTheme();
- super.onCreate(bundle);
applyFullscreen();
+ super.onCreate(bundle);
startService(new Intent(this, DownloadService.class));
setVolumeControlStream(AudioManager.STREAM_MUSIC);
@@ -164,8 +164,13 @@ public class SubsonicActivity extends ActionBarActivity implements OnItemSelecte @Override
public void startActivity(Intent intent) {
- if(intent.getComponent() != null && "github.daneren2005.dsub.activity.DownloadActivity".equals(intent.getComponent().getClassName())) {
- intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition);
+ if(intent.getComponent() != null) {
+ String name = intent.getComponent().getClassName();
+ if(name != null && name.indexOf("DownloadActivity") != -1) {
+ intent.putExtra(Constants.FRAGMENT_POSITION, lastSelectedPosition);
+ } else if(name != null && name.indexOf("SettingsActivity") != -1) {
+ intent.putExtra(Constants.FRAGMENT_POSITION, drawerItems.length - 1);
+ }
}
super.startActivity(intent);
}
@@ -174,9 +179,11 @@ public class SubsonicActivity extends ActionBarActivity implements OnItemSelecte public void setContentView(int viewId) {
super.setContentView(R.layout.abstract_activity);
rootView = (ViewGroup) findViewById(R.id.content_frame);
- LayoutInflater layoutInflater = getLayoutInflater();
- layoutInflater.inflate(viewId, rootView);
+ if(viewId != 0) {
+ LayoutInflater layoutInflater = getLayoutInflater();
+ layoutInflater.inflate(viewId, rootView);
+ }
drawerList = (ListView) findViewById(R.id.left_drawer);
@@ -201,7 +208,7 @@ public class SubsonicActivity extends ActionBarActivity implements OnItemSelecte });
drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
- drawerToggle = new ActionBarDrawerToggle(this, drawer, R.drawable.ic_drawer, R.string.common_appname, R.string.common_appname) {
+ drawerToggle = new ActionBarDrawerToggle(this, drawer, R.string.common_appname, R.string.common_appname) {
@Override
public void onDrawerClosed(View view) {
setTitle(currentFragment.getTitle());
@@ -221,7 +228,7 @@ public class SubsonicActivity extends ActionBarActivity implements OnItemSelecte drawerAdapter.setDownloadVisible(true);
}
- if(lastSelectedView == null) {
+ if(lastSelectedView == null && drawerList.getCount() > lastSelectedPosition) {
lastSelectedView = (TextView) drawerList.getChildAt(lastSelectedPosition).findViewById(R.id.drawer_name);
if(lastSelectedView != null) {
lastSelectedView.setTextAppearance(SubsonicActivity.this, R.style.DSub_TextViewStyle_Bold);
@@ -372,7 +379,7 @@ public class SubsonicActivity extends ActionBarActivity implements OnItemSelecte @Override
public void setTitle(CharSequence title) {
- if(!title.equals(getSupportActionBar().getTitle())) {
+ if(title != null && !title.equals(getSupportActionBar().getTitle())) {
getSupportActionBar().setTitle(title);
recreateSpinner();
}
diff --git a/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java b/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java index 3d1f8aab..74ef4894 100644 --- a/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java +++ b/src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java @@ -442,16 +442,16 @@ public class SubsonicFragmentActivity extends SubsonicActivity { currentState = state;
}
- if(current == null) {
+ MusicDirectory.Entry song = null;
+ if(current != null) {
+ song = current.getSong();
+ trackView.setText(song.getTitle());
+ artistView.setText(song.getArtist());
+ } else {
trackView.setText("Title");
artistView.setText("Artist");
- getImageLoader().loadImage(coverArtView, null, false, false);
- return;
}
- MusicDirectory.Entry song = current.getSong();
- trackView.setText(song.getTitle());
- artistView.setText(song.getArtist());
getImageLoader().loadImage(coverArtView, song, false, false);
int[] attrs = new int[] {(state == PlayerState.STARTED) ? R.attr.media_button_pause : R.attr.media_button_start};
TypedArray typedArray = this.obtainStyledAttributes(attrs);
diff --git a/src/github/daneren2005/dsub/domain/ArtistInfo.java b/src/github/daneren2005/dsub/domain/ArtistInfo.java new file mode 100644 index 00000000..2205d561 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/ArtistInfo.java @@ -0,0 +1,76 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.domain; + +import java.io.Serializable; +import java.util.List; + +public class ArtistInfo implements Serializable { + private String biography; + private String musicBrainzId; + private String lastFMUrl; + private String imageUrl; + private List<Artist> similarArtists; + private List<String> missingArtists; + + public String getBiography() { + return biography; + } + + public void setBiography(String biography) { + this.biography = biography; + } + + public String getMusicBrainzId() { + return musicBrainzId; + } + + public void setMusicBrainzId(String musicBrainzId) { + this.musicBrainzId = musicBrainzId; + } + + public String getLastFMUrl() { + return lastFMUrl; + } + + public void setLastFMUrl(String lastFMUrl) { + this.lastFMUrl = lastFMUrl; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public List<Artist> getSimilarArtists() { + return similarArtists; + } + + public void setSimilarArtists(List<Artist> similarArtists) { + this.similarArtists = similarArtists; + } + + public List<String> getMissingArtists() { + return missingArtists; + } + + public void setMissingArtists(List<String> missingArtists) { + this.missingArtists = missingArtists; + } +} diff --git a/src/github/daneren2005/dsub/domain/DLNADevice.java b/src/github/daneren2005/dsub/domain/DLNADevice.java index b18000d0..2de84013 100644 --- a/src/github/daneren2005/dsub/domain/DLNADevice.java +++ b/src/github/daneren2005/dsub/domain/DLNADevice.java @@ -22,10 +22,13 @@ package github.daneren2005.dsub.domain; import android.os.Parcel;
import android.os.Parcelable;
+import org.fourthline.cling.model.meta.Device;
+
/**
* Created by Scott on 11/1/2014.
*/
public class DLNADevice implements Parcelable {
+ public Device renderer;
public String id;
public String name;
public String description;
@@ -50,7 +53,8 @@ public class DLNADevice implements Parcelable { volumeMax = in.readInt();
}
- public DLNADevice(String id, String name, String description, int volume, int volumeMax) {
+ public DLNADevice(Device renderer, String id, String name, String description, int volume, int volumeMax) {
+ this.renderer = renderer;
this.id = id;
this.name = name;
this.description = description;
diff --git a/src/github/daneren2005/dsub/domain/Version.java b/src/github/daneren2005/dsub/domain/Version.java index f3566644..6b82ea99 100644 --- a/src/github/daneren2005/dsub/domain/Version.java +++ b/src/github/daneren2005/dsub/domain/Version.java @@ -88,6 +88,8 @@ public class Version implements Comparable<Version>, Serializable { return "4.8"; case 10: return "4.9"; + case 11: + return "5.1"; } } return ""; diff --git a/src/github/daneren2005/dsub/fragments/MainFragment.java b/src/github/daneren2005/dsub/fragments/MainFragment.java index 403bad03..f6f7875c 100644 --- a/src/github/daneren2005/dsub/fragments/MainFragment.java +++ b/src/github/daneren2005/dsub/fragments/MainFragment.java @@ -374,10 +374,10 @@ public class MainFragment extends SubsonicFragment { return getResources().getString(R.string.main_about_text,
context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName,
used.getFirst(),
- Util.formatBytes(used.getSecond()),
- Util.formatBytes(Util.getCacheSizeMB(context) * 1024L * 1024L),
- Util.formatBytes(bytesAvailableFs),
- Util.formatBytes(bytesTotalFs));
+ Util.formatLocalizedBytes(used.getSecond(), context),
+ Util.formatLocalizedBytes(Util.getCacheSizeMB(context) * 1024L * 1024L, context),
+ Util.formatLocalizedBytes(bytesAvailableFs, context),
+ Util.formatLocalizedBytes(bytesTotalFs, context));
}
@Override
diff --git a/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java b/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java index 4ec439b2..c0f528de 100644 --- a/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java +++ b/src/github/daneren2005/dsub/fragments/NowPlayingFragment.java @@ -233,7 +233,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis previousButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
new SilentBackgroundTask<Void>(context) {
@Override
protected Void doInBackground() throws Throwable {
@@ -259,7 +259,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
new SilentBackgroundTask<Boolean>(context) {
@Override
protected Boolean doInBackground() throws Throwable {
@@ -325,7 +325,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
new SilentBackgroundTask<Void>(context) {
@Override
protected Void doInBackground() throws Throwable {
@@ -504,7 +504,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
new SilentBackgroundTask<Void>(context) {
@Override
protected Void doInBackground() throws Throwable {
@@ -540,7 +540,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis DownloadService downloadService = getDownloadService();
if (downloadService != null && context.getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
context.getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE);
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
downloadService.setShufflePlayEnabled(true);
}
@@ -866,7 +866,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis updateButtons();
if(currentPlaying == null && downloadService != null && currentPlaying == downloadService.getCurrentPlaying()) {
- getImageLoader().loadImage(albumArtImageView, null, true, false);
+ getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false);
}
if(downloadService != null) {
downloadService.startRemoteScan();
@@ -1024,6 +1024,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if (fromUser) {
int length = getMinutes(progress);
lengthBox.setText(Util.formatDuration(length));
+ seekBar.setProgress(progress);
}
}
@@ -1035,6 +1036,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis public void onStopTrackingTouch(SeekBar seekBar) {
}
});
+ lengthBar.setProgress(length - 1);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.menu_set_timer)
@@ -1089,7 +1091,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if (state == PAUSED || state == COMPLETED || state == STOPPED) {
service.start();
} else if (state == STOPPED || state == IDLE) {
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
int current = service.getCurrentPlayingIndex();
// TODO: Use play() method.
if (current == -1) {
@@ -1236,7 +1238,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis bookmarkButton.setImageResource(bookmark);
} else {
songTitleTextView.setText(null);
- getImageLoader().loadImage(albumArtImageView, null, true, false);
+ getImageLoader().loadImage(albumArtImageView, (Entry) null, true, false);
starButton.setImageResource(android.R.drawable.btn_star_big_off);
setSubtitle(null);
}
@@ -1260,18 +1262,18 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis onProgressChangedTask = new SilentBackgroundTask<Void>(context) {
DownloadService downloadService;
- boolean isJukeboxEnabled;
int millisPlayed;
Integer duration;
PlayerState playerState;
+ boolean isSeekable;
@Override
protected Void doInBackground() throws Throwable {
downloadService = getDownloadService();
- isJukeboxEnabled = downloadService.isRemoteEnabled();
millisPlayed = Math.max(0, downloadService.getPlayerPosition());
duration = downloadService.getPlayerDuration();
playerState = getDownloadService().getPlayerState();
+ isSeekable = downloadService.isSeekable();
return null;
}
@@ -1290,7 +1292,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if(!seekInProgress) {
progressBar.setProgress(millisPlayed);
}
- progressBar.setEnabled((currentPlaying.isWorkDone() || isJukeboxEnabled) && playerState != PlayerState.PREPARING);
+ progressBar.setEnabled(isSeekable);
} else {
positionTextView.setText("0:00");
durationTextView.setText("-:--");
@@ -1507,7 +1509,7 @@ public class NowPlayingFragment extends SubsonicFragment implements OnGestureLis if(action > 0) {
final int performAction = action;
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
new SilentBackgroundTask<Void>(context) {
@Override
protected Void doInBackground() throws Throwable {
diff --git a/src/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java b/src/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java new file mode 100644 index 00000000..9f413b3b --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java @@ -0,0 +1,313 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.fragments; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.Constants; + +public class PreferenceCompatFragment extends SubsonicFragment { + private static final int FIRST_REQUEST_CODE = 100; + private static final int MSG_BIND_PREFERENCES = 1; + private static final String PREFERENCES_TAG = "android:preferences"; + private boolean mHavePrefs; + private boolean mInitDone; + private ListView mList; + private PreferenceManager mPreferenceManager; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case MSG_BIND_PREFERENCES: + bindPreferences(); + break; + } + } + }; + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + private void bindPreferences() { + PreferenceScreen localPreferenceScreen = getPreferenceScreen(); + if (localPreferenceScreen != null) { + ListView localListView = getListView(); + localPreferenceScreen.bind(localListView); + } + } + + private void ensureList() { + if (mList == null) { + View view = getView(); + if (view == null) { + throw new IllegalStateException("Content view not yet created"); + } + + View listView = view.findViewById(android.R.id.list); + if (!(listView instanceof ListView)) { + throw new RuntimeException("Content has view with id attribute 'android.R.id.list' that is not a ListView class"); + } + + mList = (ListView)listView; + if (mList == null) { + throw new RuntimeException("Your content must have a ListView whose id attribute is 'android.R.id.list'"); + } + + mHandler.post(mRequestFocus); + } + } + + private void postBindPreferences() { + if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) { + mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); + } + } + + private void requirePreferenceManager() { + if (this.mPreferenceManager == null) { + throw new RuntimeException("This should be called after super.onCreate."); + } + } + + public void addPreferencesFromIntent(Intent intent) { + requirePreferenceManager(); + PreferenceScreen screen = inflateFromIntent(intent, getPreferenceScreen()); + setPreferenceScreen(screen); + } + + public void addPreferencesFromResource(int resId) { + requirePreferenceManager(); + PreferenceScreen screen = inflateFromResource(getActivity(), resId, getPreferenceScreen()); + setPreferenceScreen(screen); + } + + public Preference findPreference(CharSequence key) { + if (mPreferenceManager == null) { + return null; + } + return mPreferenceManager.findPreference(key); + } + + public ListView getListView() { + ensureList(); + return mList; + } + + public PreferenceManager getPreferenceManager() { + return mPreferenceManager; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + getListView().setScrollBarStyle(0); + if (mHavePrefs) { + bindPreferences(); + } + mInitDone = true; + if (savedInstanceState != null) { + Bundle localBundle = savedInstanceState.getBundle(PREFERENCES_TAG); + if (localBundle != null) { + PreferenceScreen screen = getPreferenceScreen(); + if (screen != null) { + screen.restoreHierarchyState(localBundle); + } + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + dispatchActivityResult(requestCode, resultCode, data); + } + + @Override + public void onCreate(Bundle paramBundle) { + super.onCreate(paramBundle); + mPreferenceManager = createPreferenceManager(); + + int res = this.getArguments().getInt(Constants.INTENT_EXTRA_FRAGMENT_TYPE, 0); + if(res != 0) { + addPreferencesFromResource(res); + } + } + + @Override + public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) { + return paramLayoutInflater.inflate(R.layout.preferences, paramViewGroup, false); + } + + @Override + public void onDestroy() { + super.onDestroy(); + dispatchActivityDestroy(); + } + + @Override + public void onDestroyView() { + mList = null; + mHandler.removeCallbacks(mRequestFocus); + mHandler.removeMessages(MSG_BIND_PREFERENCES); + super.onDestroyView(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + PreferenceScreen screen = getPreferenceScreen(); + if (screen != null) { + Bundle localBundle = new Bundle(); + screen.saveHierarchyState(localBundle); + bundle.putBundle(PREFERENCES_TAG, localBundle); + } + } + + @Override + public void onStop() { + super.onStop(); + dispatchActivityStop(); + } + + /** Access methods with visibility private **/ + + private PreferenceManager createPreferenceManager() { + try { + Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class); + c.setAccessible(true); + return c.newInstance(this.getActivity(), FIRST_REQUEST_CODE); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private PreferenceScreen getPreferenceScreen() { + try { + Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen"); + m.setAccessible(true); + return (PreferenceScreen) m.invoke(mPreferenceManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void setPreferenceScreen(PreferenceScreen preferenceScreen) { + try { + Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class); + m.setAccessible(true); + boolean result = (Boolean) m.invoke(mPreferenceManager, preferenceScreen); + if (result && preferenceScreen != null) { + mHavePrefs = true; + if (mInitDone) { + postBindPreferences(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void dispatchActivityResult(int requestCode, int resultCode, Intent data) { + try { + Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class); + m.setAccessible(true); + m.invoke(mPreferenceManager, requestCode, resultCode, data); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void dispatchActivityDestroy() { + try { + Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy"); + m.setAccessible(true); + m.invoke(mPreferenceManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void dispatchActivityStop() { + try { + Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop"); + m.setAccessible(true); + m.invoke(mPreferenceManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + private void setFragment(PreferenceFragment preferenceFragment) { + try { + Method m = PreferenceManager.class.getDeclaredMethod("setFragment", PreferenceFragment.class); + m.setAccessible(true); + m.invoke(mPreferenceManager, preferenceFragment); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public PreferenceScreen inflateFromResource(Context context, int resId, PreferenceScreen rootPreferences) { + PreferenceScreen preferenceScreen ; + try { + Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class); + m.setAccessible(true); + preferenceScreen = (PreferenceScreen) m.invoke(mPreferenceManager, context, resId, rootPreferences); + } catch (Exception e) { + throw new RuntimeException(e); + } + return preferenceScreen; + } + + public PreferenceScreen inflateFromIntent(Intent queryIntent, PreferenceScreen rootPreferences) { + PreferenceScreen preferenceScreen ; + try { + Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class); + m.setAccessible(true); + preferenceScreen = (PreferenceScreen) m.invoke(mPreferenceManager, queryIntent, rootPreferences); + } catch (Exception e) { + throw new RuntimeException(e); + } + return preferenceScreen; + } +} diff --git a/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java index a8b4ab38..805c0de0 100644 --- a/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java +++ b/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -1,14 +1,25 @@ package github.daneren2005.dsub.fragments;
+import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.graphics.Canvas;
+import android.graphics.Paint;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
+import android.text.Html;
+import android.text.Layout;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.method.LinkMovementMethod;
+import android.text.style.LeadingMarginSpan;
import android.util.Log;
import android.view.ContextMenu;
+import android.view.Display;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -22,8 +33,10 @@ import android.widget.ImageView; import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.RatingBar;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ArtistInfo;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.ServerInfo;
import github.daneren2005.dsub.domain.Share;
@@ -48,6 +61,9 @@ import github.daneren2005.dsub.util.TabBackgroundTask; import github.daneren2005.dsub.util.UserUtil;
import github.daneren2005.dsub.util.Util;
import github.daneren2005.dsub.view.AlbumListAdapter;
+import github.daneren2005.dsub.view.HeaderGridView;
+import github.daneren2005.dsub.view.MyLeadingMarginSpan2;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -60,15 +76,14 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private GridView albumList;
private ListView entryList;
- private boolean hideButtons = false;
private Boolean licenseValid;
- private boolean showHeader = true;
private EntryAdapter entryAdapter;
private List<Entry> albums;
private List<Entry> entries;
private boolean albumContext = false;
private boolean addAlbumHeader = false;
private LoadTask currentTask;
+ ArtistInfo artistInfo;
String id;
String name;
@@ -89,7 +104,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter boolean largeAlbums = false;
boolean topTracks = false;
String lookupEntry;
-
+
public SelectDirectoryFragment() {
super();
}
@@ -200,16 +215,16 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
if(licenseValid == null) {
menuInflater.inflate(R.menu.empty, menu);
- }
- else if(hideButtons && !showAll) {
- if(albumListType != null) {
- menuInflater.inflate(R.menu.empty, menu);
- } else {
- menuInflater.inflate(R.menu.select_album, menu);
+ } else if(albumListType != null) {
+ menuInflater.inflate(R.menu.select_album_list, menu);
+ } else if(artist && !showAll) {
+ menuInflater.inflate(R.menu.select_album, menu);
- if(!ServerInfo.isMadsonic(context)) {
- menu.removeItem(R.id.menu_top_tracks);
- }
+ if(!ServerInfo.isMadsonic(context)) {
+ menu.removeItem(R.id.menu_top_tracks);
+ }
+ if(!ServerInfo.checkServerVersion(context, "1.11")) {
+ menu.removeItem(R.id.menu_similar_artists);
}
} else {
if(podcastId == null) {
@@ -298,6 +313,9 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter case R.id.menu_top_tracks:
showTopTracks();
return true;
+ case R.id.menu_similar_artists:
+ showSimilarArtists(id);
+ return true;
}
return super.onOptionsItemSelected(item);
@@ -316,12 +334,20 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter return;
}
entry = (Entry) entryList.getItemAtPosition(info.position);
- albumContext = false;
+ // When List has Grid embedded in header, this is called against the header as well
+ if(entry != null) {
+ albumContext = false;
+ }
} else {
entry = (Entry) albumList.getItemAtPosition(info.position);
albumContext = true;
}
+ // Don't try to display a context menu if error here
+ if(entry == null) {
+ return;
+ }
+
onCreateContextMenu(menu, view, menuInfo, entry);
if(!entry.isVideo() && !Util.isOffline(context) && (playlistId == null || !playlistOwner) && (podcastId == null || Util.isOffline(context) && podcastId != null)) {
menu.removeItem(R.id.song_menu_remove_playlist);
@@ -355,7 +381,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter Object selectedItem;
int headers = entryList.getHeaderViewsCount();
if(albumContext) {
- selectedItem = albums.get(info.position);
+ selectedItem = albumList.getItemAtPosition(info.position);
} else {
if(info.position == 0) {
return false;
@@ -472,7 +498,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private void getMusicDirectory(final String id, final String name, final boolean refresh) {
setTitle(name);
- new LoadTask() {
+ new LoadTask(refresh) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
MusicDirectory dir = getMusicDirectory(id, name, refresh, service, this);
@@ -509,7 +535,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private void getRecursiveMusicDirectory(final String id, final String name, final boolean refresh) {
setTitle(name);
- new LoadTask() {
+ new LoadTask(refresh) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
MusicDirectory root;
@@ -551,7 +577,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private void getPlaylist(final String playlistId, final String playlistName, final boolean refresh) {
setTitle(playlistName);
- new LoadTask() {
+ new LoadTask(refresh) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
return service.getPlaylist(refresh, playlistId, playlistName, context, this);
@@ -562,7 +588,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private void getPodcast(final String podcastId, final String podcastName, final boolean refresh) {
setTitle(podcastName);
- new LoadTask() {
+ new LoadTask(refresh) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
return service.getPodcastEpisodes(refresh, podcastId, context, this);
@@ -573,7 +599,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private void getShare(final Share share, final boolean refresh) {
setTitle(share.getName());
- new LoadTask() {
+ new LoadTask(refresh) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
return share.getMusicDirectory();
@@ -584,7 +610,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter private void getTopTracks(final String id, final String name, final boolean refresh) {
setTitle(name);
- new LoadTask() {
+ new LoadTask(refresh) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
return service.getTopTrackSongs(name, 20, context, this);
@@ -593,8 +619,6 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter }
private void getAlbumList(final String albumListType, final int size) {
- showHeader = false;
-
if ("newest".equals(albumListType)) {
setTitle(R.string.main_albums_newest);
} else if ("random".equals(albumListType)) {
@@ -611,7 +635,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter setTitle(albumListExtra);
}
- new LoadTask() {
+ new LoadTask(true) {
@Override
protected MusicDirectory load(MusicService service) throws Exception {
MusicDirectory result;
@@ -634,9 +658,11 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter }
private abstract class LoadTask extends TabBackgroundTask<Pair<MusicDirectory, Boolean>> {
+ private boolean refresh;
- public LoadTask() {
+ public LoadTask(boolean refresh) {
super(SelectDirectoryFragment.this);
+ this.refresh = refresh;
currentTask = this;
}
@@ -655,6 +681,16 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter } else {
entries = dir.getChildren();
}
+
+ // This isn't really an artist if no albums on it!
+ if(albums.size() == 0) {
+ artist = false;
+ }
+
+ // If artist, we want to load the artist info to use later
+ if(artist && ServerInfo.checkServerVersion(context, "1.11")) {
+ artistInfo = musicService.getArtistInfo(id, refresh, context, this);
+ }
return new Pair<MusicDirectory, Boolean>(dir, licenseValid);
}
@@ -667,19 +703,17 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter }
private void finishLoading() {
- if (entries.size() > 0 && albums.size() == 0 && !"root".equals(id)) {
- if(showHeader) {
- View header = createHeader(entries);
- if(header != null && entryList != null) {
- entryList.addHeaderView(header, null, false);
- }
- }
- } else {
- showHeader = false;
- if(!"root".equals(id) && (entries.size() == 0 || !largeAlbums && albums.size() == entries.size())) {
- hideButtons = true;
+ // Show header if not album list type and not root and not artist
+ // For Subsonic 5.1+ display a header for artists with getArtistInfo data if it exists
+ View header = null;
+ if(albumListType == null && !"root".equals(id) && (!artist || artistInfo != null)) {
+ header = createHeader();
+ // Only add header to entry list if we aren't going recreate album grid as root anyways
+ if(header != null && entryList != null && (!addAlbumHeader || entries.size() > 0)) {
+ entryList.addHeaderView(header, null, false);
+ header = null;
}
- }
+ }
// Needs to be added here, GB crashes if you to try to remove the header view before adapter is set
if(addAlbumHeader) {
@@ -693,6 +727,12 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter setupScrollList(albumList);
setupAlbumList();
+
+ // This should only not be null for a artist with only albums
+ if(header != null) {
+ HeaderGridView headerGridView = (HeaderGridView) albumList;
+ headerGridView.addHeaderView(header);
+ }
}
addAlbumHeader = false;
}
@@ -802,6 +842,8 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter if (hasSubFolders && (id != null || share != null || "starred".equals(albumListType))) {
downloadRecursively(id, false, append, !append, shuffle, false);
+ } else if(hasSubFolders && albumListType != null) {
+ downloadRecursively(albums, shuffle, append);
} else {
selectAll(true, false);
download(append, false, !append, false, shuffle);
@@ -863,7 +905,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter }
final List<Entry> songs = getSelectedSongs();
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
// Conditions for using play now button
if(!append && !save && autoplay && !playNext && !shuffle) {
@@ -905,6 +947,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter checkLicenseAndTrialPeriod(onValid);
}
private void downloadBackground(final boolean save) {
+ if(playlistId != null) {
+ selectAll(true, false);
+ }
+
List<Entry> songs = getSelectedSongs();
if(songs.isEmpty()) {
// Get both songs and albums
@@ -918,7 +964,7 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter return;
}
- warnIfNetworkOrStorageUnavailable();
+ warnIfStorageUnavailable();
LoadingTask<Void> onValid = new LoadingTask<Void>(context) {
@Override
protected Void doInBackground() throws Throwable {
@@ -1205,7 +1251,16 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter replaceFragment(fragment, true);
}
- private View createHeader(List<Entry> entries) {
+ private void showSimilarArtists(String artistId) {
+ SubsonicFragment fragment = new SimilarArtistFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ARTIST, artistId);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, true);
+ }
+
+ private View createHeader() {
View header = entryList.findViewById(R.id.select_album_header);
boolean add = false;
if(header == null) {
@@ -1213,37 +1268,76 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter add = true;
}
- final ImageLoader imageLoader = getImageLoader();
-
- // Try a few times to get a random cover art
- Entry coverArt = null;
- for(int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) {
- coverArt = entries.get(random.nextInt(entries.size()));
+ setupCoverArt(header);
+ setupTextDisplay(header);
+
+ if(add) {
+ setupButtonEvents(header);
}
-
- final Entry albumRep = coverArt;
+
+ if(add) {
+ return header;
+ } else {
+ return null;
+ }
+ }
+
+ private void setupCoverArt(View header) {
+ final ImageLoader imageLoader = getImageLoader();
View coverArtView = header.findViewById(R.id.select_album_art);
- coverArtView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if(albumRep.getCoverArt() == null) {
- return;
- }
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- ImageView fullScreenView = new ImageView(context);
- imageLoader.loadImage(fullScreenView, albumRep, true, true);
- builder.setCancelable(true);
-
- AlertDialog imageDialog = builder.create();
- // Set view here with unecessary 0's to remove top/bottom border
- imageDialog.setView(fullScreenView, 0, 0, 0, 0);
- imageDialog.show();
+ // Try a few times to get a random cover art
+ if(artistInfo != null) {
+ final String url = artistInfo.getImageUrl();
+ coverArtView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (url == null) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ ImageView fullScreenView = new ImageView(context);
+ imageLoader.loadImage(fullScreenView, url, true);
+ builder.setCancelable(true);
+
+ AlertDialog imageDialog = builder.create();
+ // Set view here with unecessary 0's to remove top/bottom border
+ imageDialog.setView(fullScreenView, 0, 0, 0, 0);
+ imageDialog.show();
+ }
+ });
+ imageLoader.loadImage(coverArtView, url, false);
+ } else if(entries.size() > 0) {
+ Entry coverArt = null;
+ for (int i = 0; (i < 3) && (coverArt == null || coverArt.getCoverArt() == null); i++) {
+ coverArt = entries.get(random.nextInt(entries.size()));
}
- });
- imageLoader.loadImage(coverArtView, albumRep, false, true);
- TextView titleView = (TextView) header.findViewById(R.id.select_album_title);
+ final Entry albumRep = coverArt;
+ coverArtView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (albumRep.getCoverArt() == null) {
+ return;
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ ImageView fullScreenView = new ImageView(context);
+ imageLoader.loadImage(fullScreenView, albumRep, true, true);
+ builder.setCancelable(true);
+
+ AlertDialog imageDialog = builder.create();
+ // Set view here with unecessary 0's to remove top/bottom border
+ imageDialog.setView(fullScreenView, 0, 0, 0, 0);
+ imageDialog.show();
+ }
+ });
+ imageLoader.loadImage(coverArtView, albumRep, false, true);
+ }
+ }
+ private void setupTextDisplay(final View header) {
+ final TextView titleView = (TextView) header.findViewById(R.id.select_album_title);
if(playlistName != null) {
titleView.setText(playlistName);
} else if(podcastName != null) {
@@ -1251,6 +1345,10 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter titleView.setPadding(0, 6, 4, 8);
} else if(name != null) {
titleView.setText(name);
+
+ if(artistInfo != null) {
+ titleView.setPadding(0, 6, 4, 8);
+ }
} else if(share != null) {
titleView.setVisibility(View.GONE);
}
@@ -1275,29 +1373,55 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter }
}
}
- if(songCount == 0) {
- showHeader = false;
- hideButtons = true;
- return null;
- }
final TextView artistView = (TextView) header.findViewById(R.id.select_album_artist);
- if(podcastDescription != null) {
- artistView.setText(podcastDescription);
+ if(podcastDescription != null || artistInfo != null) {
+ String text = podcastDescription != null ? podcastDescription : artistInfo.getBiography();
+ Spanned spanned = null;
+ if(text != null) {
+ spanned = Html.fromHtml(text);
+ }
+ artistView.setText(spanned);
artistView.setSingleLine(false);
artistView.setLines(5);
artistView.setTextAppearance(context, android.R.style.TextAppearance_Small);
+ final Spanned spannedText = spanned;
artistView.setOnClickListener(new View.OnClickListener() {
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onClick(View v) {
if(artistView.getMaxLines() == 5) {
+ // Use LeadingMarginSpan2 to try to make text flow around image
+ Display display = context.getWindowManager().getDefaultDisplay();
+ View coverArtView = header.findViewById(R.id.select_album_art);
+ coverArtView.measure(display.getWidth(), display.getHeight());
+ ViewGroup.MarginLayoutParams vlp = (ViewGroup.MarginLayoutParams) coverArtView.getLayoutParams();
+ int height = coverArtView.getMeasuredHeight() + coverArtView.getPaddingBottom();
+ int width = coverArtView.getWidth() + coverArtView.getPaddingRight();
+ float textLineHeight = artistView.getPaint().getTextSize();
+ int lines = (int) Math.ceil(height / textLineHeight);
+
+ SpannableString ss = new SpannableString(spannedText);
+ ss.setSpan(new MyLeadingMarginSpan2(lines, width), 0, ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ View linearLayout = header.findViewById(R.id.select_album_text_layout);
+ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams();
+ int[]rules = params.getRules();
+ rules[RelativeLayout.RIGHT_OF] = 0;
+ params.leftMargin = vlp.rightMargin;
+
+ artistView.setText(ss);
artistView.setMaxLines(100);
+
+ vlp = (ViewGroup.MarginLayoutParams) titleView.getLayoutParams();
+ vlp.leftMargin = width;
} else {
artistView.setMaxLines(5);
}
}
});
+ artistView.setMovementMethod(LinkMovementMethod.getInstance());
} else if(topTracks) {
artistView.setText(R.string.menu_top_tracks);
artistView.setVisibility(View.VISIBLE);
@@ -1317,70 +1441,63 @@ public class SelectDirectoryFragment extends SubsonicFragment implements Adapter TextView songCountView = (TextView) header.findViewById(R.id.select_album_song_count);
TextView songLengthView = (TextView) header.findViewById(R.id.select_album_song_length);
- if(podcastDescription == null) {
+ if(podcastDescription != null || artistInfo != null) {
+ songCountView.setVisibility(View.GONE);
+ songLengthView.setVisibility(View.GONE);
+ } else {
String s = context.getResources().getQuantityString(R.plurals.select_album_n_songs, songCount, songCount);
songCountView.setText(s.toUpperCase());
songLengthView.setText(Util.formatDuration(totalDuration));
+ }
+ }
+ private void setupButtonEvents(View header) {
+ ImageView shareButton = (ImageView) header.findViewById(R.id.select_album_share);
+ if(share != null || podcastId != null || !Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_SHARED, true) || Util.isOffline(context) || !UserUtil.canShare()) {
+ shareButton.setVisibility(View.GONE);
} else {
- songCountView.setVisibility(View.GONE);
- songLengthView.setVisibility(View.GONE);
+ shareButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ createShare(SelectDirectoryFragment.this.entries);
+ }
+ });
}
- if(add) {
- ImageView shareButton = (ImageView) header.findViewById(R.id.select_album_share);
- if(share != null || podcastId != null || !Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_SHARED, true) || Util.isOffline(context) || !UserUtil.canShare()) {
- shareButton.setVisibility(View.GONE);
- } else {
- shareButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- createShare(SelectDirectoryFragment.this.entries);
- }
- });
- }
-
- final ImageButton starButton = (ImageButton) header.findViewById(R.id.select_album_star);
- if(directory != null && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_STAR, true)) {
- starButton.setImageResource(directory.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
- starButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- toggleStarred(directory, new OnStarChange() {
- @Override
- void starChange(boolean starred) {
- starButton.setImageResource(directory.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
- }
- });
- }
- });
- } else {
- starButton.setVisibility(View.GONE);
- }
-
- View ratingBarWrapper = header.findViewById(R.id.select_album_rate_wrapper);
- final RatingBar ratingBar = (RatingBar) header.findViewById(R.id.select_album_rate);
- if(directory != null && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_RATING, true) && !Util.isOffline(context)) {
- ratingBar.setRating(directory.getRating());
- ratingBarWrapper.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- setRating(directory, new OnRatingChange() {
- @Override
- void ratingChange(int rating) {
- ratingBar.setRating(directory.getRating());
- }
- });
- }
- });
- } else {
- ratingBar.setVisibility(View.GONE);
- }
+ final ImageButton starButton = (ImageButton) header.findViewById(R.id.select_album_star);
+ if(directory != null && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_STAR, true)) {
+ starButton.setImageResource(directory.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ starButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggleStarred(directory, new OnStarChange() {
+ @Override
+ void starChange(boolean starred) {
+ starButton.setImageResource(directory.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ }
+ });
+ }
+ });
+ } else {
+ starButton.setVisibility(View.GONE);
}
- if(add) {
- return header;
+ View ratingBarWrapper = header.findViewById(R.id.select_album_rate_wrapper);
+ final RatingBar ratingBar = (RatingBar) header.findViewById(R.id.select_album_rate);
+ if(directory != null && Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_MENU_RATING, true) && !Util.isOffline(context)) {
+ ratingBar.setRating(directory.getRating());
+ ratingBarWrapper.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setRating(directory, new OnRatingChange() {
+ @Override
+ void ratingChange(int rating) {
+ ratingBar.setRating(directory.getRating());
+ }
+ });
+ }
+ });
} else {
- return null;
+ ratingBar.setVisibility(View.GONE);
}
}
}
diff --git a/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java index caf9079f..8c16edd5 100644 --- a/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java +++ b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java @@ -156,7 +156,7 @@ public class SelectPlaylistFragment extends SelectListFragment<Playlist> { Bundle args = new Bundle();
args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
- if(ServerInfo.checkServerVersion(context, "1.8") && playlist.getOwner() != null && playlist.getOwner().equals(UserUtil.getCurrentUsername(context))) {
+ if(ServerInfo.checkServerVersion(context, "1.8") && (playlist.getOwner() != null && playlist.getOwner().equals(UserUtil.getCurrentUsername(context)) || playlist.getId().indexOf(".m3u") != -1)) {
args.putBoolean(Constants.INTENT_EXTRA_NAME_PLAYLIST_OWNER, true);
}
fragment.setArguments(args);
diff --git a/src/github/daneren2005/dsub/fragments/SettingsFragment.java b/src/github/daneren2005/dsub/fragments/SettingsFragment.java new file mode 100644 index 00000000..8dfca3b7 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SettingsFragment.java @@ -0,0 +1,711 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.fragments; + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.text.DecimalFormat; +import java.util.LinkedHashMap; +import java.util.Map; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.LoadingTask; +import github.daneren2005.dsub.util.SyncUtil; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.ErrorDialog; + +public class SettingsFragment extends PreferenceCompatFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + private final static String TAG = SettingsFragment.class.getSimpleName(); + private final Map<String, ServerSettings> serverSettings = new LinkedHashMap<String, ServerSettings>(); + private boolean testingConnection; + private ListPreference theme; + private ListPreference maxBitrateWifi; + private ListPreference maxBitrateMobile; + private ListPreference maxVideoBitrateWifi; + private ListPreference maxVideoBitrateMobile; + private ListPreference networkTimeout; + private EditTextPreference cacheLocation; + private ListPreference preloadCountWifi; + private ListPreference preloadCountMobile; + private ListPreference tempLoss; + private ListPreference pauseDisconnect; + private Preference addServerPreference; + private PreferenceCategory serversCategory; + private ListPreference videoPlayer; + private ListPreference syncInterval; + private CheckBoxPreference syncEnabled; + private CheckBoxPreference syncWifi; + private CheckBoxPreference syncNotification; + private CheckBoxPreference syncStarred; + private CheckBoxPreference syncMostRecent; + private CheckBoxPreference replayGain; + private ListPreference replayGainType; + private Preference replayGainBump; + private Preference replayGainUntagged; + private String internalSSID; + private String internalSSIDDisplay; + private EditTextPreference cacheSize; + + private int serverCount = 3; + private SharedPreferences settings; + private DecimalFormat megabyteFromat; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + View root = super.onCreateView(inflater, container, bundle); + + this.setTitle(getResources().getString(R.string.settings_title)); + initSettings(); + + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + SharedPreferences prefs = Util.getPreferences(context); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + // Random error I have no idea how to reproduce + if(sharedPreferences == null) { + return; + } + + update(); + + if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) { + setHideMedia(sharedPreferences.getBoolean(key, false)); + } + else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) { + setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true)); + } + else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) { + setCacheLocation(sharedPreferences.getString(key, "")); + } + else if (Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION.equals(key)){ + DownloadService downloadService = DownloadService.getInstance(); + downloadService.setSleepTimerDuration(Integer.parseInt(sharedPreferences.getString(key, "60"))); + } + else if(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT.equals(key)) { + SyncUtil.removeMostRecentSyncFiles(context); + } else if(Constants.PREFERENCES_KEY_REPLAY_GAIN.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP.equals(key) || Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED.equals(key)) { + DownloadService downloadService = DownloadService.getInstance(); + if(downloadService != null) { + downloadService.reapplyVolume(); + } + } + + scheduleBackup(); + } + + private void initSettings() { + internalSSID = Util.getSSID(context); + if(internalSSID == null) { + internalSSID = ""; + } + internalSSIDDisplay = context.getResources().getString(R.string.settings_server_local_network_ssid_hint, internalSSID); + + theme = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_THEME); + maxBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI); + maxBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE); + maxVideoBitrateWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI); + maxVideoBitrateMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE); + networkTimeout = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT); + cacheLocation = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION); + preloadCountWifi = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI); + preloadCountMobile = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE); + tempLoss = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_TEMP_LOSS); + pauseDisconnect = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_PAUSE_DISCONNECT); + serversCategory = (PreferenceCategory) this.findPreference(Constants.PREFERENCES_KEY_SERVER_KEY); + addServerPreference = this.findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); + videoPlayer = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER); + syncInterval = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_INTERVAL); + syncEnabled = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED); + syncWifi = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_WIFI); + syncNotification = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION); + syncStarred = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_STARRED); + syncMostRecent = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_SYNC_MOST_RECENT); + replayGain = (CheckBoxPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN); + replayGainType = (ListPreference) this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_TYPE); + replayGainBump = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_BUMP); + replayGainUntagged = this.findPreference(Constants.PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED); + cacheSize = (EditTextPreference) this.findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE); + + settings = Util.getPreferences(context); + serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); + + this.findPreference("clearCache").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Util.confirmDialog(context, R.string.common_delete, R.string.common_confirm_message_cache, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new LoadingTask<Void>(context, false) { + @Override + protected Void doInBackground() throws Throwable { + FileUtil.deleteMusicDirectory(context); + FileUtil.deleteSerializedCache(context); + FileUtil.deleteArtworkCache(context); + FileUtil.deleteAvatarCache(context); + return null; + } + + @Override + protected void done(Void result) { + Util.toast(context, R.string.settings_cache_clear_complete); + } + + @Override + protected void error(Throwable error) { + Util.toast(context, getErrorMessage(error), false); + } + }.execute(); + } + }); + return false; + } + }); + + addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + serverCount++; + String instance = String.valueOf(serverCount); + serversCategory.addPreference(addServer(serverCount)); + + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); + // Reset set folder ID + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); + editor.commit(); + + serverSettings.put(instance, new ServerSettings(instance)); + + return true; + } + }); + + this.findPreference(Constants.PREFERENCES_KEY_SYNC_ENABLED).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Boolean syncEnabled = (Boolean) newValue; + + Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); + ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, syncEnabled); + ContentResolver.setSyncAutomatically(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, syncEnabled); + + return true; + } + }); + syncInterval.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Integer syncInterval = Integer.parseInt(((String) newValue)); + + Account account = new Account(Constants.SYNC_ACCOUNT_NAME, Constants.SYNC_ACCOUNT_TYPE); + ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PLAYLIST_AUTHORITY, new Bundle(), 60L * syncInterval); + ContentResolver.addPeriodicSync(account, Constants.SYNC_ACCOUNT_PODCAST_AUTHORITY, new Bundle(), 60L * syncInterval); + + return true; + } + }); + + serversCategory.setOrderingAsAdded(false); + for (int i = 1; i <= serverCount; i++) { + String instance = String.valueOf(i); + serversCategory.addPreference(addServer(i)); + serverSettings.put(instance, new ServerSettings(instance)); + } + + SharedPreferences prefs = Util.getPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + + update(); + } + + private void scheduleBackup() { + try { + Class managerClass = Class.forName("android.app.backup.BackupManager"); + Constructor managerConstructor = managerClass.getConstructor(Context.class); + Object manager = managerConstructor.newInstance(context); + Method m = managerClass.getMethod("dataChanged"); + m.invoke(manager); + } catch(ClassNotFoundException e) { + Log.e(TAG, "No backup manager found"); + } catch(Throwable t) { + Log.e(TAG, "Scheduling backup failed " + t); + t.printStackTrace(); + } + } + + private void update() { + if (testingConnection) { + return; + } + + theme.setSummary(theme.getEntry()); + maxBitrateWifi.setSummary(maxBitrateWifi.getEntry()); + maxBitrateMobile.setSummary(maxBitrateMobile.getEntry()); + maxVideoBitrateWifi.setSummary(maxVideoBitrateWifi.getEntry()); + maxVideoBitrateMobile.setSummary(maxVideoBitrateMobile.getEntry()); + networkTimeout.setSummary(networkTimeout.getEntry()); + cacheLocation.setSummary(cacheLocation.getText()); + preloadCountWifi.setSummary(preloadCountWifi.getEntry()); + preloadCountMobile.setSummary(preloadCountMobile.getEntry()); + tempLoss.setSummary(tempLoss.getEntry()); + pauseDisconnect.setSummary(pauseDisconnect.getEntry()); + videoPlayer.setSummary(videoPlayer.getEntry()); + syncInterval.setSummary(syncInterval.getEntry()); + try { + if(megabyteFromat == null) { + megabyteFromat = new DecimalFormat(getResources().getString(R.string.util_bytes_format_megabyte)); + } + + cacheSize.setSummary(megabyteFromat.format((double) Integer.parseInt(cacheSize.getText())).replace(".00", "")); + } catch(Exception e) { + Log.e(TAG, "Failed to format cache size", e); + cacheSize.setSummary(cacheSize.getText()); + } + if(syncEnabled.isChecked()) { + if(!syncInterval.isEnabled()) { + syncInterval.setEnabled(true); + syncWifi.setEnabled(true); + syncNotification.setEnabled(true); + syncStarred.setEnabled(true); + syncMostRecent.setEnabled(true); + } + } else { + if(syncInterval.isEnabled()) { + syncInterval.setEnabled(false); + syncWifi.setEnabled(false); + syncNotification.setEnabled(false); + syncStarred.setEnabled(false); + syncMostRecent.setEnabled(false); + } + } + if(replayGain.isChecked()) { + replayGainType.setEnabled(true); + replayGainBump.setEnabled(true); + replayGainUntagged.setEnabled(true); + } else { + replayGainType.setEnabled(false); + replayGainBump.setEnabled(false); + replayGainUntagged.setEnabled(false); + } + replayGainType.setSummary(replayGainType.getEntry()); + + for (ServerSettings ss : serverSettings.values()) { + ss.update(); + } + } + + private PreferenceScreen addServer(final int instance) { + final PreferenceScreen screen = this.getPreferenceManager().createPreferenceScreen(context); + screen.setTitle(R.string.settings_server_unused); + screen.setKey(Constants.PREFERENCES_KEY_SERVER_KEY + instance); + + final EditTextPreference serverNamePreference = new EditTextPreference(context); + serverNamePreference.setKey(Constants.PREFERENCES_KEY_SERVER_NAME + instance); + serverNamePreference.setDefaultValue(getResources().getString(R.string.settings_server_unused)); + serverNamePreference.setTitle(R.string.settings_server_name); + serverNamePreference.setDialogTitle(R.string.settings_server_name); + + if (serverNamePreference.getText() == null) { + serverNamePreference.setText(getResources().getString(R.string.settings_server_unused)); + } + + serverNamePreference.setSummary(serverNamePreference.getText()); + + final EditTextPreference serverUrlPreference = new EditTextPreference(context); + serverUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_URL + instance); + serverUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); + serverUrlPreference.setDefaultValue("http://yourhost"); + serverUrlPreference.setTitle(R.string.settings_server_address); + serverUrlPreference.setDialogTitle(R.string.settings_server_address); + + if (serverUrlPreference.getText() == null) { + serverUrlPreference.setText("http://yourhost"); + } + + serverUrlPreference.setSummary(serverUrlPreference.getText()); + screen.setSummary(serverUrlPreference.getText()); + + final EditTextPreference serverLocalNetworkSSIDPreference = new EditTextPreference(context) { + @Override + protected void onAddEditTextToDialogView(View dialogView, final EditText editText) { + super.onAddEditTextToDialogView(dialogView, editText); + ViewGroup root = (ViewGroup) ((ViewGroup) dialogView).getChildAt(0); + + Button defaultButton = new Button(getContext()); + defaultButton.setText(internalSSIDDisplay); + defaultButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + editText.setText(internalSSID); + } + }); + root.addView(defaultButton); + } + }; + serverLocalNetworkSSIDPreference.setKey(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance); + serverLocalNetworkSSIDPreference.setTitle(R.string.settings_server_local_network_ssid); + serverLocalNetworkSSIDPreference.setDialogTitle(R.string.settings_server_local_network_ssid); + + final EditTextPreference serverInternalUrlPreference = new EditTextPreference(context); + serverInternalUrlPreference.setKey(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance); + serverInternalUrlPreference.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_URI); + serverInternalUrlPreference.setDefaultValue(""); + serverInternalUrlPreference.setTitle(R.string.settings_server_internal_address); + serverInternalUrlPreference.setDialogTitle(R.string.settings_server_internal_address); + serverInternalUrlPreference.setSummary(serverInternalUrlPreference.getText()); + + final EditTextPreference serverUsernamePreference = new EditTextPreference(context); + serverUsernamePreference.setKey(Constants.PREFERENCES_KEY_USERNAME + instance); + serverUsernamePreference.setTitle(R.string.settings_server_username); + serverUsernamePreference.setDialogTitle(R.string.settings_server_username); + + final EditTextPreference serverPasswordPreference = new EditTextPreference(context); + serverPasswordPreference.setKey(Constants.PREFERENCES_KEY_PASSWORD + instance); + serverPasswordPreference.getEditText().setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + serverPasswordPreference.setSummary("***"); + serverPasswordPreference.setTitle(R.string.settings_server_password); + + final CheckBoxPreference serverTagPreference = new CheckBoxPreference(context); + serverTagPreference.setKey(Constants.PREFERENCES_KEY_BROWSE_TAGS + instance); + serverTagPreference.setChecked(Util.isTagBrowsing(context, instance)); + serverTagPreference.setSummary(R.string.settings_browse_by_tags_summary); + serverTagPreference.setTitle(R.string.settings_browse_by_tags); + serverPasswordPreference.setDialogTitle(R.string.settings_server_password); + + final CheckBoxPreference serverSyncPreference = new CheckBoxPreference(context); + serverSyncPreference.setKey(Constants.PREFERENCES_KEY_SERVER_SYNC + instance); + serverSyncPreference.setChecked(Util.isSyncEnabled(context, instance)); + serverSyncPreference.setSummary(R.string.settings_server_sync_summary); + serverSyncPreference.setTitle(R.string.settings_server_sync); + + final Preference serverOpenBrowser = new Preference(context); + serverOpenBrowser.setKey(Constants.PREFERENCES_KEY_OPEN_BROWSER); + serverOpenBrowser.setPersistent(false); + serverOpenBrowser.setTitle(R.string.settings_server_open_browser); + serverOpenBrowser.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + openInBrowser(instance); + return true; + } + }); + + Preference serverRemoveServerPreference = new Preference(context); + serverRemoveServerPreference.setKey(Constants.PREFERENCES_KEY_SERVER_REMOVE + instance); + serverRemoveServerPreference.setPersistent(false); + serverRemoveServerPreference.setTitle(R.string.settings_servers_remove); + + serverRemoveServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Util.confirmDialog(context, R.string.common_delete, screen.getTitle().toString(), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Reset values to null so when we ask for them again they are new + serverNamePreference.setText(null); + serverUrlPreference.setText(null); + serverUsernamePreference.setText(null); + serverPasswordPreference.setText(null); + + int activeServer = Util.getActiveServer(context); + for (int i = instance; i <= serverCount; i++) { + Util.removeInstanceName(context, i, activeServer); + } + + serverCount--; + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); + editor.commit(); + + serversCategory.removePreference(screen); + screen.getDialog().dismiss(); + } + }); + + return true; + } + }); + + Preference serverTestConnectionPreference = new Preference(context); + serverTestConnectionPreference.setKey(Constants.PREFERENCES_KEY_TEST_CONNECTION + instance); + serverTestConnectionPreference.setPersistent(false); + serverTestConnectionPreference.setTitle(R.string.settings_test_connection_title); + serverTestConnectionPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + testConnection(instance); + return false; + } + }); + + screen.addPreference(serverNamePreference); + screen.addPreference(serverUrlPreference); + screen.addPreference(serverInternalUrlPreference); + screen.addPreference(serverLocalNetworkSSIDPreference); + screen.addPreference(serverUsernamePreference); + screen.addPreference(serverPasswordPreference); + screen.addPreference(serverTagPreference); + screen.addPreference(serverSyncPreference); + screen.addPreference(serverTestConnectionPreference); + screen.addPreference(serverOpenBrowser); + screen.addPreference(serverRemoveServerPreference); + + screen.setOrder(instance); + + return screen; + } + + private void setHideMedia(boolean hide) { + File nomediaDir = new File(FileUtil.getSubsonicDirectory(context), ".nomedia"); + File musicNoMedia = new File(FileUtil.getMusicDirectory(context), ".nomedia"); + if (hide && !nomediaDir.exists()) { + try { + if (!nomediaDir.createNewFile()) { + Log.w(TAG, "Failed to create " + nomediaDir); + } + } catch(Exception e) { + Log.w(TAG, "Failed to create " + nomediaDir, e); + } + + try { + if(!musicNoMedia.createNewFile()) { + Log.w(TAG, "Failed to create " + musicNoMedia); + } + } catch(Exception e) { + Log.w(TAG, "Failed to create " + musicNoMedia, e); + } + } else if (nomediaDir.exists()) { + if (!nomediaDir.delete()) { + Log.w(TAG, "Failed to delete " + nomediaDir); + } + if(!musicNoMedia.delete()) { + Log.w(TAG, "Failed to delete " + musicNoMedia); + } + } + Util.toast(context, R.string.settings_hide_media_toast, false); + } + + private void setMediaButtonsEnabled(boolean enabled) { + if (enabled) { + Util.registerMediaButtonEventReceiver(context); + } else { + Util.unregisterMediaButtonEventReceiver(context); + } + } + + private void setCacheLocation(String path) { + File dir = new File(path); + if (!FileUtil.verifyCanWrite(dir)) { + Util.toast(context, R.string.settings_cache_location_error, false); + + // Reset it to the default. + String defaultPath = FileUtil.getDefaultMusicDirectory(context).getPath(); + if (!defaultPath.equals(path)) { + SharedPreferences prefs = Util.getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); + editor.commit(); + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } + + // Clear download queue. + DownloadService downloadService = DownloadService.getInstance(); + downloadService.clear(); + } + } + + private void testConnection(final int instance) { + LoadingTask<Boolean> task = new LoadingTask<Boolean>(context) { + private int previousInstance; + + @Override + protected Boolean doInBackground() throws Throwable { + updateProgress(R.string.settings_testing_connection); + + previousInstance = Util.getActiveServer(context); + testingConnection = true; + MusicService musicService = MusicServiceFactory.getMusicService(context); + try { + musicService.setInstance(instance); + musicService.ping(context, this); + return musicService.isLicenseValid(context, null); + } finally { + musicService.setInstance(null); + testingConnection = false; + } + } + + @Override + protected void done(Boolean licenseValid) { + if (licenseValid) { + Util.toast(context, R.string.settings_testing_ok); + } else { + Util.toast(context, R.string.settings_testing_unlicensed); + } + } + + @Override + public void cancel() { + super.cancel(); + Util.setActiveServer(context, previousInstance); + } + + @Override + protected void error(Throwable error) { + Log.w(TAG, error.toString(), error); + new ErrorDialog(context, getResources().getString(R.string.settings_connection_failure) + + " " + getErrorMessage(error), false); + } + }; + task.execute(); + } + + private void openInBrowser(final int instance) { + SharedPreferences prefs = Util.getPreferences(context); + String url = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + if(url == null) { + new ErrorDialog(context, R.string.settings_invalid_url, false); + return; + } + Uri uriServer = Uri.parse(url); + + Intent browserIntent = new Intent(Intent.ACTION_VIEW, uriServer); + startActivity(browserIntent); + } + + private class ServerSettings { + private EditTextPreference serverName; + private EditTextPreference serverUrl; + private EditTextPreference serverLocalNetworkSSID; + private EditTextPreference serverInternalUrl; + private EditTextPreference username; + private PreferenceScreen screen; + + private ServerSettings(String instance) { + screen = (PreferenceScreen) SettingsFragment.this.findPreference("server" + instance); + serverName = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance); + serverUrl = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance); + serverLocalNetworkSSID = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance); + serverInternalUrl = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance); + username = (EditTextPreference) SettingsFragment.this.findPreference(Constants.PREFERENCES_KEY_USERNAME + instance); + + serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + try { + String url = (String) value; + new URL(url); + if (url.contains(" ") || url.contains("@") || url.contains("_")) { + throw new Exception(); + } + } catch (Exception x) { + new ErrorDialog(context, R.string.settings_invalid_url, false); + return false; + } + return true; + } + }); + serverInternalUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + try { + String url = (String) value; + // Allow blank internal IP address + if("".equals(url) || url == null) { + return true; + } + + new URL(url); + if (url.contains(" ") || url.contains("@") || url.contains("_")) { + throw new Exception(); + } + } catch (Exception x) { + new ErrorDialog(context, R.string.settings_invalid_url, false); + return false; + } + return true; + } + }); + + username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String username = (String) value; + if (username == null || !username.equals(username.trim())) { + new ErrorDialog(context, R.string.settings_invalid_username, false); + return false; + } + return true; + } + }); + } + + public void update() { + serverName.setSummary(serverName.getText()); + serverUrl.setSummary(serverUrl.getText()); + serverLocalNetworkSSID.setSummary(serverLocalNetworkSSID.getText()); + serverInternalUrl.setSummary(serverInternalUrl.getText()); + username.setSummary(username.getText()); + screen.setSummary(serverUrl.getText()); + screen.setTitle(serverName.getText()); + } + } +} diff --git a/src/github/daneren2005/dsub/fragments/SimilarArtistFragment.java b/src/github/daneren2005/dsub/fragments/SimilarArtistFragment.java new file mode 100644 index 00000000..c029581b --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SimilarArtistFragment.java @@ -0,0 +1,135 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.fragments; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.ArtistInfo; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.ArtistAdapter; + +import java.net.URLEncoder; +import java.util.List; + +public class SimilarArtistFragment extends SelectListFragment<Artist> { + private static final String TAG = SimilarArtistFragment.class.getSimpleName(); + private ArtistInfo info; + private String artistId; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + artist = true; + + artistId = getArguments().getString(Constants.INTENT_EXTRA_NAME_ARTIST); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(super.onOptionsItemSelected(item)) { + return true; + } + + switch (item.getItemId()) { + case R.id.menu_show_missing: + showMissingArtists(); + break; + } + + return false; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, view, menuInfo); + + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + Object entry = listView.getItemAtPosition(info.position); + onCreateContextMenu(menu, view, menuInfo, entry); + + recreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem) { + if(menuItem.getGroupId() != getSupportTag()) { + return false; + } + + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo(); + Artist artist = (Artist) listView.getItemAtPosition(info.position); + return onContextItemSelected(menuItem, artist); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Artist artist = (Artist) parent.getItemAtPosition(position); + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId()); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getName()); + args.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true); + fragment.setArguments(args); + + replaceFragment(fragment); + } + + @Override + public int getOptionsMenu() { + return R.menu.similar_artists; + } + + @Override + public ArrayAdapter getAdapter(List<Artist> objects) { + return new ArtistAdapter(context, objects); + } + + @Override + public List<Artist> getObjects(MusicService musicService, boolean refresh, ProgressListener listener) throws Exception { + info = musicService.getArtistInfo(artistId, refresh, context, listener); + return info.getSimilarArtists(); + } + + @Override + public int getTitleResource() { + return R.string.menu_similar_artists; + } + + private void showMissingArtists() { + StringBuilder b = new StringBuilder(); + + for(String name: info.getMissingArtists()) { + b.append("<h3><a href=\"https://www.google.com/#q=" + URLEncoder.encode(name) + "\">" + name + "</a></h3> "); + } + + Util.showHTMLDialog(context, R.string.menu_similar_artists, b.toString()); + } +} diff --git a/src/github/daneren2005/dsub/fragments/SubsonicFragment.java b/src/github/daneren2005/dsub/fragments/SubsonicFragment.java index 5cab2497..5a190439 100644 --- a/src/github/daneren2005/dsub/fragments/SubsonicFragment.java +++ b/src/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -617,11 +617,9 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR R.color.holo_red_light);
}
- protected void warnIfNetworkOrStorageUnavailable() {
+ protected void warnIfStorageUnavailable() {
if (!Util.isExternalStoragePresent()) {
Util.toast(context, R.string.select_album_no_sdcard);
- } else if (!Util.isOffline(context) && !Util.isNetworkConnected(context)) {
- Util.toast(context, R.string.select_album_no_network);
}
}
@@ -849,12 +847,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR downloadRecursively(id, name, isDirectory, save, append, autoplay, shuffle, background, false);
}
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, final boolean playNext) {
- LoadingTask<Boolean> task = new LoadingTask<Boolean>(context) {
- private MusicService musicService;
- private static final int MAX_SONGS = 500;
- private boolean playNowOverride = false;
- private List<Entry> songs;
-
+ new RecursiveLoader(context) {
@Override
protected Boolean doInBackground() throws Throwable {
musicService = MusicServiceFactory.getMusicService(context);
@@ -889,7 +882,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR return false;
}
- if (!append) {
+ if (!append && !background) {
downloadService.clear();
}
if(!background) {
@@ -906,48 +899,47 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR return transition;
}
+ }.execute();
+ }
- private void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception {
- if (songs.size() > MAX_SONGS) {
- return;
- }
+ protected void downloadRecursively(final List<Entry> albums, final boolean shuffle, final boolean append) {
+ new RecursiveLoader(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ musicService = MusicServiceFactory.getMusicService(context);
- for (Entry song : parent.getChildren(false, true)) {
- if (!song.isVideo() && song.getRating() != 1) {
- songs.add(song);
- }
+ if(shuffle) {
+ Collections.shuffle(albums);
}
- for (Entry dir : parent.getChildren(true, false)) {
- if(dir.getRating() == 1) {
- continue;
+
+ songs = new LinkedList<Entry>();
+ MusicDirectory root = new MusicDirectory();
+ root.addChildren(albums);
+ getSongsRecursively(root, songs);
+
+ DownloadService downloadService = getDownloadService();
+ boolean transition = false;
+ if (!songs.isEmpty() && downloadService != null) {
+ // Conditions for a standard play now operation
+ if(!append && !shuffle) {
+ playNowOverride = true;
+ return false;
}
- MusicDirectory musicDirectory;
- if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
- musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this);
- } else {
- musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this);
+ if (!append) {
+ downloadService.clear();
}
- getSongsRecursively(musicDirectory, songs);
- }
- }
- @Override
- protected void done(Boolean result) {
- if(playNowOverride) {
- playNow(songs);
- return;
+ downloadService.download(songs, false, true, false, false);
+ if(!append) {
+ transition = true;
+ }
}
+ artistOverride = false;
- warnIfNetworkOrStorageUnavailable();
-
- if(result) {
- Util.startActivityWithoutTransition(context, DownloadActivity.class);
- }
+ return transition;
}
- };
-
- task.execute();
+ }.execute();
}
protected MusicDirectory getMusicDirectory(String id, String name, boolean refresh, MusicService service, ProgressListener listener) throws Exception {
@@ -1249,7 +1241,7 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR msg += "\nCached Bitrate: " + bitrate + " kbps";
}
if(size != 0) {
- msg += "\nSize: " + Util.formatBytes(size);
+ msg += "\nSize: " + Util.formatLocalizedBytes(size, context);
}
if(song.getDuration() != null && song.getDuration() != 0) {
msg += "\nLength: " + Util.formatDuration(song.getDuration());
@@ -1351,25 +1343,35 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR }
public void deleteRecursively(Artist artist) {
- File dir = FileUtil.getArtistDirectory(context, artist);
- if(dir == null) return;
-
- MediaStoreService mediaStore = new MediaStoreService(context);
- Util.recursiveDelete(dir, mediaStore);
- if(Util.isOffline(context)) {
- refresh();
- }
+ deleteRecursively(FileUtil.getArtistDirectory(context, artist));
}
public void deleteRecursively(Entry album) {
- File dir = FileUtil.getAlbumDirectory(context, album);
- if(dir == null) return;
+ deleteRecursively(FileUtil.getAlbumDirectory(context, album));
- MediaStoreService mediaStore = new MediaStoreService(context);
- Util.recursiveDelete(dir, mediaStore);
- if(Util.isOffline(context)) {
- refresh();
+ }
+ public void deleteRecursively(final File dir) {
+ if(dir == null) {
+ return;
}
+
+ new LoadingTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MediaStoreService mediaStore = new MediaStoreService(context);
+ Util.recursiveDelete(dir, mediaStore);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(Util.isOffline(context)) {
+ refresh();
+ } else {
+ UpdateView.triggerUpdate();
+ }
+ }
+ }.execute();
}
public void showAlbumArtist(Entry entry) {
@@ -1734,4 +1736,54 @@ public class SubsonicFragment extends Fragment implements SwipeRefreshLayout.OnR public abstract class OnStarChange {
abstract void starChange(boolean starred);
}
+
+ public abstract class RecursiveLoader extends LoadingTask<Boolean> {
+ protected MusicService musicService;
+ protected static final int MAX_SONGS = 500;
+ protected boolean playNowOverride = false;
+ protected List<Entry> songs;
+
+ public RecursiveLoader(Activity context) {
+ super(context);
+ }
+
+ protected void getSongsRecursively(MusicDirectory parent, List<Entry> songs) throws Exception {
+ if (songs.size() > MAX_SONGS) {
+ return;
+ }
+
+ for (Entry song : parent.getChildren(false, true)) {
+ if (!song.isVideo() && song.getRating() != 1) {
+ songs.add(song);
+ }
+ }
+ for (Entry dir : parent.getChildren(true, false)) {
+ if(dir.getRating() == 1) {
+ continue;
+ }
+
+ MusicDirectory musicDirectory;
+ if(Util.isTagBrowsing(context) && !Util.isOffline(context)) {
+ musicDirectory = musicService.getAlbum(dir.getId(), dir.getTitle(), false, context, this);
+ } else {
+ musicDirectory = musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this);
+ }
+ getSongsRecursively(musicDirectory, songs);
+ }
+ }
+
+ @Override
+ protected void done(Boolean result) {
+ warnIfStorageUnavailable();
+
+ if(playNowOverride) {
+ playNow(songs);
+ return;
+ }
+
+ if(result) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }
+ }
}
diff --git a/src/github/daneren2005/dsub/provider/DLNARouteProvider.java b/src/github/daneren2005/dsub/provider/DLNARouteProvider.java index ca6fabe0..34ac182c 100644 --- a/src/github/daneren2005/dsub/provider/DLNARouteProvider.java +++ b/src/github/daneren2005/dsub/provider/DLNARouteProvider.java @@ -33,19 +33,20 @@ import android.support.v7.media.MediaRouteProvider; import android.support.v7.media.MediaRouteProviderDescriptor; import android.util.Log; -import org.teleal.cling.android.AndroidUpnpService; -import org.teleal.cling.android.AndroidUpnpServiceImpl; -import org.teleal.cling.model.action.ActionInvocation; -import org.teleal.cling.model.message.UpnpResponse; -import org.teleal.cling.model.meta.Device; -import org.teleal.cling.model.meta.LocalDevice; -import org.teleal.cling.model.meta.RemoteDevice; -import org.teleal.cling.model.meta.StateVariable; -import org.teleal.cling.model.meta.StateVariableAllowedValueRange; -import org.teleal.cling.model.types.ServiceType; -import org.teleal.cling.registry.Registry; -import org.teleal.cling.registry.RegistryListener; -import org.teleal.cling.support.renderingcontrol.callback.GetVolume; +import org.eclipse.jetty.util.log.Logger; +import org.fourthline.cling.android.AndroidUpnpService; +import org.fourthline.cling.android.AndroidUpnpServiceImpl; +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Device; +import org.fourthline.cling.model.meta.LocalDevice; +import org.fourthline.cling.model.meta.RemoteDevice; +import org.fourthline.cling.model.meta.StateVariable; +import org.fourthline.cling.model.meta.StateVariableAllowedValueRange; +import org.fourthline.cling.model.types.ServiceType; +import org.fourthline.cling.registry.Registry; +import org.fourthline.cling.registry.RegistryListener; +import org.fourthline.cling.support.renderingcontrol.callback.GetVolume; import java.util.ArrayList; import java.util.HashMap; @@ -58,9 +59,6 @@ import github.daneren2005.dsub.service.DLNAController; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.RemoteController; -/** - * Created by Scott on 11/28/13. - */ public class DLNARouteProvider extends MediaRouteProvider { private static final String TAG = DLNARouteProvider.class.getSimpleName(); public static final String CATEGORY_DLNA = "github.daneren2005.dsub.DLNA"; @@ -75,6 +73,10 @@ public class DLNARouteProvider extends MediaRouteProvider { public DLNARouteProvider(Context context) { super(context); + + // Use custom logger + org.eclipse.jetty.util.log.Log.setLog(new JettyAndroidLog()); + this.downloadService = (DownloadService) context; dlnaServiceConnection = new ServiceConnection() { @Override @@ -83,12 +85,12 @@ public class DLNARouteProvider extends MediaRouteProvider { dlnaService.getRegistry().addListener(new RegistryListener() { @Override public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) { - + Log.i(TAG, "Stared DLNA discovery"); } @Override public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) { - + Log.w(TAG, "Failed to discover DLNA devices"); } @Override @@ -138,7 +140,10 @@ public class DLNARouteProvider extends MediaRouteProvider { dlnaService = null; } }; - context.bindService(new Intent(context, AndroidUpnpServiceImpl.class), dlnaServiceConnection, Context.BIND_AUTO_CREATE); + + if(!context.getApplicationContext().bindService(new Intent(context, AndroidUpnpServiceImpl.class), dlnaServiceConnection, Context.BIND_AUTO_CREATE)) { + Log.e(TAG, "Failed to bind to DLNA service"); + } } private void broadcastDescriptors() { @@ -156,13 +161,17 @@ public class DLNARouteProvider extends MediaRouteProvider { for(Map.Entry<String, DLNADevice> deviceEntry: devices.entrySet()) { DLNADevice device = deviceEntry.getValue(); + int increments = device.volumeMax / 10; + int volume = controller == null ? device.volume : (int) controller.getVolume(); + volume = volume / increments; + MediaRouteDescriptor.Builder routeBuilder = new MediaRouteDescriptor.Builder(device.id, device.name); routeBuilder.addControlFilter(routeIntentFilter) .setPlaybackStream(AudioManager.STREAM_MUSIC) .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) .setDescription(device.description) - .setVolume(controller == null ? 5 : (int) (controller.getVolume() * 10)) - .setVolumeMax(device.volumeMax) + .setVolume(volume) + .setVolumeMax(10) .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE); providerBuilder.addRoute(routeBuilder.build()); } @@ -189,7 +198,7 @@ public class DLNARouteProvider extends MediaRouteProvider { } private void deviceAdded(final Device device) { - final org.teleal.cling.model.meta.Service renderingControl = device.findService(new ServiceType("schemas-upnp-org", "RenderingControl")); + final org.fourthline.cling.model.meta.Service renderingControl = device.findService(new ServiceType("schemas-upnp-org", "RenderingControl")); if(renderingControl == null) { return; } @@ -202,39 +211,45 @@ public class DLNARouteProvider extends MediaRouteProvider { adding.add(id); if(device.getType().getType().equals("MediaRenderer") && device instanceof RemoteDevice) { - dlnaService.getControlPoint().execute(new GetVolume(renderingControl) { - @Override - public void received(ActionInvocation actionInvocation, int currentVolume) { - int maxVolume = 100; - StateVariable volume = renderingControl.getStateVariable("Volume"); - if(volume != null) { - StateVariableAllowedValueRange volumeRange = volume.getTypeDetails().getAllowedValueRange(); - maxVolume = (int) volumeRange.getMaximum(); - } - - // Create a new DLNADevice to represent this item - String id = device.getIdentity().getUdn().toString(); - String name = device.getDetails().getFriendlyName(); - String displayName = device.getDisplayString(); - - DLNADevice newDevice = new DLNADevice(id, name, displayName, currentVolume, maxVolume); - devices.put(id, newDevice); - downloadService.post(new Runnable() { - @Override - public void run() { - broadcastDescriptors(); + try { + dlnaService.getControlPoint().execute(new GetVolume(renderingControl) { + @Override + public void received(ActionInvocation actionInvocation, int currentVolume) { + int maxVolume = 100; + StateVariable volume = renderingControl.getStateVariable("Volume"); + if (volume != null) { + StateVariableAllowedValueRange volumeRange = volume.getTypeDetails().getAllowedValueRange(); + maxVolume = (int) volumeRange.getMaximum(); } - }); - adding.remove(id); - } - @Override - public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) { - Log.w(TAG, "Failed to get default volume for DLNA route"); - Log.w(TAG, "Reason: " + s); - adding.remove(id); - } - }); + // Create a new DLNADevice to represent this item + String id = device.getIdentity().getUdn().toString(); + String name = device.getDetails().getFriendlyName(); + String displayName = device.getDisplayString(); + + DLNADevice newDevice = new DLNADevice(device, id, name, displayName, currentVolume, maxVolume); + devices.put(id, newDevice); + downloadService.post(new Runnable() { + @Override + public void run() { + broadcastDescriptors(); + } + }); + adding.remove(id); + } + + @Override + public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) { + Log.w(TAG, "Failed to get default volume for DLNA route"); + Log.w(TAG, "Reason: " + s); + adding.remove(id); + } + }); + } catch(Exception e) { + Log.e(TAG, "Failed to add device", e); + } + } else { + adding.remove(id); } } private void deviceRemoved(Device device) { @@ -276,7 +291,7 @@ public class DLNARouteProvider extends MediaRouteProvider { @Override public void onSelect() { - controller = new DLNAController(device); + controller = new DLNAController(downloadService, dlnaService.getControlPoint(), device); downloadService.setRemoteEnabled(RemoteControlState.DLNA, controller); } @@ -302,4 +317,86 @@ public class DLNARouteProvider extends MediaRouteProvider { broadcastDescriptors(); } } + + public static class JettyAndroidLog implements Logger { + final private static java.util.logging.Logger log = java.util.logging.Logger.getLogger("Jetty"); + + public static boolean __isIgnoredEnabled = false; + public String _name; + + public JettyAndroidLog() { + this (JettyAndroidLog.class.getName()); + } + + public JettyAndroidLog(String name) { + _name = name; + } + + public String getName () { + return _name; + } + + public void debug(Throwable th) { + // Log.d(TAG, "", th); + } + + public void debug(String msg, Throwable th) { + // Log.d(TAG, msg, th); + } + + public void debug(String msg, Object... args) { + // Log.d(TAG, msg); + } + + public Logger getLogger(String name) { + return new JettyAndroidLog(name); + } + + public void info(String msg, Object... args) { + // Log.i(TAG, msg); + } + + public void info(Throwable th) { + // Log.i(TAG, "", th); + } + + public void info(String msg, Throwable th) { + // Log.i(TAG, msg, th); + } + + public boolean isDebugEnabled() { + return false; + } + + public void warn(Throwable th) { + Log.w(TAG, "", th); + } + + public void warn(String msg, Object... args) { + Log.w(TAG, msg); + } + + public void warn(String msg, Throwable th) { + Log.w(TAG, msg, th); + } + + public boolean isIgnoredEnabled () { + return __isIgnoredEnabled; + } + + + public void ignore(Throwable ignored) { + if (__isIgnoredEnabled) { + warn("IGNORED", ignored); + } + } + + public void setIgnoredEnabled(boolean enabled) { + __isIgnoredEnabled = enabled; + } + + public void setDebugEnabled(boolean enabled) { + + } + } } diff --git a/src/github/daneren2005/dsub/provider/DSubSearchProvider.java b/src/github/daneren2005/dsub/provider/DSubSearchProvider.java index 01f08565..63bbaaa4 100644 --- a/src/github/daneren2005/dsub/provider/DSubSearchProvider.java +++ b/src/github/daneren2005/dsub/provider/DSubSearchProvider.java @@ -24,6 +24,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; +import android.util.Log; import java.util.ArrayList; import java.util.Collections; @@ -45,6 +46,8 @@ import github.daneren2005.dsub.util.Util; * @author Sindre Mehus */ public class DSubSearchProvider extends ContentProvider { + private static final String TAG = DSubSearchProvider.class.getSimpleName(); + private static final String RESOURCE_PREFIX = "android.resource://github.daneren2005.dsub/"; private static final String[] COLUMNS = {"_id", SearchManager.SUGGEST_COLUMN_TEXT_1, @@ -147,7 +150,13 @@ public class DSubSearchProvider extends ContentProvider { cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), entry.getId(), entry.getTitle(), icon}); } else { String icon = RESOURCE_PREFIX + R.drawable.ic_action_song; - cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), "so-" + entry.getParent(), entry.getTitle(), icon}); + String id; + if(Util.isTagBrowsing(getContext())) { + id = entry.getAlbumId(); + } else { + id = entry.getParent(); + } + cursor.addRow(new Object[]{entry.getId().hashCode(), entry.getTitle(), entry.getArtist(), "so-" + id, entry.getTitle(), icon}); } } } diff --git a/src/github/daneren2005/dsub/service/CachedMusicService.java b/src/github/daneren2005/dsub/service/CachedMusicService.java index 8e8e120d..232d0acf 100644 --- a/src/github/daneren2005/dsub/service/CachedMusicService.java +++ b/src/github/daneren2005/dsub/service/CachedMusicService.java @@ -32,6 +32,7 @@ import android.content.Context; import android.graphics.Bitmap; import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.ChatMessage; import github.daneren2005.dsub.domain.Genre; @@ -891,6 +892,27 @@ public class CachedMusicService implements MusicService { } @Override + public ArtistInfo getArtistInfo(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + String cacheName = getCacheName(context, "artistInfo", id); + ArtistInfo info = null; + if(!refresh) { + info = FileUtil.deserialize(context, cacheName, ArtistInfo.class); + } + + if(info == null) { + info = musicService.getArtistInfo(id, refresh, context, progressListener); + FileUtil.serialize(context, info, cacheName); + } + + return info; + } + + @Override + public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + return musicService.getBitmap(url, size, context, progressListener, task); + } + + @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ return musicService.processOfflineSyncs(context, progressListener); } diff --git a/src/github/daneren2005/dsub/service/ChromeCastController.java b/src/github/daneren2005/dsub/service/ChromeCastController.java index 0c8f38a6..6f35ac1d 100644 --- a/src/github/daneren2005/dsub/service/ChromeCastController.java +++ b/src/github/daneren2005/dsub/service/ChromeCastController.java @@ -283,7 +283,7 @@ public class ChromeCastController extends RemoteController { url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate()); } - url = fixURLs(url); + url = Util.replaceInternalUrl(downloadService, url); } // Setup song/video information @@ -300,7 +300,7 @@ public class ChromeCastController extends RemoteController { String coverArt = ""; if(proxy == null) { coverArt = musicService.getCoverArtUrl(downloadService, song); - coverArt = fixURLs(coverArt); + coverArt = Util.replaceInternalUrl(downloadService, coverArt); meta.addImage(new WebImage(Uri.parse(coverArt))); } else { File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song); @@ -354,22 +354,6 @@ public class ChromeCastController extends RemoteController { } } - private String fixURLs(String url) { - // Only change to internal when using https - if(url.indexOf("https") != -1) { - SharedPreferences prefs = Util.getPreferences(downloadService); - int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); - String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); - if(internalUrl != null && !"".equals(internalUrl)) { - String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); - url = url.replace(internalUrl, externalUrl); - } - } - - // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC - return url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID); - } - private void failedLoad() { Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load)); downloadService.setPlayerState(PlayerState.STOPPED); diff --git a/src/github/daneren2005/dsub/service/DLNAController.java b/src/github/daneren2005/dsub/service/DLNAController.java index dee65d64..2afb0fea 100644 --- a/src/github/daneren2005/dsub/service/DLNAController.java +++ b/src/github/daneren2005/dsub/service/DLNAController.java @@ -15,67 +15,440 @@ package github.daneren2005.dsub.service;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import org.fourthline.cling.controlpoint.ControlPoint;
+import org.fourthline.cling.controlpoint.SubscriptionCallback;
+import org.fourthline.cling.model.action.ActionInvocation;
+import org.fourthline.cling.model.gena.CancelReason;
+import org.fourthline.cling.model.gena.GENASubscription;
+import org.fourthline.cling.model.message.UpnpResponse;
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.model.meta.Service;
+import org.fourthline.cling.model.state.StateVariableValue;
+import org.fourthline.cling.model.types.ServiceType;
+import org.fourthline.cling.support.avtransport.callback.GetPositionInfo;
+import org.fourthline.cling.support.avtransport.callback.Pause;
+import org.fourthline.cling.support.avtransport.callback.Play;
+import org.fourthline.cling.support.avtransport.callback.Seek;
+import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI;
+import org.fourthline.cling.support.avtransport.callback.Stop;
+import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
+import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable;
+import org.fourthline.cling.support.connectionmanager.callback.PrepareForConnection;
+import org.fourthline.cling.support.contentdirectory.DIDLParser;
+import org.fourthline.cling.support.lastchange.LastChange;
+import org.fourthline.cling.support.model.DIDLContent;
+import org.fourthline.cling.support.model.PersonWithRole;
+import org.fourthline.cling.support.model.PositionInfo;
+import org.fourthline.cling.support.model.Res;
+import org.fourthline.cling.support.model.SeekMode;
+import org.fourthline.cling.support.model.item.Item;
+import org.fourthline.cling.support.model.item.MusicTrack;
+import org.fourthline.cling.support.model.item.VideoItem;
+import org.fourthline.cling.support.renderingcontrol.callback.SetVolume;
+import org.seamless.util.MimeType;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicLong;
+
+import github.daneren2005.dsub.R;
import github.daneren2005.dsub.domain.DLNADevice;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.serverproxy.FileProxy;
public class DLNAController extends RemoteController {
+ private static final String TAG = DLNAController.class.getSimpleName();
+ private static final long STATUS_UPDATE_INTERVAL_SECONDS = 3000L;
+
DLNADevice device;
+ ControlPoint controlPoint;
+ SubscriptionCallback callback;
- public DLNAController(DLNADevice device) {
+ private FileProxy proxy;
+ String rootLocation = "";
+ boolean error = false;
+
+ final AtomicLong lastUpdate = new AtomicLong();
+ int currentPosition = 0;
+ String currentPlayingURI;
+ boolean running = true;
+ boolean seekable = false;
+
+ public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) {
+ this.downloadService = downloadService;
+ this.controlPoint = controlPoint;
this.device = device;
+
+ SharedPreferences prefs = Util.getPreferences(downloadService);
+ rootLocation = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null);
}
@Override
- public void create(boolean playing, int seconds) {
+ public void create(final boolean playing, final int seconds) {
+ downloadService.setPlayerState(PlayerState.PREPARING);
+
+ callback = new SubscriptionCallback(getTransportService(), 600) {
+ @Override
+ protected void failed(GENASubscription genaSubscription, UpnpResponse upnpResponse, Exception e, String msg) {
+ Log.w(TAG, "Register subscription callback failed: " + msg, e);
+ }
+
+ @Override
+ protected void established(GENASubscription genaSubscription) {
+ startSong(downloadService.getCurrentPlaying(), playing, seconds);
+ }
+
+ @Override
+ protected void ended(GENASubscription genaSubscription, CancelReason cancelReason, UpnpResponse upnpResponse) {
+
+ }
+
+ @Override
+ protected void eventReceived(GENASubscription genaSubscription) {
+ Map<String, StateVariableValue> m = genaSubscription.getCurrentValues();
+ try {
+ LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), m.get("LastChange").toString());
+ if (playing || lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) {
+ return;
+ }
+
+ switch (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class).getValue()) {
+ case PLAYING:
+ downloadService.setPlayerState(PlayerState.STARTED);
+ break;
+ case PAUSED_PLAYBACK:
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ break;
+ case STOPPED:
+ boolean failed = false;
+ for(StateVariableValue val: m.values()) {
+ if(val.toString().indexOf("TransportStatus val=\"ERROR_OCCURRED\"") != -1) {
+ Log.w(TAG, "Failed to load with event: " + val.toString());
+ failed = true;
+ }
+ }
+
+ if(failed) {
+ failedLoad();
+ } else if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ // Played until the end
+ downloadService.setPlayerState(PlayerState.COMPLETED);
+ downloadService.next();
+ } else {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ }
+ break;
+ case TRANSITIONING:
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ break;
+ case NO_MEDIA_PRESENT:
+ downloadService.setPlayerState(PlayerState.IDLE);
+ break;
+ default:
+ }
+ }
+ catch (Exception e) {
+ Log.w(TAG, "Failed to parse UPNP event", e);
+ failedLoad();
+ }
+ }
+ @Override
+ protected void eventsMissed(GENASubscription genaSubscription, int i) {
+
+ }
+ };
+ controlPoint.execute(callback);
}
@Override
public void start() {
+ if(error) {
+ Log.w(TAG, "Attempting to restart song");
+ startSong(downloadService.getCurrentPlaying(), true, 0);
+ return;
+ }
+
+ controlPoint.execute(new Play(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ lastUpdate.set(System.currentTimeMillis());
+ downloadService.setPlayerState(PlayerState.STARTED);
+ }
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to start playing: " + msg);
+ failedLoad();
+ }
+ });
}
@Override
public void stop() {
+ controlPoint.execute(new Pause(getTransportService()) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
+ currentPosition += secondsSinceLastUpdate;
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Failed to pause playing: " + msg);
+ }
+ });
}
@Override
public void shutdown() {
+ controlPoint.execute(new Stop(getTransportService()) {
+ @Override
+ public void failure(ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Stop failed: " + defaultMessage);
+ }
+ });
+
+ if(callback != null) {
+ callback.end();
+ callback = null;
+ }
+ running = false;
}
@Override
public void updatePlaylist() {
-
+ if(downloadService.getCurrentPlaying() == null) {
+ startSong(null, false, 0);
+ }
}
@Override
public void changePosition(int seconds) {
-
+ SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ controlPoint.execute(new Seek(getTransportService(), SeekMode.REL_TIME, df.format(new Date(seconds * 1000))) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Seek failed: " + defaultMessage);
+ }
+ });
}
@Override
public void changeTrack(int index, DownloadFile song) {
-
+ startSong(song, true, 0);
}
@Override
public void setVolume(int volume) {
+ if(volume < 0) {
+ volume = 0;
+ } else if(volume > device.volumeMax) {
+ volume = device.volumeMax;
+ }
+ device.volume = volume;
+ controlPoint.execute(new SetVolume(device.renderer.findService(new ServiceType("schemas-upnp-org", "RenderingControl")), volume) {
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
+ Log.w(TAG, "Set volume failed: " + defaultMessage);
+ }
+ });
}
@Override
public void updateVolume(boolean up) {
-
+ int increment = device.volumeMax / 10;
+ setVolume(device.volume + (up ? increment : -increment));
}
@Override
public double getVolume() {
- return 0;
+ return device.volume;
}
@Override
public int getRemotePosition() {
- return 0;
+ if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
+ return currentPosition + secondsSinceLastUpdate;
+ } else {
+ return currentPosition;
+ }
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return seekable;
+ }
+
+ private void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
+ if(currentPlaying == null) {
+ downloadService.setPlayerState(PlayerState.IDLE);
+ return;
+ }
+ error = false;
+
+ downloadService.setPlayerState(PlayerState.PREPARING);
+ MusicDirectory.Entry song = currentPlaying.getSong();
+
+ try {
+ // Get url for entry
+ MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
+ String url;
+ if(Util.isOffline(downloadService) || song.getId().indexOf(rootLocation) != -1) {
+ if(proxy == null) {
+ proxy = new FileProxy(downloadService);
+ proxy.start();
+ }
+
+ url = proxy.getPublicAddress(song.getId());
+ } else {
+ if(proxy != null) {
+ proxy.stop();
+ proxy = null;
+ }
+
+ if(song.isVideo()) {
+ url = musicService.getHlsUrl(song.getId(), currentPlaying.getBitRate(), downloadService);
+ } else {
+ url = musicService.getMusicUrl(downloadService, song, currentPlaying.getBitRate());
+ }
+
+ url = Util.replaceInternalUrl(downloadService, url);
+ }
+
+ // Create metadata for entry
+ Item track;
+ if(song.isVideo()) {
+ track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist());
+ } else {
+ MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist());
+ musicTrack.setOriginalTrackNumber(song.getTrack());
+ track = musicTrack;
+ }
+
+ DIDLParser parser = new DIDLParser();
+ DIDLContent didl = new DIDLContent();
+ didl.addItem(track);
+
+ String metadata = "";
+ try {
+ // <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><CurrentURI>http://192.168.1.3:57645/external/audio/media/39883.mp3</CurrentURI><CurrentURIMetaData><DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:sec="http://www.sec.co.kr/"><item id="/external/audio/albums/484/39883" parentID="/external/audio/albums/484" restricted="1"><upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:title>03-Miss Murder.complete</dc:title><dc:creator>AFI</dc:creator><upnp:artist>AFI</upnp:artist><upnp:albumArtURI>http://192.168.1.3:57645/external/audio/albums/484.jpg</upnp:albumArtURI><upnp:genre>Rock</upnp:genre><dc:date>2006-01-01</dc:date><upnp:album>&lt;unknown&gt;</upnp:album><upnp:originalTrackNumber>3</upnp:originalTrackNumber><res protocolInfo="http-get:*:audio/mpeg:*" bitrate="24000" size="4961736" duration="0:03:26.000">http://192.168.1.3:57645/external/audio/media/39883.mp3</res></item></DIDL-Lite></CurrentURIMetaData></u:SetAVTransportURI></s:Body></s:Envelope>
+ // metadata = parser.generate(didl);
+ } catch(Exception e) {
+ Log.w(TAG, "Metadata generation failed", e);
+ }
+
+ currentPlayingURI = url;
+ controlPoint.execute(new SetAVTransportURI(getTransportService(), url, metadata) {
+ @Override
+ public void success(ActionInvocation invocation) {
+ if(position != 0) {
+ changePosition(position);
+ }
+
+ if (autoStart) {
+ start();
+ } else {
+ downloadService.setPlayerState(PlayerState.PAUSED);
+ }
+
+ currentPosition = position;
+ lastUpdate.set(System.currentTimeMillis());
+ getUpdatedStatus();
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
+ Log.w(TAG, "Set URI failed: " + msg);
+ failedLoad();
+ }
+ });
+ } catch (Exception e) {
+ Log.w(TAG, "Failed startSong", e);
+ failedLoad();
+ }
+ }
+
+ private void failedLoad() {
+ downloadService.setPlayerState(PlayerState.STOPPED);
+ error = true;
+
+ if(Looper.myLooper() != Looper.getMainLooper()) {
+ downloadService.post(new Runnable() {
+ @Override
+ public void run() {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ });
+ } else {
+ Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
+ }
+ }
+
+ private Service getTransportService() {
+ return device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"));
+ }
+
+ private void getUpdatedStatus() {
+ // Don't care if shutdown in the meantime
+ if(!running) {
+ return;
+ }
+
+ controlPoint.execute(new GetPositionInfo(getTransportService()) {
+ @Override
+ public void received(ActionInvocation actionInvocation, PositionInfo positionInfo) {
+ // Don't care if shutdown in the meantime
+ if(!running) {
+ return;
+ }
+
+ long duration = positionInfo.getTrackDurationSeconds();
+ seekable = duration > 0;
+
+ lastUpdate.set(System.currentTimeMillis());
+
+ // Let's get the updated position
+ currentPosition = (int) positionInfo.getTrackElapsedSeconds();
+
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getUpdatedStatus();
+ }
+ }, STATUS_UPDATE_INTERVAL_SECONDS);
+ }
+
+ @Override
+ public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
+ Log.w(TAG, "Failed to get an update");
+
+ downloadService.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ getUpdatedStatus();
+ }
+ }, STATUS_UPDATE_INTERVAL_SECONDS);
+ }
+ });
}
}
diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java index 4cbf2317..37741b92 100644 --- a/src/github/daneren2005/dsub/service/DownloadService.java +++ b/src/github/daneren2005/dsub/service/DownloadService.java @@ -28,6 +28,7 @@ import static github.daneren2005.dsub.domain.PlayerState.PREPARED; import static github.daneren2005.dsub.domain.PlayerState.PREPARING; import static github.daneren2005.dsub.domain.PlayerState.STARTED; import static github.daneren2005.dsub.domain.PlayerState.STOPPED; +import static github.daneren2005.dsub.domain.RemoteControlState.LOCAL; import github.daneren2005.dsub.R; import github.daneren2005.dsub.audiofx.AudioEffectsController; @@ -61,6 +62,7 @@ import java.util.List; import java.util.Timer; import java.util.TimerTask; +import android.annotation.TargetApi; import android.app.Service; import android.content.ComponentName; import android.content.Context; @@ -140,7 +142,7 @@ public class DownloadService extends Service { private float volume = 1.0f; private AudioEffectsController effectsController; - private RemoteControlState remoteState = RemoteControlState.LOCAL; + private RemoteControlState remoteState = LOCAL; private PositionCache positionCache; private BufferProxy proxy; @@ -302,6 +304,9 @@ public class DownloadService extends Service { public void post(Runnable r) { handler.post(r); } + public void postDelayed(Runnable r, long millis) { + handler.postDelayed(r, millis); + } public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { download(songs, save, autoplay, playNext, shuffle, 0, 0); @@ -309,6 +314,8 @@ public class DownloadService extends Service { public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle, int start, int position) { setShufflePlayEnabled(false); int offset = 1; + boolean noNetwork = !Util.isOffline(this) && !Util.isNetworkConnected(this); + boolean warnNetwork = false; if (songs.isEmpty()) { return; @@ -321,6 +328,11 @@ public class DownloadService extends Service { if(song != null) { DownloadFile downloadFile = new DownloadFile(this, song, save); addToDownloadList(downloadFile, getCurrentPlayingIndex() + offset); + if(noNetwork && !warnNetwork) { + if(!downloadFile.isCompleteFileAvailable()) { + warnNetwork = true; + } + } offset++; } } @@ -332,6 +344,11 @@ public class DownloadService extends Service { for (MusicDirectory.Entry song : songs) { DownloadFile downloadFile = new DownloadFile(this, song, save); addToDownloadList(downloadFile, -1); + if(noNetwork && !warnNetwork) { + if(!downloadFile.isCompleteFileAvailable()) { + warnNetwork = true; + } + } } if(!autoplay && (size - 1) == index) { setNextPlaying(); @@ -343,6 +360,9 @@ public class DownloadService extends Service { if(shuffle) { shuffle(); } + if(warnNetwork) { + Util.toast(this, R.string.select_album_no_network); + } if (autoplay) { play(start, true, position); @@ -378,22 +398,26 @@ public class DownloadService extends Service { } revision++; + if(!Util.isOffline(this) && !Util.isNetworkConnected(this)) { + Util.toast(this, R.string.select_album_no_network); + } + checkDownloads(); lifecycleSupport.serializeDownloadQueue(); } private void updateJukeboxPlaylist() { - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL && remoteController != null) { remoteController.updatePlaylist(); } } public synchronized void restore(List<MusicDirectory.Entry> songs, List<MusicDirectory.Entry> toDelete, int currentPlayingIndex, int currentPlayingPosition) { SharedPreferences prefs = Util.getPreferences(this); - remoteState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)]; - if(remoteState != RemoteControlState.LOCAL) { + RemoteControlState newState = RemoteControlState.values()[prefs.getInt(Constants.PREFERENCES_KEY_CONTROL_MODE, 0)]; + if(newState != LOCAL) { String id = prefs.getString(Constants.PREFERENCES_KEY_CONTROL_ID, null); - setRemoteState(remoteState, null, id); + setRemoteState(newState, null, id); } if(prefs.getBoolean(Constants.PREFERENCES_KEY_REMOVE_PLAYED, false)) { removePlayed = true; @@ -560,6 +584,9 @@ public class DownloadService extends Service { } else { mediaRouter.removeOnlineProviders(); } + if(shufflePlay) { + setShufflePlayEnabled(false); + } lifecycleSupport.post(new Runnable() { @Override @@ -681,6 +708,7 @@ public class DownloadService extends Service { this.currentPlaying = currentPlaying; if(currentPlaying == null) { currentPlayingIndex = -1; + setPlayerState(IDLE); } else { currentPlayingIndex = downloadList.indexOf(currentPlaying); } @@ -802,10 +830,10 @@ public class DownloadService extends Service { nextPlayingTask = null; } setCurrentPlaying(index, start); - if (start && remoteState != RemoteControlState.LOCAL) { + if (start && remoteState != LOCAL) { remoteController.changeTrack(index, currentPlaying); } - if (remoteState == RemoteControlState.LOCAL) { + if (remoteState == LOCAL) { bufferAndPlay(position, start); checkDownloads(); setNextPlaying(); @@ -845,7 +873,7 @@ public class DownloadService extends Service { nextMediaPlayer = tmp; setCurrentPlaying(nextPlaying, true); setPlayerState(PlayerState.STARTED); - setupHandlers(currentPlaying, false); + setupHandlers(currentPlaying, false, start); setNextPlaying(); // Proxy should not be being used here since the next player was already setup to play @@ -874,7 +902,7 @@ public class DownloadService extends Service { } try { - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL) { remoteController.changePosition(position / 1000); } else { mediaPlayer.seekTo(position); @@ -963,7 +991,7 @@ public class DownloadService extends Service { public synchronized void pause(boolean temp) { try { if (playerState == STARTED) { - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL) { remoteController.stop(); } else { mediaPlayer.pause(); @@ -980,7 +1008,7 @@ public class DownloadService extends Service { public synchronized void stop() { try { if (playerState == STARTED) { - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL) { remoteController.stop(); setPlayerState(STOPPED); handler.post(new Runnable() { @@ -1003,7 +1031,7 @@ public class DownloadService extends Service { public synchronized void start() { try { - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL) { remoteController.start(); } else { // Only start if done preparing @@ -1020,6 +1048,7 @@ public class DownloadService extends Service { } } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public synchronized void reset() { if (bufferTask != null) { bufferTask.cancel(); @@ -1027,7 +1056,7 @@ public class DownloadService extends Service { } try { // Only set to idle if it's not being killed to start RemoteController - if(remoteState == RemoteControlState.LOCAL) { + if(remoteState == LOCAL) { setPlayerState(IDLE); } mediaPlayer.setOnErrorListener(null); @@ -1043,6 +1072,7 @@ public class DownloadService extends Service { } } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public synchronized void resetNext() { try { if (nextMediaPlayer != null) { @@ -1067,7 +1097,7 @@ public class DownloadService extends Service { if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { return 0; } - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL) { return remoteController.getRemotePosition() * 1000; } else { return Math.max(0, cachedPosition - subtractPosition); @@ -1086,7 +1116,7 @@ public class DownloadService extends Service { } } if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { - if(remoteState == RemoteControlState.LOCAL) { + if(remoteState == LOCAL) { try { return mediaPlayer.getDuration(); } catch (Exception x) { @@ -1143,7 +1173,7 @@ public class DownloadService extends Service { scrobbler.scrobble(this, currentPlaying, true); } - if(playerState == STARTED && positionCache == null && remoteState == RemoteControlState.LOCAL) { + if(playerState == STARTED && positionCache == null && remoteState == LOCAL) { positionCache = new PositionCache(); Thread thread = new Thread(positionCache, "PositionCache"); thread.start(); @@ -1166,7 +1196,16 @@ public class DownloadService extends Service { while(isRunning) { try { if(mediaPlayer != null && playerState == STARTED) { - cachedPosition = mediaPlayer.getCurrentPosition(); + int newPosition = mediaPlayer.getCurrentPosition(); + + // If sudden jump in position, something is wrong + if(subtractNextPosition == 0 && newPosition > (cachedPosition + 5000)) { + // Only 1 second should have gone by, subtract the rest + subtractPosition += (newPosition - cachedPosition) - 1000; + } + + cachedPosition = newPosition; + if(subtractNextPosition > 0) { // Subtraction amount is current position - how long ago onCompletionListener was called subtractPosition = cachedPosition - (int) (System.currentTimeMillis() - subtractNextPosition); @@ -1205,7 +1244,7 @@ public class DownloadService extends Service { public void setSuggestedPlaylistName(String name, String id) { this.suggestedPlaylistName = name; this.suggestedPlaylistId = id; - + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_NAME, name); editor.putString(Constants.PREFERENCES_KEY_PLAYLIST_ID, id); @@ -1255,7 +1294,7 @@ public class DownloadService extends Service { // Don't try again, just resetup media player and continue on controller = null; } - + // Restart from same position and state we left off in play(getCurrentPlayingIndex(), false, pos); } @@ -1267,8 +1306,18 @@ public class DownloadService extends Service { return mediaRouter.getSelector(); } + public boolean isSeekable() { + if(remoteState == LOCAL) { + return currentPlaying != null && currentPlaying.isWorkDone() && playerState != PREPARING; + } else if(remoteController != null) { + return remoteController.isSeekable(); + } else { + return false; + } + } + public boolean isRemoteEnabled() { - return remoteState != RemoteControlState.LOCAL; + return remoteState != LOCAL; } public RemoteController getRemoteController() { @@ -1295,6 +1344,11 @@ public class DownloadService extends Service { setRemoteState(newState, ref, null); } private void setRemoteState(final RemoteControlState newState, final Object ref, final String routeId) { + // Don't try to do anything if already in the correct state + if(remoteState == newState) { + return; + } + boolean isPlaying = playerState == STARTED; int position = getPlayerPosition(); @@ -1304,19 +1358,20 @@ public class DownloadService extends Service { remoteController.shutdown(); remoteController = null; - if(newState == RemoteControlState.LOCAL) { + if(newState == LOCAL) { mediaRouter.setDefaultRoute(); } } + Log.i(TAG, remoteState.name() + " => " + newState.name() + " (" + currentPlaying + ")"); remoteState = newState; switch(newState) { case JUKEBOX_SERVER: remoteController = new JukeboxController(this, handler); break; - case CHROMECAST: + case CHROMECAST: case DLNA: if(ref == null) { - remoteState = RemoteControlState.LOCAL; + remoteState = LOCAL; break; } remoteController = (RemoteController) ref; @@ -1331,7 +1386,7 @@ public class DownloadService extends Service { play(getCurrentPlayingIndex(), isPlaying, position); } - if (remoteState != RemoteControlState.LOCAL) { + if (remoteState != LOCAL) { reset(); // Cancel current download, if necessary. @@ -1350,7 +1405,7 @@ public class DownloadService extends Service { } } - if(remoteState == RemoteControlState.LOCAL) { + if(remoteState == LOCAL) { checkDownloads(); } @@ -1360,7 +1415,7 @@ public class DownloadService extends Service { public void run() { RouteInfo info = mediaRouter.getRouteForId(routeId); if(info == null) { - setRemoteState(RemoteControlState.LOCAL, null); + setRemoteState(LOCAL, null); } else if(newState == RemoteControlState.CHROMECAST) { RemoteController controller = mediaRouter.getRemoteController(info); if(controller != null) { @@ -1435,6 +1490,7 @@ public class DownloadService extends Service { subtractPosition = 0; mediaPlayer.setOnCompletionListener(null); + mediaPlayer.setOnPreparedListener(null); mediaPlayer.reset(); setPlayerState(IDLE); try { @@ -1484,7 +1540,7 @@ public class DownloadService extends Service { if (start || autoPlayStart) { mediaPlayer.start(); setPlayerState(STARTED); - + // Disable autoPlayStart after done autoPlayStart = false; } else { @@ -1502,7 +1558,7 @@ public class DownloadService extends Service { } }); - setupHandlers(downloadFile, isPartial); + setupHandlers(downloadFile, isPartial, start); mediaPlayer.prepareAsync(); } catch (Exception x) { @@ -1510,13 +1566,14 @@ public class DownloadService extends Service { } } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private synchronized void setupNext(final DownloadFile downloadFile) { try { final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); resetNext(); // Exit when using remote controllers - if(remoteState != RemoteControlState.LOCAL) { + if(remoteState != LOCAL) { return; } @@ -1560,7 +1617,7 @@ public class DownloadService extends Service { } } - private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) { + private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial, final boolean isPlaying) { final int duration = downloadFile.getSong().getDuration() == null ? 0 : downloadFile.getSong().getDuration() * 1000; mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { @@ -1571,7 +1628,7 @@ public class DownloadService extends Service { playNext(); } else { downloadFile.setPlaying(false); - doPlay(downloadFile, pos, true); + doPlay(downloadFile, pos, isPlaying); downloadFile.setPlaying(true); } return true; @@ -1684,7 +1741,7 @@ public class DownloadService extends Service { DownloadFile movedSong = list.remove(from); list.add(to, movedSong); currentPlayingIndex = downloadList.indexOf(currentPlaying); - if(remoteState != RemoteControlState.LOCAL && mainList) { + if(remoteState != LOCAL && mainList) { updateJukeboxPlaylist(); } else if(mainList && (movedSong == nextPlaying || movedSong == currentPlaying || (currentPlayingIndex + 1) == to)) { // Moving next playing, current playing, or moving a song to be next playing @@ -1731,7 +1788,7 @@ public class DownloadService extends Service { checkShufflePlay(); } - if (remoteState != RemoteControlState.LOCAL || !Util.isNetworkConnected(this, true) || Util.isOffline(this)) { + if (!Util.isNetworkConnected(this, true) || Util.isOffline(this)) { return; } @@ -1739,8 +1796,8 @@ public class DownloadService extends Service { return; } - // Need to download current playing? - if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { + // Need to download current playing and not casting? + if (currentPlaying != null && remoteState == LOCAL && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { // Cancel current download, if necessary. if (currentDownloading != null) { currentDownloading.cancelDownload(); @@ -1752,13 +1809,13 @@ public class DownloadService extends Service { } // Find a suitable target for download. - else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) { + else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && ((!downloadList.isEmpty() && remoteState == LOCAL) || !backgroundDownloadList.isEmpty())) { currentDownloading = null; int n = size(); int preloaded = 0; - if(n != 0) { + if(n != 0 && remoteState == LOCAL) { int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); if(start == -1) { start = 0; @@ -1784,7 +1841,7 @@ public class DownloadService extends Service { } while (i != start); } - if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) { + if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty() || remoteState != LOCAL) && !backgroundDownloadList.isEmpty()) { for(int i = 0; i < backgroundDownloadList.size(); i++) { DownloadFile downloadFile = backgroundDownloadList.get(i); if(downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved()) || downloadFile.isFailedMax()) { diff --git a/src/github/daneren2005/dsub/service/MusicService.java b/src/github/daneren2005/dsub/service/MusicService.java index 765f498a..854a0aa4 100644 --- a/src/github/daneren2005/dsub/service/MusicService.java +++ b/src/github/daneren2005/dsub/service/MusicService.java @@ -25,6 +25,7 @@ import org.apache.http.HttpResponse; import android.content.Context; import android.graphics.Bitmap; +import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.Bookmark; import github.daneren2005.dsub.domain.ChatMessage; import github.daneren2005.dsub.domain.Genre; @@ -177,6 +178,10 @@ public interface MusicService { void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception; Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception; + + ArtistInfo getArtistInfo(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception; int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception; diff --git a/src/github/daneren2005/dsub/service/OfflineMusicService.java b/src/github/daneren2005/dsub/service/OfflineMusicService.java index c26a2fc4..4bd90d09 100644 --- a/src/github/daneren2005/dsub/service/OfflineMusicService.java +++ b/src/github/daneren2005/dsub/service/OfflineMusicService.java @@ -37,6 +37,7 @@ import android.util.Log; import org.apache.http.HttpResponse; import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.ArtistInfo; import github.daneren2005.dsub.domain.ChatMessage; import github.daneren2005.dsub.domain.Genre; import github.daneren2005.dsub.domain.Indexes; @@ -320,6 +321,10 @@ public class OfflineMusicService implements MusicService { for(File songFile : FileUtil.listMediaFiles(albumFile)) { String songName = getName(songFile); + if(songName == null) { + continue; + } + if(songFile.isDirectory()) { recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs); } @@ -779,6 +784,16 @@ public class OfflineMusicService implements MusicService { } @Override + public ArtistInfo getArtistInfo(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override + public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + throw new OfflineException(ERRORMSG); + } + + @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ throw new OfflineException(ERRORMSG); } diff --git a/src/github/daneren2005/dsub/service/RESTMusicService.java b/src/github/daneren2005/dsub/service/RESTMusicService.java index db1504f0..cd0ae376 100644 --- a/src/github/daneren2005/dsub/service/RESTMusicService.java +++ b/src/github/daneren2005/dsub/service/RESTMusicService.java @@ -69,6 +69,7 @@ import android.util.Log; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; import github.daneren2005.dsub.service.parser.AlbumListParser; +import github.daneren2005.dsub.service.parser.ArtistInfoParser; import github.daneren2005.dsub.service.parser.BookmarkParser; import github.daneren2005.dsub.service.parser.ChatMessageParser; import github.daneren2005.dsub.service.parser.ErrorParser; @@ -598,7 +599,7 @@ public class RESTMusicService implements MusicService { @Override public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception { StringBuilder builder = new StringBuilder(getRestUrl(context, "getCoverArt")); - builder.append("&id=").append(entry.getId()); + builder.append("&id=").append(entry.getCoverArt()); return builder.toString(); } @@ -1379,6 +1380,66 @@ public class RESTMusicService implements MusicService { } @Override + public ArtistInfo getArtistInfo(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.11", "Getting artist info is not supported"); + + Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtistInfo2" : "getArtistInfo", null, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true")); + try { + return new ArtistInfoParser(context, getInstance(context)).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { + // Synchronize on the url so that we don't download concurrently + synchronized (url) { + // Use cached file, if existing. + Bitmap bitmap = FileUtil.getMiscBitmap(context, url, size); + if(bitmap != null) { + return bitmap; + } + + InputStream in = null; + try { + HttpEntity entity = getEntityForURL(context, url, null, null, null, progressListener, task); + in = entity.getContent(); + Header contentEncoding = entity.getContentEncoding(); + if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { + in = new GZIPInputStream(in); + } + + // If content type is XML, an error occurred. Get it. + String contentType = Util.getContentType(entity); + if (contentType != null && contentType.startsWith("text/xml")) { + new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); + return null; // Never reached. + } + + byte[] bytes = Util.toByteArray(in); + if(task != null && task.isCancelled()) { + // Handle case where partial is downloaded and cancelled + return null; + } + + OutputStream out = null; + try { + out = new FileOutputStream(FileUtil.getMiscFile(context, url)); + out.write(bytes); + } finally { + Util.close(out); + } + + return FileUtil.getSampledBitmap(bytes, size, false); + } + finally { + Util.close(in); + } + } + } + + @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener); } @@ -1670,14 +1731,17 @@ public class RESTMusicService implements MusicService { redirectedUrl = request.getURI().toString(); } - redirectFrom = originalUrl.substring(0, originalUrl.indexOf("/rest/")); - redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/")); + int index = originalUrl.indexOf("/rest/"); + if(index != -1) { + redirectFrom = originalUrl.substring(0, index); + redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/")); - if(redirectFrom.compareTo(redirectTo) != 0) { - Log.i(TAG, redirectFrom + " redirects to " + redirectTo); + if (redirectFrom.compareTo(redirectTo) != 0) { + Log.i(TAG, redirectFrom + " redirects to " + redirectTo); + } + redirectionLastChecked = System.currentTimeMillis(); + redirectionNetworkType = getCurrentNetworkType(context); } - redirectionLastChecked = System.currentTimeMillis(); - redirectionNetworkType = getCurrentNetworkType(context); } private String rewriteUrlWithRedirect(Context context, String url) { diff --git a/src/github/daneren2005/dsub/service/RemoteController.java b/src/github/daneren2005/dsub/service/RemoteController.java index 89d4f4fd..02deaf85 100644 --- a/src/github/daneren2005/dsub/service/RemoteController.java +++ b/src/github/daneren2005/dsub/service/RemoteController.java @@ -48,6 +48,9 @@ public abstract class RemoteController { public abstract void setVolume(int volume); public abstract void updateVolume(boolean up); public abstract double getVolume(); + public boolean isSeekable() { + return true; + } public abstract int getRemotePosition(); public int getRemoteDuration() { diff --git a/src/github/daneren2005/dsub/service/parser/ArtistInfoParser.java b/src/github/daneren2005/dsub/service/parser/ArtistInfoParser.java new file mode 100644 index 00000000..5c3d2412 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/ArtistInfoParser.java @@ -0,0 +1,82 @@ +/* + 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 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import android.util.Log; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.ArtistInfo; +import github.daneren2005.dsub.util.ProgressListener; + +public class ArtistInfoParser extends AbstractParser { + private static final String TAG = ArtistInfo.class.getSimpleName(); + + public ArtistInfoParser(Context context, int instance) { + super(context, instance); + } + + public ArtistInfo parse(Reader reader, ProgressListener progressListener) throws Exception { + init(reader); + + ArtistInfo info = new ArtistInfo(); + List<Artist> artists = new ArrayList<Artist>(); + List<String> missingArtists = new ArrayList<String>(); + + String tagName = null; + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + tagName = getElementName(); + if ("similarArtist".equals(tagName)) { + String id = get("id"); + if(id.equals("-1")) { + missingArtists.add(get("name")); + } else { + Artist artist = new Artist(); + artist.setId(id); + artist.setName(get("name")); + artist.setStarred(get("starred") != null); + artists.add(artist); + } + } else if ("error".equals(tagName)) { + handleError(); + } + } else if(eventType == XmlPullParser.TEXT) { + if ("biography".equals(tagName) && info.getBiography() == null) { + info.setBiography(getText()); + } else if ("musicBrainzId".equals(tagName) && info.getMusicBrainzId() == null) { + info.setMusicBrainzId(getText()); + } else if ("lastFmUrl".equals(tagName) && info.getLastFMUrl() == null) { + info.setLastFMUrl(getText()); + } else if ("largeImageUrl".equals(tagName) && info.getImageUrl() == null) { + info.setImageUrl(getText()); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + info.setSimilarArtists(artists); + info.setMissingArtists(missingArtists); + return info; + } +} diff --git a/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java b/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java index 2f11730a..3b1203c7 100644 --- a/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java +++ b/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java @@ -49,6 +49,7 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.io.IOException; +import java.lang.reflect.Array; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; @@ -58,8 +59,13 @@ import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.Provider; import java.security.SecureRandom; +import java.security.Security; import java.security.UnrecoverableKeyException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Layered socket factory for TLS/SSL connections. @@ -144,8 +150,6 @@ import java.security.UnrecoverableKeyException; public class SSLSocketFactory implements LayeredSocketFactory { private static final String TAG = SSLSocketFactory.class.getSimpleName(); public static final String TLS = "TLS"; - public static final String SSL = "SSL"; - public static final String SSLV2 = "SSLv2"; public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER = new AllowAllHostnameVerifier(); @@ -343,16 +347,18 @@ public class SSLSocketFactory implements LayeredSocketFactory { @SuppressWarnings("cast") public Socket createSocket(final HttpParams params) throws IOException { // the cast makes sure that the factory is working as expected - SSLSocket sslsocket = (SSLSocket) this.socketfactory.createSocket(); - sslsocket.setEnabledProtocols(sslsocket.getSupportedProtocols()); - return sslsocket; + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); + return sslSocket; } @SuppressWarnings("cast") public Socket createSocket() throws IOException { // the cast makes sure that the factory is working as expected SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(); - sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols()); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); return sslSocket; } @@ -444,7 +450,8 @@ public class SSLSocketFactory implements LayeredSocketFactory { port, autoClose ); - sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols()); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); if (this.hostnameVerifier != null) { this.hostnameVerifier.verify(host, sslSocket); } @@ -500,7 +507,8 @@ public class SSLSocketFactory implements LayeredSocketFactory { final String host, int port, boolean autoClose) throws IOException, UnknownHostException { SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(socket, host, port, autoClose); - sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols()); + sslSocket.setEnabledProtocols(getProtocols(sslSocket)); + sslSocket.setEnabledCipherSuites(getCiphers(sslSocket)); setHostName(sslSocket, host); return sslSocket; } @@ -513,4 +521,29 @@ public class SSLSocketFactory implements LayeredSocketFactory { Log.w(TAG, "SNI not useable", e); } } + + private String[] getProtocols(SSLSocket sslSocket) { + String[] protocols = sslSocket.getEnabledProtocols(); + + // Remove SSLv3 if it is not the only option + if(protocols.length > 1) { + List<String> protocolList = new ArrayList(Arrays.asList(protocols)); + protocolList.remove("SSLv3"); + protocols = protocolList.toArray(new String[protocolList.size()]); + } + + return protocols; + } + + private String[] getCiphers(SSLSocket sslSocket) { + String[] ciphers = sslSocket.getEnabledCipherSuites(); + + List<String> enabledCiphers = new ArrayList(Arrays.asList(ciphers)); + // On Android 5.0 release, Jetty doesn't seem to play nice with these ciphers + enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"); + enabledCiphers.remove("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"); + + ciphers = enabledCiphers.toArray(new String[enabledCiphers.size()]); + return ciphers; + } } diff --git a/src/github/daneren2005/dsub/util/FileUtil.java b/src/github/daneren2005/dsub/util/FileUtil.java index 54888b59..34838f33 100644 --- a/src/github/daneren2005/dsub/util/FileUtil.java +++ b/src/github/daneren2005/dsub/util/FileUtil.java @@ -250,6 +250,33 @@ public class FileUtil { return null; } + public static File getMiscDirectory(Context context) { + File dir = new File(getSubsonicDirectory(context), "misc"); + ensureDirectoryExistsAndIsReadWritable(dir); + ensureDirectoryExistsAndIsReadWritable(new File(dir, ".nomedia")); + return dir; + } + + public static File getMiscFile(Context context, String url) { + return new File(getMiscDirectory(context), Util.md5Hex(url) + ".jpeg"); + } + + public static Bitmap getMiscBitmap(Context context, String url, int size) { + File avatarFile = getMiscFile(context, url); + if (avatarFile.exists()) { + final BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + BitmapFactory.decodeFile(avatarFile.getPath(), opt); + opt.inPurgeable = true; + opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size)); + opt.inJustDecodeBounds = false; + + Bitmap bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt); + return bitmap == null ? null : getScaledBitmap(bitmap, size, false); + } + return null; + } + public static Bitmap getSampledBitmap(byte[] bytes, int size) { return getSampledBitmap(bytes, size, true); } @@ -403,7 +430,13 @@ public class FileUtil { public static File getDefaultMusicDirectory(Context context) { if(DEFAULT_MUSIC_DIR == null) { - File[] dirs = ContextCompat.getExternalFilesDirs(context, null); + File[] dirs; + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dirs = context.getExternalMediaDirs(); + } else { + dirs = ContextCompat.getExternalFilesDirs(context, null); + } + for(int i = dirs.length - 1; i >= 0; i--) { DEFAULT_MUSIC_DIR = new File(dirs[i], "music"); if(dirs[i] != null) { @@ -436,14 +469,27 @@ public class FileUtil { } } } + public static boolean deleteArtworkCache(Context context) { + File artDirectory = FileUtil.getAlbumArtDirectory(context); + return Util.recursiveDelete(artDirectory); + } + public static boolean deleteAvatarCache(Context context) { + File artDirectory = FileUtil.getAvatarDirectory(context); + return Util.recursiveDelete(artDirectory); + } public static void unpinSong(Context context, File saveFile) { + // Don't try to unpin a song which isn't actually pinned + if(saveFile.getName().contains(".complete")) { + return; + } + // Unpin file, rename to .complete File completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + ".complete." + FileUtil.getExtension(saveFile.getName())); if(!saveFile.renameTo(completeFile)) { - Log.w(TAG, "Failed to rename " + saveFile + " to " + completeFile); + Log.w(TAG, "Failed to upin " + saveFile + " to " + completeFile); } else { try { new MediaStoreService(context).renameInMediaStore(completeFile, saveFile); diff --git a/src/github/daneren2005/dsub/util/ImageLoader.java b/src/github/daneren2005/dsub/util/ImageLoader.java index ccdb3432..5adf5e34 100644 --- a/src/github/daneren2005/dsub/util/ImageLoader.java +++ b/src/github/daneren2005/dsub/util/ImageLoader.java @@ -18,8 +18,15 @@ */ package github.daneren2005.dsub.util; +import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; @@ -55,7 +62,8 @@ public class ImageLoader { private final int imageSizeDefault; private final int imageSizeLarge; private final int avatarSizeDefault; - private Drawable largeUnknownImage; + + private final static int[] COLORS = {0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444}; public ImageLoader(Context context) { this.context = context; @@ -71,7 +79,7 @@ public class ImageLoader { @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) { if(evicted) { - if(oldBitmap != nowPlaying) { + if(oldBitmap != nowPlaying && key.indexOf("unknown") == -1) { if(sizeOf("", oldBitmap) > 500) { oldBitmap.recycle(); } @@ -87,8 +95,6 @@ public class ImageLoader { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); imageSizeLarge = Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); avatarSizeDefault = context.getResources().getDrawable(R.drawable.ic_social_person).getIntrinsicHeight(); - - createLargeUnknownImage(context); } public void clearCache() { @@ -96,18 +102,74 @@ public class ImageLoader { cache.evictAll(); } - private void createLargeUnknownImage(Context context) { - BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); - Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true); - largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap); + private Bitmap getUnknownImage(MusicDirectory.Entry entry, int size) { + String key; + int color; + if(entry == null) { + key = getKey("unknown", size); + color = COLORS[0]; + + return getUnknownImage(key, size, color, null, null); + } else { + key = getKey(entry.getId() + "unknown", size); + + String hash; + if(entry.getAlbum() != null) { + hash = entry.getAlbum(); + } else if(entry.getArtist() != null) { + hash = entry.getArtist(); + } else { + hash = entry.getId(); + } + color = COLORS[Math.abs(hash.hashCode()) % COLORS.length]; + + return getUnknownImage(key, size, color, entry.getAlbum(), entry.getArtist()); + } + } + private Bitmap getUnknownImage(String key, int size, int color, String topText, String bottomText) { + Bitmap bitmap = cache.get(key); + if(bitmap == null) { + bitmap = createUnknownImage(size, color, topText, bottomText); + cache.put(key, bitmap); + } + + return bitmap; + } + private Bitmap createUnknownImage(int size, int primaryColor, String topText, String bottomText) { + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint color = new Paint(); + color.setColor(primaryColor); + canvas.drawRect(0, 0, size, size * 2.0f / 3.0f, color); + + color.setShader(new LinearGradient(0, 0, 0, size / 3.0f, Color.rgb(82, 82, 82), Color.BLACK, Shader.TileMode.MIRROR)); + canvas.drawRect(0, size * 2.0f / 3.0f, size, size, color); + + if(topText != null || bottomText != null) { + Paint font = new Paint(); + font.setFlags(Paint.ANTI_ALIAS_FLAG); + font.setColor(Color.WHITE); + font.setTextSize(3.0f + size * 0.07f); + + if(topText != null) { + canvas.drawText(topText, size * 0.05f, size * 0.6f, font); + } + + if(bottomText != null) { + canvas.drawText(bottomText, size * 0.05f, size * 0.8f, font); + } + } + + return bitmap; } public Bitmap getCachedImage(Context context, MusicDirectory.Entry entry, boolean large) { + int size = large ? imageSizeLarge : imageSizeDefault; if(entry == null || entry.getCoverArt() == null) { - return null; + return getUnknownImage(entry, size); } - int size = large ? imageSizeLarge : imageSizeDefault; Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size)); if(bitmap == null || bitmap.isRecycled()) { bitmap = FileUtil.getAlbumArtBitmap(context, entry, size); @@ -120,10 +182,6 @@ public class ImageLoader { } public ImageTask loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) { - if (largeUnknownImage != null && ((BitmapDrawable)largeUnknownImage).getBitmap().isRecycled()) { - createLargeUnknownImage(view.getContext()); - } - if(entry != null && entry.getCoverArt() == null && entry.isDirectory() && !Util.isOffline(context)) { // Try to lookup child cover art MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, entry, true); @@ -131,13 +189,16 @@ public class ImageLoader { entry.setCoverArt(firstChild.getCoverArt()); } } + + Bitmap bitmap; + int size = large ? imageSizeLarge : imageSizeDefault; if (entry == null || entry.getCoverArt() == null) { - setUnknownImage(view, large); + bitmap = getUnknownImage(entry, size); + setImage(view, Util.createDrawableFromBitmap(context, bitmap), crossfade); return null; } - int size = large ? imageSizeLarge : imageSizeDefault; - Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size)); + bitmap = cache.get(getKey(entry.getCoverArt(), size)); if (bitmap != null && !bitmap.isRecycled()) { final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); setImage(view, drawable, crossfade); @@ -148,31 +209,52 @@ public class ImageLoader { } if (!large) { - setUnknownImage(view, large); + setImage(view, Util.createDrawableFromBitmap(context, null), false); } ImageTask task = new ViewImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade); task.execute(); return task; } - public SilentBackgroundTask<Void> loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) { - if (largeUnknownImage != null && ((BitmapDrawable)largeUnknownImage).getBitmap().isRecycled()) { - createLargeUnknownImage(context); + public SilentBackgroundTask<Void> loadImage(View view, String url, boolean large) { + Bitmap bitmap; + int size = large ? imageSizeLarge : imageSizeDefault; + if (url == null) { + String key = getKey(url + "unknown", size); + int color = COLORS[Math.abs(key.hashCode()) % COLORS.length]; + bitmap = getUnknownImage(key, size, color, null, null); + setImage(view, Util.createDrawableFromBitmap(context, bitmap), true); + return null; } + bitmap = cache.get(getKey(url, size)); + if (bitmap != null && !bitmap.isRecycled()) { + final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); + setImage(view, drawable, true); + return null; + } + + SilentBackgroundTask<Void> task = new ViewUrlTask(view.getContext(), view, url, size); + task.execute(); + return task; + } + + public SilentBackgroundTask<Void> loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) { + Bitmap bitmap; if (entry == null || entry.getCoverArt() == null) { - setUnknownImage(remoteControl); + bitmap = getUnknownImage(entry, imageSizeLarge); + setImage(remoteControl, Util.createDrawableFromBitmap(context, bitmap)); return null; } - Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge)); + bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge)); if (bitmap != null && !bitmap.isRecycled()) { Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); setImage(remoteControl, drawable); return null; } - setUnknownImage(remoteControl); + setImage(remoteControl, Util.createDrawableFromBitmap(context, null)); ImageTask task = new RemoteControlClientImageTask(context, entry, imageSizeLarge, imageSizeLarge, false, remoteControl); task.execute(); return task; @@ -239,10 +321,11 @@ public class ImageLoader { } } + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void setImage(RemoteControlClient remoteControl, Drawable drawable) { if(remoteControl != null && drawable != null) { Bitmap origBitmap = ((BitmapDrawable)drawable).getBitmap(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && origBitmap != null) { origBitmap = origBitmap.copy(origBitmap.getConfig(), false); } if ( origBitmap != null && !origBitmap.isRecycled()) { @@ -256,22 +339,6 @@ public class ImageLoader { } } - private void setUnknownImage(View view, boolean large) { - if (large) { - setImage(view, largeUnknownImage, false); - } else { - if (view instanceof TextView) { - ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); - } else if (view instanceof ImageView) { - ((ImageView) view).setImageResource(R.drawable.unknown_album); - } - } - } - - private void setUnknownImage(RemoteControlClient remoteControl) { - setImage(remoteControl, largeUnknownImage); - } - public abstract class ImageTask extends SilentBackgroundTask<Void> { private final Context mContext; private final MusicDirectory.Entry mEntry; @@ -344,6 +411,50 @@ public class ImageLoader { } } + private class ViewUrlTask extends SilentBackgroundTask<Void> { + private final Context mContext; + private final String mUrl; + private final ImageView mView; + private Drawable mDrawable; + private int mSize; + + public ViewUrlTask(Context context, View view, String url, int size) { + super(context); + mContext = context; + mView = (ImageView) view; + mUrl = url; + mSize = size; + } + + @Override + protected Void doInBackground() throws Throwable { + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + Bitmap bitmap = musicService.getBitmap(mUrl, mSize, mContext, null, this); + if(bitmap != null) { + String key = getKey(mUrl, mSize); + cache.put(key, bitmap); + // Make sure key is the most recently "used" + cache.get(key); + + mDrawable = Util.createDrawableFromBitmap(mContext, bitmap); + } + } catch (Throwable x) { + Log.e(TAG, "Failed to download from url " + mUrl, x); + cancelled.set(true); + } + + return null; + } + + @Override + protected void done(Void result) { + if(mDrawable != null) { + mView.setImageDrawable(mDrawable); + } + } + } + private class AvatarTask extends SilentBackgroundTask<Void> { private final Context mContext; private final String mUsername; diff --git a/src/github/daneren2005/dsub/util/MediaRouteManager.java b/src/github/daneren2005/dsub/util/MediaRouteManager.java index 11e0d387..2d0c2a87 100644 --- a/src/github/daneren2005/dsub/util/MediaRouteManager.java +++ b/src/github/daneren2005/dsub/util/MediaRouteManager.java @@ -15,6 +15,7 @@ package github.daneren2005.dsub.util; +import android.os.Build; import android.support.v7.media.MediaRouteProvider; import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; @@ -147,10 +148,12 @@ public class MediaRouteManager extends MediaRouter.Callback { providers.add(jukeboxProvider); offlineProviders.add(jukeboxProvider); - DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService); - router.addProvider(dlnaProvider); - providers.add(dlnaProvider); - offlineProviders.add(dlnaProvider); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService); + router.addProvider(dlnaProvider); + providers.add(dlnaProvider); + offlineProviders.add(dlnaProvider); + } } public void removeOnlineProviders() { for(MediaRouteProvider provider: offlineProviders) { @@ -171,7 +174,9 @@ public class MediaRouteManager extends MediaRouter.Callback { if(castAvailable) { builder.addControlCategory(CastCompat.getCastControlCategory()); } - builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA); + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA); + } selector = builder.build(); } } diff --git a/src/github/daneren2005/dsub/util/Notifications.java b/src/github/daneren2005/dsub/util/Notifications.java index 520c4a6c..330e14ec 100644 --- a/src/github/daneren2005/dsub/util/Notifications.java +++ b/src/github/daneren2005/dsub/util/Notifications.java @@ -69,6 +69,9 @@ public final class Notifications { notification.bigContentView = expandedContentView;
notification.priority = Notification.PRIORITY_HIGH;
}
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ notification.visibility = Notification.VISIBILITY_PUBLIC;
+ }
RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification);
setupViews(smallContentView, context, song, false, playing, remote);
@@ -133,14 +136,6 @@ public final class Notifications { rv.setTextViewText(R.id.notification_artist, arist);
rv.setTextViewText(R.id.notification_album, album);
- Pair<Integer, Integer> colors = getNotificationTextColors(context);
- if (colors.getFirst() != null) {
- rv.setTextColor(R.id.notification_title, colors.getFirst());
- }
- if (colors.getSecond() != null) {
- rv.setTextColor(R.id.notification_artist, colors.getSecond());
- }
-
boolean persistent = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false);
if(persistent) {
if(expanded) {
@@ -236,7 +231,7 @@ public final class Notifications { String currentDownloading, currentSize;
if(file != null) {
currentDownloading = file.getSong().getTitle();
- currentSize = Util.formatBytes(file.getEstimatedSize());
+ currentSize = Util.formatLocalizedBytes(file.getEstimatedSize(), context);
} else {
currentDownloading = "none";
currentSize = "0";
@@ -341,44 +336,4 @@ public final class Notifications { notificationManager.notify(stringId, builder.build());
}
}
-
- /**
- * Resolves the default text color for notifications.
- *
- * Based on http://stackoverflow.com/questions/4867338/custom-notification-layouts-and-text-colors/7320604#7320604
- */
- private static Pair<Integer, Integer> getNotificationTextColors(Context context) {
- if (NOTIFICATION_TEXT_COLORS.getFirst() == null && NOTIFICATION_TEXT_COLORS.getSecond() == null) {
- try {
- Notification notification = new Notification();
- String title = "title";
- String content = "content";
- notification.setLatestEventInfo(context, title, content, null);
- LinearLayout group = new LinearLayout(context);
- ViewGroup event = (ViewGroup) notification.contentView.apply(context, group);
- findNotificationTextColors(event, title, content);
- group.removeAllViews();
- } catch (Exception x) {
- Log.w(TAG, "Failed to resolve notification text colors.", x);
- }
- }
- return NOTIFICATION_TEXT_COLORS;
- }
-
- private static void findNotificationTextColors(ViewGroup group, String title, String content) {
- for (int i = 0; i < group.getChildCount(); i++) {
- if (group.getChildAt(i) instanceof TextView) {
- TextView textView = (TextView) group.getChildAt(i);
- String text = textView.getText().toString();
- if (title.equals(text)) {
- NOTIFICATION_TEXT_COLORS.setFirst(textView.getTextColors().getDefaultColor());
- }
- else if (content.equals(text)) {
- NOTIFICATION_TEXT_COLORS.setSecond(textView.getTextColors().getDefaultColor());
- }
- }
- else if (group.getChildAt(i) instanceof ViewGroup)
- findNotificationTextColors((ViewGroup) group.getChildAt(i), title, content);
- }
- }
}
diff --git a/src/github/daneren2005/dsub/util/Util.java b/src/github/daneren2005/dsub/util/Util.java index 0b3f03b3..83bedfc8 100644 --- a/src/github/daneren2005/dsub/util/Util.java +++ b/src/github/daneren2005/dsub/util/Util.java @@ -20,9 +20,6 @@ package github.daneren2005.dsub.util; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -42,33 +39,19 @@ import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Environment; -import android.os.Handler; -import android.support.v4.app.NotificationCompat; import android.text.Html; import android.text.SpannableString; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.Log; import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.KeyEvent; -import android.widget.LinearLayout; -import android.widget.RemoteViews; import android.widget.TextView; import android.widget.Toast; import github.daneren2005.dsub.R; -import github.daneren2005.dsub.activity.SubsonicActivity; -import github.daneren2005.dsub.activity.SubsonicFragmentActivity; import github.daneren2005.dsub.domain.MusicDirectory; import github.daneren2005.dsub.domain.PlayerState; import github.daneren2005.dsub.domain.RepeatMode; -import github.daneren2005.dsub.domain.ServerInfo; -import github.daneren2005.dsub.domain.User; -import github.daneren2005.dsub.domain.Version; -import github.daneren2005.dsub.provider.DSubWidgetProvider; import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; -import github.daneren2005.dsub.service.DownloadFile; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.MediaStoreService; @@ -77,8 +60,6 @@ import org.apache.http.HttpEntity; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -90,8 +71,6 @@ import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Arrays; import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * @author Sindre Mehus @@ -378,6 +357,22 @@ public final class Util { return builder.toString(); } + public static String replaceInternalUrl(Context context, String url) { + // Only change to internal when using https + if(url.indexOf("https") != -1) { + SharedPreferences prefs = Util.getPreferences(context); + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null); + if(internalUrl != null && !"".equals(internalUrl)) { + String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + url = url.replace(internalUrl, externalUrl); + } + } + + // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC + return url.replace(Constants.REST_CLIENT_ID, Constants.CHROMECAST_CLIENT_ID); + } + public static boolean isTagBrowsing(Context context) { return isTagBrowsing(context, Util.getActiveServer(context)); } @@ -606,6 +601,26 @@ public final class Util { } return true; } + public static boolean recursiveDelete(File dir) { + if (dir != null && dir.exists()) { + File[] list = dir.listFiles(); + if(list != null) { + for(File file: list) { + if(file.isDirectory()) { + if(!recursiveDelete(file)) { + return false; + } + } else if(file.exists()) { + if(!file.delete()) { + return false; + } + } + } + } + return dir.delete(); + } + return false; + } public static boolean recursiveDelete(File dir, MediaStoreService mediaStore) { if (dir != null && dir.exists()) { File[] list = dir.listFiles(); @@ -892,6 +907,10 @@ public final class Util { throw new IllegalArgumentException("Strings must not be null"); } + if(t.toString().toLowerCase().indexOf(s.toString().toLowerCase()) != -1) { + return 1; + } + int n = s.length(); int m = t.length(); @@ -1037,10 +1056,13 @@ public final class Util { ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } public static void showHTMLDialog(Context context, int title, int message) { + showHTMLDialog(context, title, context.getResources().getString(message)); + } + public static void showHTMLDialog(Context context, int title, String message) { AlertDialog dialog = new AlertDialog.Builder(context) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(title) - .setMessage(Html.fromHtml(context.getResources().getString(message))) + .setMessage(Html.fromHtml(message)) .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { @@ -1048,6 +1070,8 @@ public final class Util { } }) .show(); + + ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); } public static void sleepQuietly(long millis) { diff --git a/src/github/daneren2005/dsub/view/HeaderGridView.java b/src/github/daneren2005/dsub/view/HeaderGridView.java new file mode 100644 index 00000000..3ae39c88 --- /dev/null +++ b/src/github/daneren2005/dsub/view/HeaderGridView.java @@ -0,0 +1,785 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package github.daneren2005.dsub.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * A {@link GridView} that supports adding header rows in a + * very similar way to {@link android.widget.ListView}. + * See {@link HeaderGridView#addHeaderView(View, Object, boolean)} + * See {@link HeaderGridView#addFooterView(View, Object, boolean)} + */ +public class HeaderGridView extends GridView { + + public static boolean DEBUG = false; + + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + private static class FixedViewInfo { + /** + * The view to add to the grid + */ + public View view; + public ViewGroup viewContainer; + /** + * The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. + */ + public Object data; + /** + * <code>true</code> if the fixed view should be selectable in the grid + */ + public boolean isSelectable; + } + + private int mNumColumns = AUTO_FIT; + private View mViewForMeasureRowHeight = null; + private int mRowHeight = -1; + private static final String LOG_TAG = "grid-view-with-header-and-footer"; + + private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>(); + private ArrayList<FixedViewInfo> mFooterViewInfos = new ArrayList<FixedViewInfo>(); + + private void initHeaderGridView() { + } + + public HeaderGridView(Context context) { + super(context); + initHeaderGridView(); + } + + public HeaderGridView(Context context, AttributeSet attrs) { + super(context, attrs); + initHeaderGridView(); + } + + public HeaderGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initHeaderGridView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter instanceof HeaderViewGridAdapter) { + ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumnsCompatible()); + ((HeaderViewGridAdapter) adapter).setRowHeight(getRowHeight()); + } + } + + @Override + public void setClipChildren(boolean clipChildren) { + // Ignore, since the header rows depend on not being clipped + } + + /** + * Do not call this method unless you know how it works. + * + * @param clipChildren + */ + public void setClipChildrenSupper(boolean clipChildren) { + super.setClipChildren(false); + } + + /** + * Add a fixed view to appear at the top of the grid. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + * <p/> + * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap + * the supplied cursor with one that will also account for header views. + * + * @param v The view to add. + */ + public void addHeaderView(View v) { + addHeaderView(v, null, true); + } + + /** + * Add a fixed view to appear at the top of the grid. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + * <p/> + * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap + * the supplied cursor with one that will also account for header views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable whether the item is selectable + */ + public void addHeaderView(View v, Object data, boolean isSelectable) { + ListAdapter adapter = getAdapter(); + if (adapter != null && !(adapter instanceof HeaderViewGridAdapter)) { + throw new IllegalStateException( + "Cannot add header view to grid -- setAdapter has already been called."); + } + + ViewGroup.LayoutParams lyp = v.getLayoutParams(); + + FixedViewInfo info = new FixedViewInfo(); + FrameLayout fl = new FullWidthFixedViewLayout(getContext()); + + if (lyp != null) { + v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); + fl.setLayoutParams(new AbsListView.LayoutParams(lyp.width, lyp.height)); + } + fl.addView(v); + info.view = v; + info.viewContainer = fl; + info.data = data; + info.isSelectable = isSelectable; + mHeaderViewInfos.add(info); + // in the case of re-adding a header view, or adding one later on, + // we need to notify the observer + if (adapter != null) { + ((HeaderViewGridAdapter) adapter).notifyDataSetChanged(); + } + } + + public void addFooterView(View v) { + addFooterView(v, null, true); + } + + public void addFooterView(View v, Object data, boolean isSelectable) { + ListAdapter mAdapter = getAdapter(); + if (mAdapter != null && !(mAdapter instanceof HeaderViewGridAdapter)) { + throw new IllegalStateException( + "Cannot add header view to grid -- setAdapter has already been called."); + } + + ViewGroup.LayoutParams lyp = v.getLayoutParams(); + + FixedViewInfo info = new FixedViewInfo(); + FrameLayout fl = new FullWidthFixedViewLayout(getContext()); + + if (lyp != null) { + v.setLayoutParams(new FrameLayout.LayoutParams(lyp.width, lyp.height)); + fl.setLayoutParams(new AbsListView.LayoutParams(lyp.width, lyp.height)); + } + fl.addView(v); + info.view = v; + info.viewContainer = fl; + info.data = data; + info.isSelectable = isSelectable; + mFooterViewInfos.add(info); + + if (mAdapter != null) { + ((HeaderViewGridAdapter) mAdapter).notifyDataSetChanged(); + } + } + + public int getHeaderViewCount() { + return mHeaderViewInfos.size(); + } + + public int getFooterViewCount() { + return mFooterViewInfos.size(); + } + + /** + * Removes a previously-added header view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeHeaderView(View v) { + if (mHeaderViewInfos.size() > 0) { + boolean result = false; + ListAdapter adapter = getAdapter(); + if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) { + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; + } + + /** + * Removes a previously-added footer view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeFooterView(View v) { + if (mFooterViewInfos.size() > 0) { + boolean result = false; + ListAdapter adapter = getAdapter(); + if (adapter != null && ((HeaderViewGridAdapter) adapter).removeFooter(v)) { + result = true; + } + removeFixedViewInfo(v, mFooterViewInfos); + return result; + } + return false; + } + + private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) { + int len = where.size(); + for (int i = 0; i < len; ++i) { + FixedViewInfo info = where.get(i); + if (info.view == v) { + where.remove(i); + break; + } + } + } + + @TargetApi(11) + private int getNumColumnsCompatible() { + if (Build.VERSION.SDK_INT >= 11) { + return super.getNumColumns(); + } else { + try { + Field numColumns = getClass().getSuperclass().getDeclaredField("mNumColumns"); + numColumns.setAccessible(true); + return numColumns.getInt(this); + } catch (Exception e) { + if (mNumColumns != -1) { + return mNumColumns; + } + throw new RuntimeException("Can not determine the mNumColumns for this API platform, please call setNumColumns to set it."); + } + } + } + + @TargetApi(16) + private int getColumnWidthCompatible() { + if (Build.VERSION.SDK_INT >= 16) { + return super.getColumnWidth(); + } else { + try { + Field numColumns = getClass().getSuperclass().getDeclaredField("mColumnWidth"); + numColumns.setAccessible(true); + return numColumns.getInt(this); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mViewForMeasureRowHeight = null; + } + + public void invalidateRowHeight() { + mRowHeight = -1; + } + + public int getRowHeight() { + if (mRowHeight > 0) { + return mRowHeight; + } + ListAdapter adapter = getAdapter(); + int numColumns = getNumColumnsCompatible(); + + // adapter has not been set or has no views in it; + if (adapter == null || adapter.getCount() <= numColumns * (mHeaderViewInfos.size() + mFooterViewInfos.size())) { + return -1; + } + int mColumnWidth = getColumnWidthCompatible(); + View view = getAdapter().getView(numColumns * mHeaderViewInfos.size(), mViewForMeasureRowHeight, this); + AbsListView.LayoutParams p = (AbsListView.LayoutParams) view.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(-1, -2, 0); + view.setLayoutParams(p); + } + int childHeightSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + int childWidthSpec = getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); + view.measure(childWidthSpec, childHeightSpec); + mViewForMeasureRowHeight = view; + mRowHeight = view.getMeasuredHeight(); + return mRowHeight; + } + + @TargetApi(11) + public void tryToScrollToBottomSmoothly() { + int lastPos = getAdapter().getCount() - 1; + if (Build.VERSION.SDK_INT >= 11) { + smoothScrollToPositionFromTop(lastPos, 0); + } else { + setSelection(lastPos); + } + } + + @TargetApi(11) + public void tryToScrollToBottomSmoothly(int duration) { + int lastPos = getAdapter().getCount() - 1; + if (Build.VERSION.SDK_INT >= 11) { + smoothScrollToPositionFromTop(lastPos, 0, duration); + } else { + setSelection(lastPos); + } + } + + @Override + public void setAdapter(ListAdapter adapter) { + if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) { + HeaderViewGridAdapter headerViewGridAdapter = new HeaderViewGridAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); + int numColumns = getNumColumnsCompatible(); + if (numColumns > 1) { + headerViewGridAdapter.setNumColumns(numColumns); + } + headerViewGridAdapter.setRowHeight(getRowHeight()); + super.setAdapter(headerViewGridAdapter); + } else { + super.setAdapter(adapter); + } + } + + /** + * full width + */ + private class FullWidthFixedViewLayout extends FrameLayout { + + public FullWidthFixedViewLayout(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int realLeft = HeaderGridView.this.getPaddingLeft() + getPaddingLeft(); + // Try to make where it should be, from left, full width + if (realLeft != left) { + offsetLeftAndRight(realLeft - left); + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int targetWidth = HeaderGridView.this.getMeasuredWidth() + - HeaderGridView.this.getPaddingLeft() + - HeaderGridView.this.getPaddingRight(); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, + MeasureSpec.getMode(widthMeasureSpec)); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + public void setNumColumns(int numColumns) { + super.setNumColumns(numColumns); + mNumColumns = numColumns; + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter instanceof HeaderViewGridAdapter) { + ((HeaderViewGridAdapter) adapter).setNumColumns(numColumns); + } + } + + /** + * ListAdapter used when a HeaderGridView has header views. This ListAdapter + * wraps another one and also keeps track of the header views and their + * associated data objects. + * <p>This is intended as a base class; you will probably not need to + * use this class directly in your own code. + */ + private static class HeaderViewGridAdapter extends BaseAdapter implements WrapperListAdapter, Filterable { + // This is used to notify the container of updates relating to number of columns + // or headers changing, which changes the number of placeholders needed + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + private final ListAdapter mAdapter; + static final ArrayList<FixedViewInfo> EMPTY_INFO_LIST = + new ArrayList<FixedViewInfo>(); + + // This ArrayList is assumed to NOT be null. + ArrayList<FixedViewInfo> mHeaderViewInfos; + ArrayList<FixedViewInfo> mFooterViewInfos; + private int mNumColumns = 1; + private int mRowHeight = -1; + boolean mAreAllFixedViewsSelectable; + private final boolean mIsFilterable; + private boolean mCachePlaceHoldView = true; + // From Recycle Bin or calling getView, this a question... + private boolean mCacheFirstHeaderView = false; + + public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ArrayList<FixedViewInfo> footViewInfos, ListAdapter adapter) { + mAdapter = adapter; + mIsFilterable = adapter instanceof Filterable; + if (headerViewInfos == null) { + mHeaderViewInfos = EMPTY_INFO_LIST; + } else { + mHeaderViewInfos = headerViewInfos; + } + + if (footViewInfos == null) { + mFooterViewInfos = EMPTY_INFO_LIST; + } else { + mFooterViewInfos = footViewInfos; + } + mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public void setNumColumns(int numColumns) { + if (numColumns < 1) { + return; + } + if (mNumColumns != numColumns) { + mNumColumns = numColumns; + notifyDataSetChanged(); + } + } + + public void setRowHeight(int height) { + mRowHeight = height; + } + + public int getHeadersCount() { + return mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos.size(); + } + + @Override + public boolean isEmpty() { + return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0 && getFootersCount() == 0; + } + + private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) { + if (infos != null) { + for (FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + FixedViewInfo info = mHeaderViewInfos.get(i); + if (info.view == v) { + mHeaderViewInfos.remove(i); + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); + mDataSetObservable.notifyChanged(); + return true; + } + } + return false; + } + + public boolean removeFooter(View v) { + for (int i = 0; i < mFooterViewInfos.size(); i++) { + FixedViewInfo info = mFooterViewInfos.get(i); + if (info.view == v) { + mFooterViewInfos.remove(i); + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) && areAllListInfosSelectable(mFooterViewInfos); + mDataSetObservable.notifyChanged(); + return true; + } + } + return false; + } + + @Override + public int getCount() { + if (mAdapter != null) { + return (getFootersCount() + getHeadersCount()) * mNumColumns + getAdapterAndPlaceHolderCount(); + } else { + return (getFootersCount() + getHeadersCount()) * mNumColumns; + } + } + + @Override + public boolean areAllItemsEnabled() { + if (mAdapter != null) { + return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); + } else { + return true; + } + } + + private int getAdapterAndPlaceHolderCount() { + final int adapterCount = (int) (Math.ceil(1f * mAdapter.getCount() / mNumColumns) * mNumColumns); + return adapterCount; + } + + @Override + public boolean isEnabled(int position) { + // Header (negative positions will throw an IndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + return position % mNumColumns == 0 + && mHeaderViewInfos.get(position / mNumColumns).isSelectable; + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + return adjPosition < mAdapter.getCount() && mAdapter.isEnabled(adjPosition); + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + final int footerPosition = adjPosition - adapterCount; + return footerPosition % mNumColumns == 0 + && mFooterViewInfos.get(footerPosition / mNumColumns).isSelectable; + } + + @Override + public Object getItem(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + if (position % mNumColumns == 0) { + return mHeaderViewInfos.get(position / mNumColumns).data; + } + return null; + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + return mAdapter.getItem(adjPosition); + } else { + return null; + } + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + final int footerPosition = adjPosition - adapterCount; + if (footerPosition % mNumColumns == 0) { + return mFooterViewInfos.get(footerPosition).data; + } else { + return null; + } + } + + @Override + public long getItemId(int position) { + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (mAdapter != null && position >= numHeadersAndPlaceholders) { + int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemId(adjPosition); + } + } + return -1; + } + + @Override + public boolean hasStableIds() { + if (mAdapter != null) { + return mAdapter.hasStableIds(); + } + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (DEBUG) { + Log.d(LOG_TAG, String.format("getView: %s, reused: %s", position, convertView == null)); + } + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + if (position < numHeadersAndPlaceholders) { + View headerViewContainer = mHeaderViewInfos + .get(position / mNumColumns).viewContainer; + if (position % mNumColumns == 0) { + return headerViewContainer; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + // We need to do this because GridView uses the height of the last item + // in a row to determine the height for the entire row. + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(headerViewContainer.getHeight()); + return convertView; + } + } + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + View view = mAdapter.getView(adjPosition, convertView, parent); + return view; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(mRowHeight); + return convertView; + } + } + } + // Footer + final int footerPosition = adjPosition - adapterCount; + if (footerPosition < getCount()) { + View footViewContainer = mFooterViewInfos + .get(footerPosition / mNumColumns).viewContainer; + if (position % mNumColumns == 0) { + return footViewContainer; + } else { + if (convertView == null) { + convertView = new View(parent.getContext()); + } + // We need to do this because GridView uses the height of the last item + // in a row to determine the height for the entire row. + convertView.setVisibility(View.INVISIBLE); + convertView.setMinimumHeight(footViewContainer.getHeight()); + return convertView; + } + } + throw new ArrayIndexOutOfBoundsException(position); + } + + @Override + public int getItemViewType(int position) { + + final int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns; + final int adapterViewTypeStart = mAdapter == null ? 0 : mAdapter.getViewTypeCount() - 1; + int type = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + if (mCachePlaceHoldView) { + // Header + if (position < numHeadersAndPlaceholders) { + if (position == 0) { + if (mCacheFirstHeaderView) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + mFooterViewInfos.size() + 1 + 1; + } + } + if (position % mNumColumns != 0) { + type = adapterViewTypeStart + (position / mNumColumns + 1); + } + } + } + + // Adapter + final int adjPosition = position - numHeadersAndPlaceholders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = getAdapterAndPlaceHolderCount(); + if (adjPosition >= 0 && adjPosition < adapterCount) { + if (adjPosition < mAdapter.getCount()) { + type = mAdapter.getItemViewType(adjPosition); + } else { + if (mCachePlaceHoldView) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + 1; + } + } + } + } + + if (mCachePlaceHoldView) { + // Footer + final int footerPosition = adjPosition - adapterCount; + if (footerPosition >= 0 && footerPosition < getCount() && (footerPosition % mNumColumns) != 0) { + type = adapterViewTypeStart + mHeaderViewInfos.size() + 1 + (footerPosition / mNumColumns + 1); + } + } + if (DEBUG) { + Log.d(LOG_TAG, String.format("getItemViewType: pos: %s, result: %s", position, type, mCachePlaceHoldView, mCacheFirstHeaderView)); + } + return type; + } + + /** + * content view, content view holder, header[0], header and footer placeholder(s) + * + * @return + */ + @Override + public int getViewTypeCount() { + int count = mAdapter == null ? 1 : mAdapter.getViewTypeCount(); + if (mCachePlaceHoldView) { + int offset = mHeaderViewInfos.size() + 1 + mFooterViewInfos.size(); + if (mCacheFirstHeaderView) { + offset += 1; + } + count += offset; + } + if (DEBUG) { + Log.d(LOG_TAG, String.format("getViewTypeCount: %s", count)); + } + return count; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + @Override + public Filter getFilter() { + if (mIsFilterable) { + return ((Filterable) mAdapter).getFilter(); + } + return null; + } + + @Override + public ListAdapter getWrappedAdapter() { + return mAdapter; + } + + public void notifyDataSetChanged() { + mDataSetObservable.notifyChanged(); + } + } +} diff --git a/src/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java b/src/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java new file mode 100644 index 00000000..20281a28 --- /dev/null +++ b/src/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java @@ -0,0 +1,34 @@ +package github.daneren2005.dsub.view; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Layout; +import android.text.style.LeadingMarginSpan; + +/** + * Created by Scott on 1/13/2015. + */ +public class MyLeadingMarginSpan2 implements LeadingMarginSpan.LeadingMarginSpan2 { + private int margin; + private int lines; + + public MyLeadingMarginSpan2(int lines, int margin) { + this.margin = margin; + this.lines = lines; + } + + @Override + public int getLeadingMargin(boolean first) { + return first ? margin : 0; + } + + @Override + public int getLeadingMarginLineCount() { + return lines; + } + + @Override + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, + int top, int baseline, int bottom, CharSequence text, + int start, int end, boolean first, Layout layout) {} +} diff --git a/src/github/daneren2005/dsub/view/SongView.java b/src/github/daneren2005/dsub/view/SongView.java index 630f747f..2fbaedc3 100644 --- a/src/github/daneren2005/dsub/view/SongView.java +++ b/src/github/daneren2005/dsub/view/SongView.java @@ -231,7 +231,9 @@ public class SongView extends UpdateView implements Checkable { } if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFileExists) { - statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext())); + double percentage = (partialFile.length() * 100.0) / downloadFile.getEstimatedSize(); + percentage = Math.min(percentage, 100); + statusTextView.setText((int)percentage + " %"); if(!rightImage) { statusImageView.setVisibility(View.VISIBLE); rightImage = true; |