aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--AndroidManifest.xml6
-rw-r--r--libs/cling-core-1.0.5.jarbin551922 -> 0 bytes
-rw-r--r--libs/cling-core-2.0.1.jarbin0 -> 686501 bytes
-rw-r--r--libs/cling-support-1.0.5.jarbin359154 -> 0 bytes
-rw-r--r--libs/cling-support-2.0.1.jarbin0 -> 490043 bytes
-rw-r--r--libs/javax.servlet-3.0.0.v201112011016.jarbin0 -> 200387 bytes
-rw-r--r--libs/jetty-all-8.1.16.v20140903.jarbin0 -> 1880786 bytes
-rw-r--r--libs/seamless-http-1.1.0.jarbin0 -> 21646 bytes
-rw-r--r--libs/seamless-util-1.1.0.jarbin0 -> 94456 bytes
-rw-r--r--libs/seamless-xml-1.1.0.jarbin0 -> 63142 bytes
-rw-r--r--libs/teleal-common-1.0.13.jarbin231831 -> 0 bytes
-rw-r--r--proguard.cfg29
-rw-r--r--project.properties2
-rw-r--r--res/drawable-hdpi/btn_check_buttonless_off.pngbin762 -> 0 bytes
-rw-r--r--res/drawable-hdpi/btn_check_buttonless_on.pngbin2996 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_drawer.pngbin113 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_drawer.pngbin104 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_drawer.pngbin120 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_drawer.pngbin202 -> 0 bytes
-rw-r--r--res/drawable/btn_check.xml28
-rw-r--r--res/layout/abstract_fragment_activity.xml3
-rw-r--r--res/layout/album_cell_item.xml3
-rw-r--r--res/layout/album_list_item.xml6
-rw-r--r--res/layout/basic_list_item.xml3
-rw-r--r--res/layout/complex_list_item.xml3
-rw-r--r--res/layout/grid_view.xml2
-rw-r--r--res/layout/preferences.xml10
-rw-r--r--res/layout/select_album_header.xml2
-rw-r--r--res/layout/song_list_item.xml5
-rw-r--r--res/layout/user_list_item.xml3
-rw-r--r--res/menu/select_album.xml4
-rw-r--r--res/menu/select_album_list.xml19
-rw-r--r--res/menu/similar_artists.xml8
-rw-r--r--res/values-es/strings.xml2
-rw-r--r--res/values-hu/strings.xml5
-rw-r--r--res/values-ru/strings.xml2
-rw-r--r--res/values/strings.xml4
-rw-r--r--res/values/styles.xml4
-rw-r--r--res/values/themes.xml50
-rw-r--r--res/xml/changelog.xml40
-rw-r--r--res/xml/settings.xml12
-rw-r--r--src/github/daneren2005/dsub/activity/SettingsActivity.java661
-rw-r--r--src/github/daneren2005/dsub/activity/SubsonicActivity.java25
-rw-r--r--src/github/daneren2005/dsub/activity/SubsonicFragmentActivity.java12
-rw-r--r--src/github/daneren2005/dsub/domain/ArtistInfo.java76
-rw-r--r--src/github/daneren2005/dsub/domain/DLNADevice.java6
-rw-r--r--src/github/daneren2005/dsub/domain/Version.java2
-rw-r--r--src/github/daneren2005/dsub/fragments/MainFragment.java8
-rw-r--r--src/github/daneren2005/dsub/fragments/NowPlayingFragment.java26
-rw-r--r--src/github/daneren2005/dsub/fragments/PreferenceCompatFragment.java313
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java371
-rw-r--r--src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java2
-rw-r--r--src/github/daneren2005/dsub/fragments/SettingsFragment.java711
-rw-r--r--src/github/daneren2005/dsub/fragments/SimilarArtistFragment.java135
-rw-r--r--src/github/daneren2005/dsub/fragments/SubsonicFragment.java166
-rw-r--r--src/github/daneren2005/dsub/provider/DLNARouteProvider.java205
-rw-r--r--src/github/daneren2005/dsub/provider/DSubSearchProvider.java11
-rw-r--r--src/github/daneren2005/dsub/service/CachedMusicService.java22
-rw-r--r--src/github/daneren2005/dsub/service/ChromeCastController.java20
-rw-r--r--src/github/daneren2005/dsub/service/DLNAController.java389
-rw-r--r--src/github/daneren2005/dsub/service/DownloadService.java133
-rw-r--r--src/github/daneren2005/dsub/service/MusicService.java5
-rw-r--r--src/github/daneren2005/dsub/service/OfflineMusicService.java15
-rw-r--r--src/github/daneren2005/dsub/service/RESTMusicService.java78
-rw-r--r--src/github/daneren2005/dsub/service/RemoteController.java3
-rw-r--r--src/github/daneren2005/dsub/service/parser/ArtistInfoParser.java82
-rw-r--r--src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java49
-rw-r--r--src/github/daneren2005/dsub/util/FileUtil.java50
-rw-r--r--src/github/daneren2005/dsub/util/ImageLoader.java193
-rw-r--r--src/github/daneren2005/dsub/util/MediaRouteManager.java15
-rw-r--r--src/github/daneren2005/dsub/util/Notifications.java53
-rw-r--r--src/github/daneren2005/dsub/util/Util.java68
-rw-r--r--src/github/daneren2005/dsub/view/HeaderGridView.java785
-rw-r--r--src/github/daneren2005/dsub/view/MyLeadingMarginSpan2.java34
-rw-r--r--src/github/daneren2005/dsub/view/SongView.java4
76 files changed, 3767 insertions, 1220 deletions
diff --git a/.gitignore b/.gitignore
index f9cfb1a4..ae198bf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
deleted file mode 100644
index 8079f329..00000000
--- a/libs/cling-core-1.0.5.jar
+++ /dev/null
Binary files differ
diff --git a/libs/cling-core-2.0.1.jar b/libs/cling-core-2.0.1.jar
new file mode 100644
index 00000000..632d3038
--- /dev/null
+++ b/libs/cling-core-2.0.1.jar
Binary files differ
diff --git a/libs/cling-support-1.0.5.jar b/libs/cling-support-1.0.5.jar
deleted file mode 100644
index a0ca6363..00000000
--- a/libs/cling-support-1.0.5.jar
+++ /dev/null
Binary files differ
diff --git a/libs/cling-support-2.0.1.jar b/libs/cling-support-2.0.1.jar
new file mode 100644
index 00000000..7fa28604
--- /dev/null
+++ b/libs/cling-support-2.0.1.jar
Binary files differ
diff --git a/libs/javax.servlet-3.0.0.v201112011016.jar b/libs/javax.servlet-3.0.0.v201112011016.jar
new file mode 100644
index 00000000..b1354096
--- /dev/null
+++ b/libs/javax.servlet-3.0.0.v201112011016.jar
Binary files differ
diff --git a/libs/jetty-all-8.1.16.v20140903.jar b/libs/jetty-all-8.1.16.v20140903.jar
new file mode 100644
index 00000000..25b1d324
--- /dev/null
+++ b/libs/jetty-all-8.1.16.v20140903.jar
Binary files differ
diff --git a/libs/seamless-http-1.1.0.jar b/libs/seamless-http-1.1.0.jar
new file mode 100644
index 00000000..98ec884a
--- /dev/null
+++ b/libs/seamless-http-1.1.0.jar
Binary files differ
diff --git a/libs/seamless-util-1.1.0.jar b/libs/seamless-util-1.1.0.jar
new file mode 100644
index 00000000..12026b7f
--- /dev/null
+++ b/libs/seamless-util-1.1.0.jar
Binary files differ
diff --git a/libs/seamless-xml-1.1.0.jar b/libs/seamless-xml-1.1.0.jar
new file mode 100644
index 00000000..1e740877
--- /dev/null
+++ b/libs/seamless-xml-1.1.0.jar
Binary files differ
diff --git a/libs/teleal-common-1.0.13.jar b/libs/teleal-common-1.0.13.jar
deleted file mode 100644
index 2d6403ef..00000000
--- a/libs/teleal-common-1.0.13.jar
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index d705b420..00000000
--- a/res/drawable-hdpi/btn_check_buttonless_off.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/btn_check_buttonless_on.png b/res/drawable-hdpi/btn_check_buttonless_on.png
deleted file mode 100644
index a2612d7d..00000000
--- a/res/drawable-hdpi/btn_check_buttonless_on.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_drawer.png b/res/drawable-hdpi/ic_drawer.png
deleted file mode 100644
index eb90af58..00000000
--- a/res/drawable-hdpi/ic_drawer.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_drawer.png b/res/drawable-mdpi/ic_drawer.png
deleted file mode 100644
index 1681d12c..00000000
--- a/res/drawable-mdpi/ic_drawer.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer.png b/res/drawable-xhdpi/ic_drawer.png
deleted file mode 100644
index daba1451..00000000
--- a/res/drawable-xhdpi/ic_drawer.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drawer.png b/res/drawable-xxhdpi/ic_drawer.png
deleted file mode 100644
index 9c4685d6..00000000
--- a/res/drawable-xxhdpi/ic_drawer.png
+++ /dev/null
Binary files differ
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 &gt; 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>Настройки &gt; Проигрыватели</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>&lt;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/"&gt;&lt;item id="/external/audio/albums/484/39883" parentID="/external/audio/albums/484" restricted="1"&gt;&lt;upnp:class&gt;object.item.audioItem.musicTrack&lt;/upnp:class&gt;&lt;dc:title&gt;03-Miss Murder.complete&lt;/dc:title&gt;&lt;dc:creator&gt;AFI&lt;/dc:creator&gt;&lt;upnp:artist&gt;AFI&lt;/upnp:artist&gt;&lt;upnp:albumArtURI&gt;http://192.168.1.3:57645/external/audio/albums/484.jpg&lt;/upnp:albumArtURI&gt;&lt;upnp:genre&gt;Rock&lt;/upnp:genre&gt;&lt;dc:date&gt;2006-01-01&lt;/dc:date&gt;&lt;upnp:album&gt;&amp;lt;unknown&amp;gt;&lt;/upnp:album&gt;&lt;upnp:originalTrackNumber&gt;3&lt;/upnp:originalTrackNumber&gt;&lt;res protocolInfo="http-get:*:audio/mpeg:*" bitrate="24000" size="4961736" duration="0:03:26.000"&gt;http://192.168.1.3:57645/external/audio/media/39883.mp3&lt;/res&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</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;