diff options
363 files changed, 29761 insertions, 0 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 00000000..5eabe457 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="github.daneren2005.dsub"
+ android:installLocation="internalOnly"
+ android:versionCode="59"
+ android:versionName="4.1.2">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
+ <uses-permission android:name="android.permission.READ_LOGS"/>
+
+ <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
+ <uses-feature android:name="android.hardware.microphone" android:required="false" />
+
+ <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
+
+ <supports-screens android:anyDensity="true" android:xlargeScreens="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/>
+
+ <application android:label="@string/common.appname"
+ android:backupAgent="github.daneren2005.dsub.util.SettingsBackupAgent"
+ android:icon="@drawable/launch2"
+ android:theme="@style/Theme.DSub.Holo">
+
+ <activity android:name="github.daneren2005.dsub.activity.MainActivity"
+ android:label="DSub"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="standard">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity android:name="github.daneren2005.dsub.activity.SearchActivity"
+ android:label="@string/search.label"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.DownloadActivity"
+ android:configChanges="keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.SettingsActivity"
+ android:theme="@style/Theme.DSub.Dark"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.HelpActivity"
+ android:label="@string/help.label"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.EqualizerActivity"
+ android:label="@string/equalizer.label"
+ android:configChanges="orientation|keyboardHidden"
+ android:launchMode="singleTask"/>
+
+ <activity android:name="github.daneren2005.dsub.activity.VoiceQueryReceiverActivity"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="github.daneren2005.dsub.activity.QueryReceiverActivity"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <action android:name="android.intent.action.SEARCH"/>
+ </intent-filter>
+ <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
+ </activity>
+
+ <service android:name="github.daneren2005.dsub.service.DownloadServiceImpl"
+ android:label="Subsonic Download Service"/>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.MediaButtonIntentReceiver">
+ <intent-filter android:priority="999">
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.BluetoothIntentReceiver">
+ <intent-filter>
+ <action android:name="android.bluetooth.a2dp.action.SINK_STATE_CHANGED"/>
+ <action android:name="android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED"/> <!-- API Level 11 -->
+ <action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
+ <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="github.daneren2005.dsub.receiver.A2dpIntentReceiver">
+ <intent-filter>
+ <action android:name="android.music.playstatusrequest"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x1"
+ android:label="@string/widget.4x1">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x1"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x2"
+ android:label="@string/widget.4x2">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x2"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x3"
+ android:label="@string/widget.4x3">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x3"/>
+ </receiver>
+ <receiver
+ android:name="github.daneren2005.dsub.provider.DSubWidget4x4"
+ android:label="@string/widget.4x4">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget4x4"/>
+ </receiver>
+
+ <provider android:name="github.daneren2005.dsub.provider.DSubSearchProvider"
+ android:authorities="github.daneren2005.dsub.provider.DSubSearchProvider"/>
+
+ <meta-data android:name="android.app.default_searchable"
+ android:value="github.daneren2005.dsub.activity.QueryReceiverActivity"/>
+
+ <meta-data android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAIUhOMtwa_eG-f0oYUHnetl_Cz7cO9zae8ZXOK5w"/>
+
+ </application>
+
+</manifest>
diff --git a/Subsonic.iml b/Subsonic.iml new file mode 100644 index 00000000..f770f3fc --- /dev/null +++ b/Subsonic.iml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android" name="Android">
+ <configuration />
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="libs1" level="project" />
+ <orderEntry type="module" module-name="ActionBarSherlock" />
+ <orderEntry type="module" module-name="DragSortListView" />
+ </component>
+</module>
+
diff --git a/ant.properties b/ant.properties new file mode 100644 index 00000000..de5f19ef --- /dev/null +++ b/ant.properties @@ -0,0 +1,20 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked in Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + +key.store=C:/Users/Scott/Documents/Subsonic/subsonic-android/subsonic.keystore +key.alias=subsonic + diff --git a/assets/fonts/Storopia.ttf b/assets/fonts/Storopia.ttf Binary files differnew file mode 100644 index 00000000..cbdc4c1f --- /dev/null +++ b/assets/fonts/Storopia.ttf diff --git a/assets/html/en/index.html b/assets/html/en/index.html new file mode 100644 index 00000000..9ad7542c --- /dev/null +++ b/assets/html/en/index.html @@ -0,0 +1,98 @@ +<html> +<head> + <title>DSub Help</title> + <link rel="stylesheet" href="../style.css" type="text/css"> + +</head> + +<body> + +<h3><img src="../img/subsonic.png" alt=""> Welcome to DSub!</h3> + +<p> + With <b>DSub</b> you can easily stream or download music from your home computer to your Android phone + (and do lots of other cool stuff too). +</p> + +<p> + To install the Subsonic server software on your computer, please visit <a href="http://subsonic.org">subsonic.org</a>. + It's available for Windows, Mac, Linux and Unix. +</p> + +<p> + By default, this program is configured to use the <b>Subsonic demo server</b>. Once you've set up your own + server, please go to <b>Settings</b> and change the configuration so that it connects to your own computer. +</p> + +<p> + You can use this program freely for 30 days. After that you will have to make a donation to the Subsonic project. + As a donor you get the following benefits: +</p> +<ul> + <li>Unlimited streaming and download to any number of iPhone and Android phones.</li> + <li>Video streaming.</li> + <li>A personal web address for your Subsonic server (<em>yourname</em>.subsonic.org).</li> + <li>No ads in the Subsonic web interface.</li> + <li>Free access to new premium features.</li> +</ul> + +<p> + The suggested donation amount is <b>€20</b>, but you can give any amount you like. +</p> + +<p> + Click one of the buttons to go to PayPal where you can pay by credit card or by using your PayPal account. + Once the donation is processed, you will receive a license key by email. +</p> + +<table> + <tr> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3RTGWJRNAW2PU"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">€10</td> + </tr> + </table> + </td> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UCUUB2TYE4PGN"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">€20</td> + </tr> + </table> + </td> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3M6TFHWEPSU44"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">€25</td> + </tr> + </table> + </td> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5KP7LPQU77UAS"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">€30</td> + </tr> + </table> + </td> + </tr> +</table> + +<p> + For more information, please visit <a href="http://subsonic.org/">subsonic.org</a> +</p> + +</body> +</html> diff --git a/assets/html/fr/index.html b/assets/html/fr/index.html new file mode 100644 index 00000000..4ac8c9c3 --- /dev/null +++ b/assets/html/fr/index.html @@ -0,0 +1,100 @@ +<html> +<head> + <title>Aide de Subsonic</title> + <link rel="stylesheet" href="../style.css" type="text/css"> + +</head> + +<body> + +<h3><img src="../img/subsonic.png" alt=""> Bienvenue dans Subsonic</h3> + +<p> + Avec <b>Subsonic</b>, vous pouvez facilement écouter ou télécharger de la musique à partir de votre ordinateur personnel sur votre appareil Android + (et faire plein d'autres trucs cools aussi). +</p> + +<p> + Pour installer le serveur Subsonic sur votre ordinateur, veuillez visiter <a href="http://subsonic.org">subsonic.org</a>. + Celui-ci est disponible pour Windows, Mac, Linux et Unix. +</p> + +<p> + Par défaut, cette application est configuré pour utiliser le <b>serveur démo Subsonic</b>. + Après avoir configuré votre serveur personnel, veuillez accéder aux <b>Paramètres</b> et modifier la configuration + afin de vous connecter à votre propre ordinateur. +</p> + +<p> + Vous pouvez utiliser cette application gratuitement pendant 30 jours. + Ensuite, vous devrez effectuer un don au projet Subsonic. + En tant que donateur, vous obtiendrez les bénéfices suivants: +</p> +<ul> + <li>Écoute et téléchargement illimités vers autant de iPhones et d'appareils Android que désiré.</li> + <li>Écoute de vidéos.</li> + <li>Une adresse web personnalisée pour votre serveur Subsonic (<em>votrenom</em>.subsonic.org).</li> + <li>Aucune publicité dans l'interface web de Subsonic.</li> + <li>Accès gratuit aux nouvelles fonctionnalités avancées.</li> +</ul> + +<p> + Le montant suggéré pour le don est de <b>20€</b>, mais n'importe quel montant fera l'affaire. +</p> + +<p> + Cliquez l'un des boutons suivants pour accéder à PayPal, d'où vous pourrez payer soit par carte de crédit ou en utilisant votre compte PayPal. + Une fois le don reçu et traité, vous recevrez votre clé d'activation par courriel. +</p> + +<table> + <tr> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3RTGWJRNAW2PU"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">10€</td> + </tr> + </table> + </td> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UCUUB2TYE4PGN"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">20€</td> + </tr> + </table> + </td> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3M6TFHWEPSU44"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">25€</td> + </tr> + </table> + </td> + <td style="border:none;"> + <table> + <tr> + <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5KP7LPQU77UAS"><img src="../img/paypal.gif" alt=""/></a> </td> + </tr> + <tr> + <td style="text-align:center;border:none;padding:0">30€</td> + </tr> + </table> + </td> + </tr> +</table> + +<p> + Pour plus d'information, veuillez visiter <a href="http://subsonic.org/">subsonic.org</a> +</p> + +</body> +</html> diff --git a/assets/html/img/paypal.gif b/assets/html/img/paypal.gif Binary files differnew file mode 100644 index 00000000..d017250a --- /dev/null +++ b/assets/html/img/paypal.gif diff --git a/assets/html/img/subsonic.png b/assets/html/img/subsonic.png Binary files differnew file mode 100644 index 00000000..38c521c5 --- /dev/null +++ b/assets/html/img/subsonic.png diff --git a/assets/html/ru/index.html b/assets/html/ru/index.html new file mode 100644 index 00000000..57979152 --- /dev/null +++ b/assets/html/ru/index.html @@ -0,0 +1,98 @@ +<html>
+<head>
+ <title>Помощь DSub</title>
+ <link rel="stylesheet" href="../style.css" type="text/css">
+
+</head>
+
+<body>
+
+<h3><img src="../img/subsonic.png" alt=""> Добро пожаловать в DSub!</h3>
+
+<p>
+ С программой <b>DSub</b> Вы можете легко включить поточное воспроизведение или скачивать музыку с Вашего домашнего компьютера на Android устройство
+ (и использовать множество других полезных функции).
+</p>
+
+<p>
+ Для установки серверного приложения Subsonic на Ваш компьютер, пожалуйста, посетите <a href="http://subsonic.org">subsonic.org</a>.
+ Приложение доступно для Windows, Mac, а также Linux и Unix.
+</p>
+
+<p>
+ По умолчанию данная программа настроена на работу с <b>демо сервером Subsonic</b>. После установки серверного
+ приложения, пожалуйста, перейдите в раздел <b>Настройки</b> и измените параметры для подключения.
+</p>
+
+<p>
+ Вы можете бесплатно использовать программу до 30 дней. После этого Вам необходимо сделать пожертвование проекту Subsonic.
+ После этого Вы получите следующие возможности:
+</p>
+<ul>
+ <li>Неограниченное поточное воспроизведение или скачивание с любого количества iPhone и Android устройств.</li>
+ <li>Потоковое воспроизведение видео.</li>
+ <li>Персональный адрес страницы на сервере Subsonic (<em>вашеимя</em>.subsonic.org).</li>
+ <li>Отсутствие рекламы в веб-интерфейсе Subsonic.</li>
+ <li>Бесплатный доступ к новым премиум-функциям.</li>
+</ul>
+
+<p>
+ Рекомендуемая сумма пожертвования - <b>€20</b>, но Вы можете пожертвовать любую сумму.
+</p>
+
+<p>
+ Нажмите одну из кнопок для перехода на страницу PayPal, откуда Вы сможете сделать перевод с Вашей кредитной карты или используя аккаунт PayPal.
+ После отправки пожертвования Вы получите лицензионный ключ на Ваш email.
+</p>
+
+<table>
+ <tr>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3RTGWJRNAW2PU"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">€10</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UCUUB2TYE4PGN"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">€20</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3M6TFHWEPSU44"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">€25</td>
+ </tr>
+ </table>
+ </td>
+ <td style="border:none;">
+ <table>
+ <tr>
+ <td style="border:none;padding:0"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5KP7LPQU77UAS"><img src="../img/paypal.gif" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td style="text-align:center;border:none;padding:0">€30</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+
+<p>
+ За дополнительной информацией посетите <a href="http://subsonic.org/">subsonic.org</a>
+</p>
+
+</body>
+</html>
\ No newline at end of file diff --git a/assets/html/style.css b/assets/html/style.css new file mode 100644 index 00000000..9c1d55f2 --- /dev/null +++ b/assets/html/style.css @@ -0,0 +1,11 @@ +/* +* Taken from http://yui.yahooapis.com/2.8.0r4/build/fonts/fonts.css +*/ +body { + font: 13px / 1.231 arial, helvetica, clean, sans-serif; +} + +table { + font-size:inherit; + font:100%; +}
\ No newline at end of file diff --git a/build.properties b/build.properties new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/build.properties diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..6d0d026f --- /dev/null +++ b/build.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<project name="subsonic-android" default="help">
+
+ <!-- The local.properties file is created and updated by the 'android' tool.
+ It contains the path to the SDK. It should *NOT* be checked into
+ Version Control Systems. -->
+ <property file="local.properties" />
+
+ <!-- The ant.properties file can be created by you. It is only edited by the
+ 'android' tool to add properties to it.
+ This is the place to change some Ant specific build properties.
+ Here are some properties you may want to change/update:
+
+ source.dir
+ The name of the source directory. Default is 'src'.
+ out.dir
+ The name of the output directory. Default is 'bin'.
+
+ For other overridable properties, look at the beginning of the rules
+ files in the SDK, at tools/ant/build.xml
+
+ Properties related to the SDK location or the project target should
+ be updated using the 'android' tool with the 'update' action.
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems.
+
+ -->
+ <property file="ant.properties" />
+
+ <!-- if sdk.dir was not set from one of the property file, then
+ get it from the ANDROID_HOME env var.
+ This must be done before we load project.properties since
+ the proguard config can use sdk.dir -->
+ <property environment="env" />
+ <condition property="sdk.dir" value="${env.ANDROID_HOME}">
+ <isset property="env.ANDROID_HOME" />
+ </condition>
+
+ <!-- The project.properties file is created and updated by the 'android'
+ tool, as well as ADT.
+
+ This contains project specific properties such as project target, and library
+ dependencies. Lower level build properties are stored in ant.properties
+ (or in .classpath for Eclipse projects).
+
+ This file is an integral part of the build system for your
+ application and should be checked into Version Control Systems. -->
+ <loadproperties srcFile="project.properties" />
+
+ <!-- quick check on sdk.dir -->
+ <fail
+ message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
+ unless="sdk.dir"
+ />
+
+ <!--
+ Import per project custom build rules if present at the root of the project.
+ This is the place to put custom intermediary targets such as:
+ -pre-build
+ -pre-compile
+ -post-compile (This is typically used for code obfuscation.
+ Compiled code location: ${out.classes.absolute.dir}
+ If this is not done in place, override ${out.dex.input.absolute.dir})
+ -post-package
+ -post-build
+ -pre-clean
+ -->
+ <import file="custom_rules.xml" optional="true" />
+
+ <!-- Import the actual build file.
+
+ To customize existing targets, there are two options:
+ - Customize only one target:
+ - copy/paste the target into this file, *before* the
+ <import> task.
+ - customize it to your needs.
+ - Customize the whole content of build.xml
+ - copy/paste the content of the rules files (minus the top node)
+ into this file, replacing the <import> task.
+ - customize to your needs.
+
+ ***********************
+ ****** IMPORTANT ******
+ ***********************
+ In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
+ in order to avoid having your file be overridden by tools such as "android update project"
+ -->
+ <!-- version-tag: 1 -->
+ <import file="${sdk.dir}/tools/ant/build.xml" />
+
+</project>
diff --git a/debug.keystore b/debug.keystore Binary files differnew file mode 100644 index 00000000..4e662d41 --- /dev/null +++ b/debug.keystore diff --git a/default.properties b/default.properties new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/default.properties diff --git a/libs/CWAC-AdapterWrapper.jar b/libs/CWAC-AdapterWrapper.jar Binary files differnew file mode 100644 index 00000000..692fe4d3 --- /dev/null +++ b/libs/CWAC-AdapterWrapper.jar diff --git a/libs/CWAC-EndlessAdapter.jar b/libs/CWAC-EndlessAdapter.jar Binary files differnew file mode 100644 index 00000000..ec20d936 --- /dev/null +++ b/libs/CWAC-EndlessAdapter.jar diff --git a/libs/android-support-v4.jar b/libs/android-support-v4.jar Binary files differnew file mode 100644 index 00000000..6080877d --- /dev/null +++ b/libs/android-support-v4.jar diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 00000000..b60ae7ea --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/proguard.cfg b/proguard.cfg new file mode 100644 index 00000000..f0b04dc3 --- /dev/null +++ b/proguard.cfg @@ -0,0 +1,40 @@ +-optimizationpasses 5
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontpreverify
+-verbose
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+-keep public class com.android.vending.licensing.ILicensingService
+
+-keepclasseswithmembernames class * {
+ native <methods>;
+}
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+ public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keep class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator *;
+}
diff --git a/project.properties b/project.properties new file mode 100644 index 00000000..56014cda --- /dev/null +++ b/project.properties @@ -0,0 +1,13 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-17 +android.library.reference.1=ActionBarSherlock/actionbarsherlock +android.library.reference.2=DragSortListView/library
\ No newline at end of file diff --git a/releases/DSub 4.0.1.apk b/releases/DSub 4.0.1.apk Binary files differnew file mode 100644 index 00000000..850a4bfa --- /dev/null +++ b/releases/DSub 4.0.1.apk diff --git a/releases/DSub 4.0.2.apk b/releases/DSub 4.0.2.apk Binary files differnew file mode 100644 index 00000000..0c168ba8 --- /dev/null +++ b/releases/DSub 4.0.2.apk diff --git a/releases/DSub 4.0.3.apk b/releases/DSub 4.0.3.apk Binary files differnew file mode 100644 index 00000000..a1749e3c --- /dev/null +++ b/releases/DSub 4.0.3.apk diff --git a/releases/DSub 4.0.4.apk b/releases/DSub 4.0.4.apk Binary files differnew file mode 100644 index 00000000..1332af32 --- /dev/null +++ b/releases/DSub 4.0.4.apk diff --git a/releases/DSub 4.0.5.apk b/releases/DSub 4.0.5.apk Binary files differnew file mode 100644 index 00000000..1ece93f6 --- /dev/null +++ b/releases/DSub 4.0.5.apk diff --git a/releases/DSub 4.0.6.apk b/releases/DSub 4.0.6.apk Binary files differnew file mode 100644 index 00000000..278256d7 --- /dev/null +++ b/releases/DSub 4.0.6.apk diff --git a/releases/DSub 4.0.7.apk b/releases/DSub 4.0.7.apk Binary files differnew file mode 100644 index 00000000..a97c215c --- /dev/null +++ b/releases/DSub 4.0.7.apk diff --git a/releases/DSub 4.1.0.apk b/releases/DSub 4.1.0.apk Binary files differnew file mode 100644 index 00000000..1bb3fe41 --- /dev/null +++ b/releases/DSub 4.1.0.apk diff --git a/releases/DSub 4.1.1.apk b/releases/DSub 4.1.1.apk Binary files differnew file mode 100644 index 00000000..9d582aec --- /dev/null +++ b/releases/DSub 4.1.1.apk diff --git a/releases/DSub 4.1.2.apk b/releases/DSub 4.1.2.apk Binary files differnew file mode 100644 index 00000000..0a4ee2bc --- /dev/null +++ b/releases/DSub 4.1.2.apk diff --git a/res/anim/push_down_in.xml b/res/anim/push_down_in.xml new file mode 100644 index 00000000..6ab9a047 --- /dev/null +++ b/res/anim/push_down_in.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate android:fromYDelta="-100%p" android:toYDelta="0" + android:duration="@android:integer/config_longAnimTime"/> + <alpha android:fromAlpha="0.0" android:toAlpha="1.0" + android:duration="@android:integer/config_longAnimTime" /> +</set> diff --git a/res/anim/push_down_out.xml b/res/anim/push_down_out.xml new file mode 100644 index 00000000..ce36458a --- /dev/null +++ b/res/anim/push_down_out.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate android:fromYDelta="0" android:toYDelta="100%p" + android:duration="@android:integer/config_longAnimTime"/> + <alpha android:fromAlpha="1.0" android:toAlpha="0.0" + android:duration="@android:integer/config_longAnimTime" /> +</set> diff --git a/res/anim/push_up_in.xml b/res/anim/push_up_in.xml new file mode 100644 index 00000000..6ef582c4 --- /dev/null +++ b/res/anim/push_up_in.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate android:fromYDelta="100%p" android:toYDelta="0" + android:duration="@android:integer/config_longAnimTime"/> + <alpha android:fromAlpha="0.0" android:toAlpha="1.0" + android:duration="@android:integer/config_longAnimTime" /> +</set> diff --git a/res/anim/push_up_out.xml b/res/anim/push_up_out.xml new file mode 100644 index 00000000..2b267d59 --- /dev/null +++ b/res/anim/push_up_out.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate android:fromYDelta="0" android:toYDelta="-100%p" + android:duration="@android:integer/config_longAnimTime"/> + <alpha android:fromAlpha="1.0" android:toAlpha="0.0" + android:duration="@android:integer/config_longAnimTime" /> +</set> diff --git a/res/drawable-hdpi-v4/action_browse.png b/res/drawable-hdpi-v4/action_browse.png Binary files differnew file mode 100644 index 00000000..54296909 --- /dev/null +++ b/res/drawable-hdpi-v4/action_browse.png diff --git a/res/drawable-hdpi-v4/action_compass.png b/res/drawable-hdpi-v4/action_compass.png Binary files differnew file mode 100644 index 00000000..39760f89 --- /dev/null +++ b/res/drawable-hdpi-v4/action_compass.png diff --git a/res/drawable-hdpi-v4/action_exit.png b/res/drawable-hdpi-v4/action_exit.png Binary files differnew file mode 100644 index 00000000..09e18dee --- /dev/null +++ b/res/drawable-hdpi-v4/action_exit.png diff --git a/res/drawable-hdpi-v4/action_help.png b/res/drawable-hdpi-v4/action_help.png Binary files differnew file mode 100644 index 00000000..aaf8304c --- /dev/null +++ b/res/drawable-hdpi-v4/action_help.png diff --git a/res/drawable-hdpi-v4/action_moreoverflow.png b/res/drawable-hdpi-v4/action_moreoverflow.png Binary files differnew file mode 100644 index 00000000..cb6ebdaf --- /dev/null +++ b/res/drawable-hdpi-v4/action_moreoverflow.png diff --git a/res/drawable-hdpi-v4/action_offline.png b/res/drawable-hdpi-v4/action_offline.png Binary files differnew file mode 100644 index 00000000..a85f0931 --- /dev/null +++ b/res/drawable-hdpi-v4/action_offline.png diff --git a/res/drawable-hdpi-v4/action_play_all.png b/res/drawable-hdpi-v4/action_play_all.png Binary files differnew file mode 100644 index 00000000..6ce5629a --- /dev/null +++ b/res/drawable-hdpi-v4/action_play_all.png diff --git a/res/drawable-hdpi-v4/action_refresh.png b/res/drawable-hdpi-v4/action_refresh.png Binary files differnew file mode 100644 index 00000000..9f30dc95 --- /dev/null +++ b/res/drawable-hdpi-v4/action_refresh.png diff --git a/res/drawable-hdpi-v4/action_remove_all.png b/res/drawable-hdpi-v4/action_remove_all.png Binary files differnew file mode 100644 index 00000000..97b88837 --- /dev/null +++ b/res/drawable-hdpi-v4/action_remove_all.png diff --git a/res/drawable-hdpi-v4/action_save.png b/res/drawable-hdpi-v4/action_save.png Binary files differnew file mode 100644 index 00000000..7bda97d6 --- /dev/null +++ b/res/drawable-hdpi-v4/action_save.png diff --git a/res/drawable-hdpi-v4/action_screen_on_off.png b/res/drawable-hdpi-v4/action_screen_on_off.png Binary files differnew file mode 100644 index 00000000..c7168563 --- /dev/null +++ b/res/drawable-hdpi-v4/action_screen_on_off.png diff --git a/res/drawable-hdpi-v4/action_search.png b/res/drawable-hdpi-v4/action_search.png Binary files differnew file mode 100644 index 00000000..6bc3d426 --- /dev/null +++ b/res/drawable-hdpi-v4/action_search.png diff --git a/res/drawable-hdpi-v4/action_select.png b/res/drawable-hdpi-v4/action_select.png Binary files differnew file mode 100644 index 00000000..e9e83e3d --- /dev/null +++ b/res/drawable-hdpi-v4/action_select.png diff --git a/res/drawable-hdpi-v4/action_settings.png b/res/drawable-hdpi-v4/action_settings.png Binary files differnew file mode 100644 index 00000000..1ab7722b --- /dev/null +++ b/res/drawable-hdpi-v4/action_settings.png diff --git a/res/drawable-hdpi-v4/action_share.png b/res/drawable-hdpi-v4/action_share.png Binary files differnew file mode 100644 index 00000000..28376157 --- /dev/null +++ b/res/drawable-hdpi-v4/action_share.png diff --git a/res/drawable-hdpi-v4/action_shuffle.png b/res/drawable-hdpi-v4/action_shuffle.png Binary files differnew file mode 100644 index 00000000..0613965c --- /dev/null +++ b/res/drawable-hdpi-v4/action_shuffle.png diff --git a/res/drawable-hdpi-v4/action_toggle_list.png b/res/drawable-hdpi-v4/action_toggle_list.png Binary files differnew file mode 100644 index 00000000..87f9280f --- /dev/null +++ b/res/drawable-hdpi-v4/action_toggle_list.png diff --git a/res/drawable-hdpi-v4/actionbar_background.9.png b/res/drawable-hdpi-v4/actionbar_background.9.png Binary files differnew file mode 100644 index 00000000..9ce38a61 --- /dev/null +++ b/res/drawable-hdpi-v4/actionbar_background.9.png diff --git a/res/drawable-hdpi-v4/actionbar_button_normal.9.png b/res/drawable-hdpi-v4/actionbar_button_normal.9.png Binary files differnew file mode 100644 index 00000000..385f751c --- /dev/null +++ b/res/drawable-hdpi-v4/actionbar_button_normal.9.png diff --git a/res/drawable-hdpi-v4/album_art_background.png b/res/drawable-hdpi-v4/album_art_background.png Binary files differnew file mode 100644 index 00000000..f0757695 --- /dev/null +++ b/res/drawable-hdpi-v4/album_art_background.png diff --git a/res/drawable-hdpi-v4/appwidget_art_default.png b/res/drawable-hdpi-v4/appwidget_art_default.png Binary files differnew file mode 100644 index 00000000..5bd39cc2 --- /dev/null +++ b/res/drawable-hdpi-v4/appwidget_art_default.png diff --git a/res/drawable-hdpi-v4/appwidget_art_unknown.png b/res/drawable-hdpi-v4/appwidget_art_unknown.png Binary files differnew file mode 100644 index 00000000..5bd39cc2 --- /dev/null +++ b/res/drawable-hdpi-v4/appwidget_art_unknown.png diff --git a/res/drawable-hdpi-v4/appwidget_bg.9.png b/res/drawable-hdpi-v4/appwidget_bg.9.png Binary files differnew file mode 100644 index 00000000..6bacc7fe --- /dev/null +++ b/res/drawable-hdpi-v4/appwidget_bg.9.png diff --git a/res/drawable-hdpi-v4/background.png b/res/drawable-hdpi-v4/background.png Binary files differnew file mode 100644 index 00000000..07d6a9cc --- /dev/null +++ b/res/drawable-hdpi-v4/background.png diff --git a/res/drawable-hdpi-v4/btn_check_buttonless_off.png b/res/drawable-hdpi-v4/btn_check_buttonless_off.png Binary files differnew file mode 100644 index 00000000..d705b420 --- /dev/null +++ b/res/drawable-hdpi-v4/btn_check_buttonless_off.png diff --git a/res/drawable-hdpi-v4/btn_check_buttonless_on.png b/res/drawable-hdpi-v4/btn_check_buttonless_on.png Binary files differnew file mode 100644 index 00000000..a2612d7d --- /dev/null +++ b/res/drawable-hdpi-v4/btn_check_buttonless_on.png diff --git a/res/drawable-hdpi-v4/downloaded.png b/res/drawable-hdpi-v4/downloaded.png Binary files differnew file mode 100644 index 00000000..f854aaf4 --- /dev/null +++ b/res/drawable-hdpi-v4/downloaded.png diff --git a/res/drawable-hdpi-v4/downloading.png b/res/drawable-hdpi-v4/downloading.png Binary files differnew file mode 100644 index 00000000..afff39a9 --- /dev/null +++ b/res/drawable-hdpi-v4/downloading.png diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_next.png b/res/drawable-hdpi-v4/ic_appwidget_music_next.png Binary files differnew file mode 100644 index 00000000..99d28766 --- /dev/null +++ b/res/drawable-hdpi-v4/ic_appwidget_music_next.png diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_pause.png b/res/drawable-hdpi-v4/ic_appwidget_music_pause.png Binary files differnew file mode 100644 index 00000000..a05a8c50 --- /dev/null +++ b/res/drawable-hdpi-v4/ic_appwidget_music_pause.png diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_play.png b/res/drawable-hdpi-v4/ic_appwidget_music_play.png Binary files differnew file mode 100644 index 00000000..a754b920 --- /dev/null +++ b/res/drawable-hdpi-v4/ic_appwidget_music_play.png diff --git a/res/drawable-hdpi-v4/ic_appwidget_music_previous.png b/res/drawable-hdpi-v4/ic_appwidget_music_previous.png Binary files differnew file mode 100644 index 00000000..7fb3921b --- /dev/null +++ b/res/drawable-hdpi-v4/ic_appwidget_music_previous.png diff --git a/res/drawable-hdpi-v4/ic_menu_chat_dark.png b/res/drawable-hdpi-v4/ic_menu_chat_dark.png Binary files differnew file mode 100644 index 00000000..be04b06e --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_chat_dark.png diff --git a/res/drawable-hdpi-v4/ic_menu_chat_light.png b/res/drawable-hdpi-v4/ic_menu_chat_light.png Binary files differnew file mode 100644 index 00000000..3f58695c --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_chat_light.png diff --git a/res/drawable-hdpi-v4/ic_menu_chat_send_dark.png b/res/drawable-hdpi-v4/ic_menu_chat_send_dark.png Binary files differnew file mode 100644 index 00000000..bd37dc59 --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_chat_send_dark.png diff --git a/res/drawable-hdpi-v4/ic_menu_chat_send_light.png b/res/drawable-hdpi-v4/ic_menu_chat_send_light.png Binary files differnew file mode 100644 index 00000000..0c870d2c --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_chat_send_light.png diff --git a/res/drawable-hdpi-v4/ic_menu_exit.png b/res/drawable-hdpi-v4/ic_menu_exit.png Binary files differnew file mode 100644 index 00000000..847a1ed3 --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_exit.png diff --git a/res/drawable-hdpi-v4/ic_menu_help.png b/res/drawable-hdpi-v4/ic_menu_help.png Binary files differnew file mode 100644 index 00000000..9f11f434 --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_help.png diff --git a/res/drawable-hdpi-v4/ic_menu_settings.png b/res/drawable-hdpi-v4/ic_menu_settings.png Binary files differnew file mode 100644 index 00000000..48775c1e --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_settings.png diff --git a/res/drawable-hdpi-v4/ic_menu_shuffle.png b/res/drawable-hdpi-v4/ic_menu_shuffle.png Binary files differnew file mode 100644 index 00000000..0613965c --- /dev/null +++ b/res/drawable-hdpi-v4/ic_menu_shuffle.png diff --git a/res/drawable-hdpi-v4/ic_stat_star.png b/res/drawable-hdpi-v4/ic_stat_star.png Binary files differnew file mode 100644 index 00000000..b16e803c --- /dev/null +++ b/res/drawable-hdpi-v4/ic_stat_star.png diff --git a/res/drawable-hdpi-v4/launch.png b/res/drawable-hdpi-v4/launch.png Binary files differnew file mode 100644 index 00000000..10693360 --- /dev/null +++ b/res/drawable-hdpi-v4/launch.png diff --git a/res/drawable-hdpi-v4/launch2.png b/res/drawable-hdpi-v4/launch2.png Binary files differnew file mode 100644 index 00000000..a23d09d9 --- /dev/null +++ b/res/drawable-hdpi-v4/launch2.png diff --git a/res/drawable-hdpi-v4/list_item_more.9.png b/res/drawable-hdpi-v4/list_item_more.9.png Binary files differnew file mode 100644 index 00000000..79ca860d --- /dev/null +++ b/res/drawable-hdpi-v4/list_item_more.9.png diff --git a/res/drawable-hdpi-v4/list_item_more_saved.9.png b/res/drawable-hdpi-v4/list_item_more_saved.9.png Binary files differnew file mode 100644 index 00000000..f3805bfb --- /dev/null +++ b/res/drawable-hdpi-v4/list_item_more_saved.9.png diff --git a/res/drawable-hdpi-v4/list_item_more_shaded.9.png b/res/drawable-hdpi-v4/list_item_more_shaded.9.png Binary files differnew file mode 100644 index 00000000..99c2f5b8 --- /dev/null +++ b/res/drawable-hdpi-v4/list_item_more_shaded.9.png diff --git a/res/drawable-hdpi-v4/main_header_icon.png b/res/drawable-hdpi-v4/main_header_icon.png Binary files differnew file mode 100644 index 00000000..4252ba5b --- /dev/null +++ b/res/drawable-hdpi-v4/main_header_icon.png diff --git a/res/drawable-hdpi-v4/main_header_icon2.png b/res/drawable-hdpi-v4/main_header_icon2.png Binary files differnew file mode 100644 index 00000000..0889aee6 --- /dev/null +++ b/res/drawable-hdpi-v4/main_header_icon2.png diff --git a/res/drawable-hdpi-v4/main_offline.png b/res/drawable-hdpi-v4/main_offline.png Binary files differnew file mode 100644 index 00000000..a1d27cec --- /dev/null +++ b/res/drawable-hdpi-v4/main_offline.png diff --git a/res/drawable-hdpi-v4/main_offline_light.png b/res/drawable-hdpi-v4/main_offline_light.png Binary files differnew file mode 100644 index 00000000..69bee782 --- /dev/null +++ b/res/drawable-hdpi-v4/main_offline_light.png diff --git a/res/drawable-hdpi-v4/main_select_server.png b/res/drawable-hdpi-v4/main_select_server.png Binary files differnew file mode 100644 index 00000000..c2cefead --- /dev/null +++ b/res/drawable-hdpi-v4/main_select_server.png diff --git a/res/drawable-hdpi-v4/media_backward.png b/res/drawable-hdpi-v4/media_backward.png Binary files differnew file mode 100644 index 00000000..3bb85e68 --- /dev/null +++ b/res/drawable-hdpi-v4/media_backward.png diff --git a/res/drawable-hdpi-v4/media_backward_light.png b/res/drawable-hdpi-v4/media_backward_light.png Binary files differnew file mode 100644 index 00000000..14188c86 --- /dev/null +++ b/res/drawable-hdpi-v4/media_backward_light.png diff --git a/res/drawable-hdpi-v4/media_forward.png b/res/drawable-hdpi-v4/media_forward.png Binary files differnew file mode 100644 index 00000000..cf39f1f0 --- /dev/null +++ b/res/drawable-hdpi-v4/media_forward.png diff --git a/res/drawable-hdpi-v4/media_forward_light.png b/res/drawable-hdpi-v4/media_forward_light.png Binary files differnew file mode 100644 index 00000000..9e172d8f --- /dev/null +++ b/res/drawable-hdpi-v4/media_forward_light.png diff --git a/res/drawable-hdpi-v4/media_pause.png b/res/drawable-hdpi-v4/media_pause.png Binary files differnew file mode 100644 index 00000000..d4cab525 --- /dev/null +++ b/res/drawable-hdpi-v4/media_pause.png diff --git a/res/drawable-hdpi-v4/media_pause_light.png b/res/drawable-hdpi-v4/media_pause_light.png Binary files differnew file mode 100644 index 00000000..8ebf9b45 --- /dev/null +++ b/res/drawable-hdpi-v4/media_pause_light.png diff --git a/res/drawable-hdpi-v4/media_repeat_all.png b/res/drawable-hdpi-v4/media_repeat_all.png Binary files differnew file mode 100644 index 00000000..c2255058 --- /dev/null +++ b/res/drawable-hdpi-v4/media_repeat_all.png diff --git a/res/drawable-hdpi-v4/media_repeat_off.png b/res/drawable-hdpi-v4/media_repeat_off.png Binary files differnew file mode 100644 index 00000000..10315ab3 --- /dev/null +++ b/res/drawable-hdpi-v4/media_repeat_off.png diff --git a/res/drawable-hdpi-v4/media_repeat_off_light.png b/res/drawable-hdpi-v4/media_repeat_off_light.png Binary files differnew file mode 100644 index 00000000..39408bec --- /dev/null +++ b/res/drawable-hdpi-v4/media_repeat_off_light.png diff --git a/res/drawable-hdpi-v4/media_repeat_single.png b/res/drawable-hdpi-v4/media_repeat_single.png Binary files differnew file mode 100644 index 00000000..6d280e7a --- /dev/null +++ b/res/drawable-hdpi-v4/media_repeat_single.png diff --git a/res/drawable-hdpi-v4/media_start.png b/res/drawable-hdpi-v4/media_start.png Binary files differnew file mode 100644 index 00000000..2af5996f --- /dev/null +++ b/res/drawable-hdpi-v4/media_start.png diff --git a/res/drawable-hdpi-v4/media_start_light.png b/res/drawable-hdpi-v4/media_start_light.png Binary files differnew file mode 100644 index 00000000..45cad73c --- /dev/null +++ b/res/drawable-hdpi-v4/media_start_light.png diff --git a/res/drawable-hdpi-v4/media_stop.png b/res/drawable-hdpi-v4/media_stop.png Binary files differnew file mode 100644 index 00000000..329eb906 --- /dev/null +++ b/res/drawable-hdpi-v4/media_stop.png diff --git a/res/drawable-hdpi-v4/media_stop_light.png b/res/drawable-hdpi-v4/media_stop_light.png Binary files differnew file mode 100644 index 00000000..110d538e --- /dev/null +++ b/res/drawable-hdpi-v4/media_stop_light.png diff --git a/res/drawable-hdpi-v4/menu_browse.png b/res/drawable-hdpi-v4/menu_browse.png Binary files differnew file mode 100644 index 00000000..54296909 --- /dev/null +++ b/res/drawable-hdpi-v4/menu_browse.png diff --git a/res/drawable-hdpi-v4/menu_home.png b/res/drawable-hdpi-v4/menu_home.png Binary files differnew file mode 100644 index 00000000..3cec6246 --- /dev/null +++ b/res/drawable-hdpi-v4/menu_home.png diff --git a/res/drawable-hdpi-v4/menu_now_playing.png b/res/drawable-hdpi-v4/menu_now_playing.png Binary files differnew file mode 100644 index 00000000..6ce5629a --- /dev/null +++ b/res/drawable-hdpi-v4/menu_now_playing.png diff --git a/res/drawable-hdpi-v4/menu_playlists.png b/res/drawable-hdpi-v4/menu_playlists.png Binary files differnew file mode 100644 index 00000000..e9e83e3d --- /dev/null +++ b/res/drawable-hdpi-v4/menu_playlists.png diff --git a/res/drawable-hdpi-v4/menubar_button_selected.9.png b/res/drawable-hdpi-v4/menubar_button_selected.9.png Binary files differnew file mode 100644 index 00000000..d47bec40 --- /dev/null +++ b/res/drawable-hdpi-v4/menubar_button_selected.9.png diff --git a/res/drawable-hdpi-v4/notification_next.png b/res/drawable-hdpi-v4/notification_next.png Binary files differnew file mode 100644 index 00000000..5835f654 --- /dev/null +++ b/res/drawable-hdpi-v4/notification_next.png diff --git a/res/drawable-hdpi-v4/notification_pause.png b/res/drawable-hdpi-v4/notification_pause.png Binary files differnew file mode 100644 index 00000000..3324f88f --- /dev/null +++ b/res/drawable-hdpi-v4/notification_pause.png diff --git a/res/drawable-hdpi-v4/notification_play.png b/res/drawable-hdpi-v4/notification_play.png Binary files differnew file mode 100644 index 00000000..8c95b6a5 --- /dev/null +++ b/res/drawable-hdpi-v4/notification_play.png diff --git a/res/drawable-hdpi-v4/notification_prev.png b/res/drawable-hdpi-v4/notification_prev.png Binary files differnew file mode 100644 index 00000000..73fb16f2 --- /dev/null +++ b/res/drawable-hdpi-v4/notification_prev.png diff --git a/res/drawable-hdpi-v4/notification_stop.png b/res/drawable-hdpi-v4/notification_stop.png Binary files differnew file mode 100644 index 00000000..ab98e188 --- /dev/null +++ b/res/drawable-hdpi-v4/notification_stop.png diff --git a/res/drawable-hdpi-v4/refresh.png b/res/drawable-hdpi-v4/refresh.png Binary files differnew file mode 100644 index 00000000..2f887c26 --- /dev/null +++ b/res/drawable-hdpi-v4/refresh.png diff --git a/res/drawable-hdpi-v4/saved.png b/res/drawable-hdpi-v4/saved.png Binary files differnew file mode 100644 index 00000000..6c7c276f --- /dev/null +++ b/res/drawable-hdpi-v4/saved.png diff --git a/res/drawable-hdpi-v4/search.png b/res/drawable-hdpi-v4/search.png Binary files differnew file mode 100644 index 00000000..43d8c87e --- /dev/null +++ b/res/drawable-hdpi-v4/search.png diff --git a/res/drawable-hdpi-v4/select_album_play_all_normal.png b/res/drawable-hdpi-v4/select_album_play_all_normal.png Binary files differnew file mode 100644 index 00000000..bcf0efe6 --- /dev/null +++ b/res/drawable-hdpi-v4/select_album_play_all_normal.png diff --git a/res/drawable-hdpi-v4/select_album_play_all_pressed.png b/res/drawable-hdpi-v4/select_album_play_all_pressed.png Binary files differnew file mode 100644 index 00000000..31bbfff0 --- /dev/null +++ b/res/drawable-hdpi-v4/select_album_play_all_pressed.png diff --git a/res/drawable-hdpi-v4/slider_knob.png b/res/drawable-hdpi-v4/slider_knob.png Binary files differnew file mode 100644 index 00000000..ae21a4f9 --- /dev/null +++ b/res/drawable-hdpi-v4/slider_knob.png diff --git a/res/drawable-hdpi-v4/stat_notify_playing.png b/res/drawable-hdpi-v4/stat_notify_playing.png Binary files differnew file mode 100644 index 00000000..bfd3e6a5 --- /dev/null +++ b/res/drawable-hdpi-v4/stat_notify_playing.png diff --git a/res/drawable-hdpi-v4/toast_frame.9.png b/res/drawable-hdpi-v4/toast_frame.9.png Binary files differnew file mode 100644 index 00000000..8f5d8119 --- /dev/null +++ b/res/drawable-hdpi-v4/toast_frame.9.png diff --git a/res/drawable-hdpi-v4/unknown_album.png b/res/drawable-hdpi-v4/unknown_album.png Binary files differnew file mode 100644 index 00000000..18b664e4 --- /dev/null +++ b/res/drawable-hdpi-v4/unknown_album.png diff --git a/res/drawable-hdpi-v4/unknown_album_large.png b/res/drawable-hdpi-v4/unknown_album_large.png Binary files differnew file mode 100644 index 00000000..bd9c6cc9 --- /dev/null +++ b/res/drawable-hdpi-v4/unknown_album_large.png diff --git a/res/drawable/actionbar_button.xml b/res/drawable/actionbar_button.xml new file mode 100644 index 00000000..5445cdcb --- /dev/null +++ b/res/drawable/actionbar_button.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:state_pressed="true" + android:drawable="@drawable/menubar_button_pressed" + /> + + <item + android:state_focused="true" + android:drawable="@drawable/menubar_button_pressed" + /> + + <item + android:drawable="@drawable/actionbar_button_normal" + /> + +</selector>
\ No newline at end of file diff --git a/res/drawable/btn_bg.xml b/res/drawable/btn_bg.xml new file mode 100644 index 00000000..79d40784 --- /dev/null +++ b/res/drawable/btn_bg.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2010 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" android:exitFadeDuration="@android:integer/config_mediumAnimTime">
+
+ <item android:drawable="@color/ics_opaque" android:state_pressed="true"/>
+ <item android:drawable="@color/ics_opaque" android:state_enabled="true" android:state_focused="true"/>
+
+</selector>
\ No newline at end of file diff --git a/res/drawable/btn_check.xml b/res/drawable/btn_check.xml new file mode 100644 index 00000000..f363a2d2 --- /dev/null +++ b/res/drawable/btn_check.xml @@ -0,0 +1,28 @@ +<?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/drawable/media_button.xml b/res/drawable/media_button.xml new file mode 100644 index 00000000..f144393d --- /dev/null +++ b/res/drawable/media_button.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="true" + android:drawable="@drawable/menubar_button_pressed"/> + + <item android:drawable="@drawable/menubar_button_normal"/> + +</selector>
\ No newline at end of file diff --git a/res/drawable/menubar_button.xml b/res/drawable/menubar_button.xml new file mode 100644 index 00000000..1dc79176 --- /dev/null +++ b/res/drawable/menubar_button.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="true" + android:drawable="@drawable/menubar_button_pressed"/> + + <item android:state_enabled="true" + android:drawable="@drawable/menubar_button_normal"/> + + <item android:drawable="@drawable/menubar_button_selected"/> + +</selector> + +
\ No newline at end of file diff --git a/res/drawable/menubar_button_normal.xml b/res/drawable/menubar_button_normal.xml new file mode 100644 index 00000000..76589c0c --- /dev/null +++ b/res/drawable/menubar_button_normal.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@android:color/transparent" /> +</shape> diff --git a/res/drawable/menubar_button_pressed.xml b/res/drawable/menubar_button_pressed.xml new file mode 100644 index 00000000..b7b42ee5 --- /dev/null +++ b/res/drawable/menubar_button_pressed.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/dividerColor" />
+</shape>
diff --git a/res/drawable/select_album_play_all.xml b/res/drawable/select_album_play_all.xml new file mode 100644 index 00000000..7e6a81ac --- /dev/null +++ b/res/drawable/select_album_play_all.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" android:drawable="@drawable/select_album_play_all_pressed" /> + <item android:drawable="@drawable/select_album_play_all_normal" /> +</selector>
\ No newline at end of file diff --git a/res/layout-land/download.xml b/res/layout-land/download.xml new file mode 100644 index 00000000..5b4db35e --- /dev/null +++ b/res/layout-land/download.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1">
+
+ <ImageView
+ android:id="@+id/download_album_art_image"
+ android:src="@drawable/unknown_album_large"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:scaleType="fitStart"/>
+
+ <include layout="@layout/download_playlist"/>
+
+ </github.daneren2005.dsub.view.MyViewFlipper>
+
+ <LinearLayout android:orientation="vertical"
+ android:id="@+id/download_control_layout"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <LinearLayout
+ android:id="@+id/download_other_controls_layout"
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <Button
+ android:id="@+id/download_jukebox"
+ android:text="RC"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dip"
+ android:padding="9dip"/>
+ <Button
+ android:id="@+id/download_equalizer"
+ android:text="EQ"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dip"
+ android:padding="9dip"/>
+ <Button
+ android:id="@+id/download_visualizer"
+ android:text="VIS"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dip"
+ android:padding="9dip"/>
+ <ImageButton
+ android:id="@+id/download_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="@drawable/menubar_button"
+ android:src="@android:drawable/star_big_off"
+ android:padding="10dip"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_visualizer_view_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="60dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:layout_gravity="center_horizontal"/>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:textColorPrimary"/>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_marginBottom="8dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary" />
+
+ <include layout="@layout/download_media_buttons"/>
+
+ <include layout="@layout/download_slider"/>
+
+ </LinearLayout>
+
+ </LinearLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/res/layout-port/download.xml b/res/layout-port/download.xml new file mode 100644 index 00000000..4b39286a --- /dev/null +++ b/res/layout-port/download.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <github.daneren2005.dsub.view.MyViewFlipper
+ android:id="@+id/download_playlist_flipper"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <RelativeLayout
+ android:id="@+id/download_album_art_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent">
+
+ <RelativeLayout android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:layout_above="@+id/download_song_title">
+
+ <ImageView
+ android:id="@+id/download_album_art_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="centerCrop"/>
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/download_overlay_buttons"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/overlayColor"
+ android:layout_alignParentBottom="true">
+
+ <Button
+ android:id="@+id/download_jukebox"
+ android:text="RC"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="4dip"
+ android:paddingLeft="14dip"
+ android:paddingBottom="4dip"/>
+
+ <Button
+ android:id="@+id/download_equalizer"
+ android:text="EQ"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="4dip"
+ android:paddingLeft="7dip"
+ android:paddingRight="7dip"
+ android:paddingBottom="4dip"/>
+
+ <Button
+ android:id="@+id/download_visualizer"
+ android:text="VIS"
+ android:textStyle="bold"
+ android:textSize="22sp"
+ android:background="@drawable/menubar_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="4dip"
+ android:paddingLeft="7dip"
+ android:paddingRight="7dip"
+ android:paddingBottom="4dip"/>
+
+ <ImageButton
+ android:id="@+id/download_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/download_jukebox"
+ android:background="@drawable/menubar_button"
+ android:src="@android:drawable/star_big_off"
+ android:paddingTop="8dip"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingBottom="8dip"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/download_visualizer_view_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="60dip"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_horizontal"
+ android:layout_alignParentBottom="true"/>
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/download_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"/>
+
+ <TextView
+ android:id="@+id/download_song_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_above="@+id/download_status"
+ android:layout_centerHorizontal="true"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textStyle="bold"
+ android:textSize="18sp"
+ android:ellipsize="end"/>
+
+ </RelativeLayout>
+
+ <include layout="@layout/download_playlist"/>
+
+ </github.daneren2005.dsub.view.MyViewFlipper>
+
+ <include layout="@layout/download_media_buttons"/>
+
+ <include layout="@layout/download_slider"/>
+ </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/actionbar_spinner.xml b/res/layout/actionbar_spinner.xml new file mode 100644 index 00000000..22055901 --- /dev/null +++ b/res/layout/actionbar_spinner.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:gravity="fill_horizontal" > + <Spinner + android:id="@+id/spinner" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:prompt="@string/common.appname" + /> +</RelativeLayout> diff --git a/res/layout/album_list_item.xml b/res/layout/album_list_item.xml new file mode 100644 index 00000000..0b84b4f3 --- /dev/null +++ b/res/layout/album_list_item.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/album_coverart"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|center_vertical"
+ android:paddingTop="1dip"
+ android:paddingBottom="1dip"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:paddingLeft="10dip"
+ android:paddingRight="3dip">
+
+ <TextView
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@+id/album_artist"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"/>
+
+ </LinearLayout>
+
+ <ImageButton
+ android:id="@+id/album_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@android:color/transparent"
+ android:focusable="false"/>
+
+ <ImageView
+ android:id="@+id/album_more"
+ android:src="@drawable/list_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="6dip"
+ android:background="@drawable/menubar_button"/>
+</LinearLayout>
diff --git a/res/layout/appwidget4x1.xml b/res/layout/appwidget4x1.xml new file mode 100644 index 00000000..5e55aa37 --- /dev/null +++ b/res/layout/appwidget4x1.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:minWidth="250dp" + android:minHeight="40dp" + android:background="@drawable/appwidget_bg" + android:orientation="horizontal" > + + <ImageView + android:id="@+id/appwidget_coverart" + android:layout_width="80dp" + android:layout_height="80dp" + android:layout_gravity="center_vertical" + android:clickable="true" + android:focusable="true" + android:src="@drawable/appwidget_art_default" /> + + <LinearLayout + android:id="@+id/linearLayout1" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/appwidget_top" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + android:background="@drawable/media_button"> + + <TextView + android:id="@+id/title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="20dip" + android:minHeight="16sp" + android:paddingLeft="4dip" + android:paddingRight="4dip" + android:paddingTop="4dip" + android:singleLine="true" + android:gravity="center_horizontal" + android:text="Title" + android:textColor="@color/appwidget_text" + android:textSize="16sp" + android:textStyle="bold" /> + + <TextView + android:id="@+id/artist" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="10dip" + android:minHeight="12sp" + android:paddingBottom="4dip" + android:paddingLeft="4dip" + android:paddingRight="4dip" + android:singleLine="true" + android:gravity="center_horizontal" + android:text="Artist" + android:textColor="@color/appwidget_text" + android:textSize="12sp" /> + </LinearLayout> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="horizontal" + android:paddingBottom="4dip" + android:paddingTop="4dip" > + + <ImageButton + android:id="@+id/control_previous" + android:layout_width="0dip" + android:layout_height="fill_parent" + android:layout_weight="1" + android:scaleType="center" + android:background="@drawable/media_button" + android:src="@drawable/ic_appwidget_music_previous" /> + + <ImageButton + android:id="@+id/control_play" + android:layout_width="0dip" + android:layout_height="fill_parent" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_play" + android:background="@drawable/media_button" /> + + <ImageButton + android:id="@+id/control_next" + android:layout_width="0dip" + android:layout_height="fill_parent" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_next" + android:background="@drawable/media_button" /> + </LinearLayout> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/appwidget4x2.xml b/res/layout/appwidget4x2.xml new file mode 100644 index 00000000..575ae1c2 --- /dev/null +++ b/res/layout/appwidget4x2.xml @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:minWidth="250dp" + android:minHeight="110dp" + android:background="@drawable/appwidget_bg" + android:orientation="horizontal" > + + <ImageView + android:id="@+id/appwidget_coverart" + android:layout_width="120dp" + android:layout_height="120dp" + android:layout_gravity="center_vertical" + android:clickable="true" + android:focusable="true" + android:src="@drawable/appwidget_art_default" /> + + <LinearLayout + android:id="@+id/linearLayout1" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/appwidget_top" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + android:paddingTop="4dip" + android:paddingBottom="4dip" + android:background="@drawable/media_button"> + + <TextView + android:id="@+id/title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="20dip" + android:minHeight="16sp" + android:paddingLeft="4dip" + android:paddingRight="4dip" + android:paddingTop="4dip" + android:paddingBottom="2dip" + android:singleLine="true" + android:gravity="center_horizontal" + android:text="Title" + android:textColor="@color/appwidget_text" + android:textSize="16sp" + android:textStyle="bold" /> + + <TextView + android:id="@+id/artist" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="10dip" + android:minHeight="12sp" + android:paddingLeft="4dip" + android:paddingRight="4dip" + android:paddingBottom="2dip" + android:singleLine="true" + android:gravity="center_horizontal" + android:text="Artist" + android:textColor="@color/appwidget_text" + android:textSize="12sp" /> + + <TextView + android:id="@+id/album" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="10dip" + android:minHeight="12sp" + android:paddingBottom="2dip" + android:paddingLeft="4dip" + android:paddingRight="4dip" + android:singleLine="true" + android:gravity="center_horizontal" + android:text="Album" + android:textColor="@color/appwidget_text" + android:textSize="12sp" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_gravity="bottom" + android:gravity="bottom" + android:paddingBottom="4dip" + android:paddingTop="4dip" >" + + <ImageButton + android:id="@+id/control_previous" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:scaleType="center" + android:background="@drawable/media_button" + android:src="@drawable/ic_appwidget_music_previous" /> + + <ImageButton + android:id="@+id/control_play" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_play" + android:background="@drawable/media_button" /> + + <ImageButton + android:id="@+id/control_next" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_next" + android:background="@drawable/media_button" /> + </LinearLayout> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/appwidget4x3.xml b/res/layout/appwidget4x3.xml new file mode 100644 index 00000000..b4f685bc --- /dev/null +++ b/res/layout/appwidget4x3.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@drawable/appwidget_bg" + android:orientation="vertical" > + + <ImageView + android:id="@+id/appwidget_coverart" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:scaleType="fitCenter" + android:layout_weight="1" + android:layout_gravity="center_horizontal" + android:clickable="true" + android:focusable="true" + android:paddingTop="6dip" + android:paddingBottom="6dip" + android:src="@drawable/appwidget_art_default" /> + + <LinearLayout + android:id="@+id/linearLayout1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/appwidget_top" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + android:paddingBottom="4dip" + android:paddingTop="4dip" + android:background="@drawable/media_button"> + + <TextView + android:id="@+id/title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="20dip" + android:minHeight="16sp" + android:paddingLeft="5dip" + android:paddingRight="5dip" + android:singleLine="true" + android:textColor="@color/appwidget_text" + android:textSize="16sp" + android:text="Title" + android:layout_gravity="center_horizontal" + android:gravity="center" + android:textStyle="bold" /> + + <TextView + android:id="@+id/artist" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="10dip" + android:minHeight="12sp" + android:paddingBottom="2dip" + android:paddingLeft="5dip" + android:singleLine="true" + android:text="Artist" + android:layout_gravity="center_horizontal" + android:gravity="center" + android:textColor="@color/appwidget_text" + android:textSize="12sp" /> + </LinearLayout> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:gravity="bottom" + android:orientation="horizontal" + android:paddingBottom="4dip"> + + <ImageButton + android:id="@+id/control_previous" + android:layout_width="0dip" + android:layout_height="56dip" + android:layout_weight="1" + android:scaleType="center" + android:background="@drawable/media_button" + android:src="@drawable/ic_appwidget_music_previous" /> + + <ImageButton + android:id="@+id/control_play" + android:layout_width="0dip" + android:layout_height="56dip" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_play" + android:background="@drawable/media_button" /> + + <ImageButton + android:id="@+id/control_next" + android:layout_width="0dip" + android:layout_height="56dip" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_next" + android:background="@drawable/media_button" /> + </LinearLayout> + + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/appwidget4x4.xml b/res/layout/appwidget4x4.xml new file mode 100644 index 00000000..6e6c12ab --- /dev/null +++ b/res/layout/appwidget4x4.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" + android:background="@drawable/appwidget_bg" > + + <ImageView + android:id="@+id/appwidget_coverart" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:scaleType="fitCenter" + android:layout_weight="1" + android:layout_gravity="center_horizontal" + android:clickable="true" + android:focusable="true" + android:layout_margin="6dip" + android:paddingTop="6dip" + android:paddingBottom="6dip" + android:src="@drawable/appwidget_art_default" /> + + <LinearLayout + android:id="@+id/linearLayout1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/appwidget_top" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + android:paddingTop="4dip" + android:paddingBottom="4dip" + android:background="@drawable/media_button"> + + <TextView + android:id="@+id/title" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="20dip" + android:minHeight="16sp" + android:paddingLeft="5dip" + android:paddingRight="5dip" + android:singleLine="true" + android:textColor="@color/appwidget_text" + android:textSize="16sp" + android:text="Title" + android:layout_gravity="center_horizontal" + android:gravity="center" + android:textStyle="bold" /> + + <TextView + android:id="@+id/artist" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:fadingEdge="horizontal" + android:fadingEdgeLength="10dip" + android:minHeight="12sp" + android:paddingBottom="2dip" + android:paddingLeft="5dip" + android:singleLine="true" + android:text="Artist" + android:layout_gravity="center_horizontal" + android:gravity="center" + android:textColor="@color/appwidget_text" + android:textSize="12sp" /> + </LinearLayout> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:gravity="bottom" + android:orientation="horizontal" + android:paddingBottom="4dip" + android:paddingTop="4dip" > + + <ImageButton + android:id="@+id/control_previous" + android:layout_width="0dip" + android:layout_height="56dip" + android:layout_weight="1" + android:scaleType="center" + android:background="@drawable/media_button" + android:src="@drawable/ic_appwidget_music_previous" /> + + <ImageButton + android:id="@+id/control_play" + android:layout_width="0dip" + android:layout_height="56dip" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_play" + android:background="@drawable/media_button" /> + + <ImageButton + android:id="@+id/control_next" + android:layout_width="0dip" + android:layout_height="56dip" + android:layout_weight="1" + android:scaleType="center" + android:src="@drawable/ic_appwidget_music_next" + android:background="@drawable/media_button" /> + </LinearLayout> + + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/artist_list_item.xml b/res/layout/artist_list_item.xml new file mode 100644 index 00000000..3684e176 --- /dev/null +++ b/res/layout/artist_list_item.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@android:color/transparent"> + + <TextView + android:id="@+id/artist_name" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceMedium" + android:gravity="left|center_vertical" + android:paddingLeft="6dip" + android:paddingRight="6dip" + android:minHeight="50dip" + android:background="@android:color/transparent"/> + + <ImageButton + android:id="@+id/artist_star" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right|center_vertical" + android:src="@drawable/ic_stat_star" + android:background="@android:color/transparent" + android:focusable="false"/> + + <ImageView + android:id="@+id/artist_more" + android:src="@drawable/list_item_more" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:layout_gravity="right|center_vertical" + android:paddingRight="6dip" + android:background="@drawable/menubar_button"/> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/button_bar.xml b/res/layout/button_bar.xml new file mode 100644 index 00000000..8f49c99a --- /dev/null +++ b/res/layout/button_bar.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/button_bar"
+ android:layout_gravity="bottom"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:background="@android:color/transparent">
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_home"
+ android:text="@string/button_bar.home"/>
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_music"
+ android:text="@string/button_bar.browse"/>
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_playlists"
+ android:text="@string/button_bar.playlists"/>
+
+ <Button style="@style/MenuBarButton"
+ android:id="@+id/button_bar_now_playing"
+ android:text="@string/button_bar.now_playing"/>
+</LinearLayout>
+
diff --git a/res/layout/chat.xml b/res/layout/chat.xml new file mode 100644 index 00000000..fdeb5b36 --- /dev/null +++ b/res/layout/chat.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" > + + <include layout="@layout/tab_progress" /> + + <ListView + android:id="@+id/chat_entries" + android:layout_width="fill_parent" + android:layout_height="0dip" + android:layout_weight="1.0" + android:textFilterEnabled="true" /> + + <LinearLayout + android:layout_height="4dip" + android:layout_width="fill_parent" + android:layout_marginTop="4dip"/> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="bottom" > + + <EditText + android:id="@+id/chat_edittext" + android:layout_width="0dip" + android:layout_height="40dip" + android:layout_weight="1" + android:autoLink="all" + android:hint="@string/chat.send_a_message" + android:inputType="textEmailAddress|textMultiLine" + android:linksClickable="true" + android:paddingBottom="10dip" + android:paddingTop="10dip" /> + + <ImageButton + android:id="@+id/chat_send" + android:layout_width="60dip" + android:layout_height="40dip" + android:src="?attr/chat_send" /> + + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/chat_item.xml b/res/layout/chat_item.xml new file mode 100644 index 00000000..b44631d1 --- /dev/null +++ b/res/layout/chat_item.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <TextView + android:id="@+id/chat_username" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="6dip" + android:layout_marginRight="6dip" + android:ellipsize="marquee" + android:singleLine="true" + android:text="User" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:textColorSecondary"/> + + <LinearLayout + android:id="@+id/chat_message_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="2dip" + android:orientation="horizontal" > + + <TextView + android:id="@+id/chat_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="6dip" + android:singleLine="true" + android:text="00:00" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <TextView + android:id="@+id/chat_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="6dip" + android:layout_marginRight="6dip" + android:autoLink="all" + android:linksClickable="true" + android:singleLine="false" + android:text="Message Text Goes Here" + android:textAppearance="?android:attr/textAppearanceMedium" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/chat_item_reverse.xml b/res/layout/chat_item_reverse.xml new file mode 100644 index 00000000..62695521 --- /dev/null +++ b/res/layout/chat_item_reverse.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <TextView + android:id="@+id/chat_username" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="6dip" + android:gravity="right" + android:layout_gravity="right" + android:ellipsize="marquee" + android:singleLine="true" + android:text="User" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:textColorSecondary"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="2dip" + android:orientation="horizontal" + android:layout_gravity="right" > + + <TextView + android:id="@+id/chat_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="6dip" + android:singleLine="true" + android:gravity="right" + android:text="00:00" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <TextView + android:id="@+id/chat_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="6dip" + android:layout_marginRight="6dip" + android:autoLink="all" + android:linksClickable="true" + android:singleLine="false" + android:gravity="right" + android:text="Chat message" + android:textAppearance="?android:attr/textAppearanceMedium" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/create_podcast.xml b/res/layout/create_podcast.xml new file mode 100644 index 00000000..5a2ec970 --- /dev/null +++ b/res/layout/create_podcast.xml @@ -0,0 +1,27 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/create_podcast_url_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/select_podcasts.add_url"/> + <EditText + android:id="@+id/create_podcast_url" + android:inputType="textUri" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:text="http://"/> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/download_activity.xml b/res/layout/download_activity.xml new file mode 100644 index 00000000..3a1aa5e4 --- /dev/null +++ b/res/layout/download_activity.xml @@ -0,0 +1,4 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/download_container" + android:layout_width="match_parent" + android:layout_height="match_parent" />
\ No newline at end of file diff --git a/res/layout/download_media_buttons.xml b/res/layout/download_media_buttons.xml new file mode 100644 index 00000000..1835a373 --- /dev/null +++ b/res/layout/download_media_buttons.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@android:color/transparent"> + + <ImageButton + style="@style/PlaybackControl.Small" + android:id="@+id/download_repeat" + android:src="?attr/media_button_repeat_off" + android:layout_alignParentLeft="true" + android:layout_centerVertical="true" + /> + + <github.daneren2005.dsub.view.AutoRepeatButton + style="@style/PlaybackControl" + android:id="@+id/download_previous" + android:src="?attr/media_button_backward" + android:layout_toLeftOf="@+id/download_pause" + android:layout_centerVertical="true" + /> + + <ImageButton + style="@style/PlaybackControl" + android:id="@+id/download_pause" + android:src="?attr/media_button_pause" + android:layout_centerInParent="true" + /> + + <ImageButton + style="@style/PlaybackControl" + android:id="@+id/download_stop" + android:src="?attr/media_button_stop" + android:layout_centerInParent="true" + /> + + <ImageButton + style="@style/PlaybackControl" + android:id="@+id/download_start" + android:src="?attr/media_button_start" + android:layout_centerInParent="true" + /> + + <github.daneren2005.dsub.view.AutoRepeatButton + style="@style/PlaybackControl" + android:id="@+id/download_next" + android:src="?attr/media_button_forward" + android:layout_toRightOf="@+id/download_start" + android:layout_centerVertical="true" + /> + + <ImageButton + style="@style/PlaybackControl.Small" + android:id="@+id/download_toggle_list" + android:src="@drawable/action_toggle_list" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + /> +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/download_playlist.xml b/res/layout/download_playlist.xml new file mode 100644 index 00000000..e37981e2 --- /dev/null +++ b/res/layout/download_playlist.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <TextView
+ android:id="@+id/download_empty"
+ android:text="@string/download.empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"/>
+
+ <com.mobeta.android.dslv.DragSortListView
+ style="@style/DragDropListView"
+ android:id="@+id/download_list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:cacheColorHint="#00000000"
+ android:fastScrollEnabled="true"/>
+
+</LinearLayout>
\ No newline at end of file diff --git a/res/layout/download_slider.xml b/res/layout/download_slider.xml new file mode 100644 index 00000000..d4998eea --- /dev/null +++ b/res/layout/download_slider.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:background="@android:color/transparent"
+ android:paddingBottom="10dip">
+
+ <TextView
+ android:id="@+id/download_position"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:paddingLeft="8dip"
+ android:text="0:00"
+ android:textSize="12sp"
+ android:textColor="?android:textColorPrimary"
+ android:paddingBottom="4dip"/>
+
+ <SeekBar
+ android:id="@+id/download_progress_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:indeterminate="false"
+ android:paddingLeft="55dip"
+ android:paddingRight="55dip"
+ android:paddingTop="3dip"
+ android:paddingBottom="7dip" />
+
+ <TextView
+ android:id="@+id/download_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingRight="8dip"
+ android:text="-:--"
+ android:textSize="12sp"
+ android:textColor="?android:textColorPrimary"
+ android:paddingBottom="4dip"/>
+</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/equalizer.xml b/res/layout/equalizer.xml new file mode 100644 index 00000000..ee1a9560 --- /dev/null +++ b/res/layout/equalizer.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@drawable/album_art_background"
+ android:padding="16dip">
+
+ <CheckBox
+ android:id="@+id/equalizer_enabled"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/equalizer.enabled"
+ android:textColor="#c0c0c0"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/equalizer_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/equalizer_preset"
+ android:text="@string/equalizer.preset"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginTop="20dip"
+ android:paddingLeft="40dip"
+ android:paddingRight="40dip"/>
+
+ </LinearLayout>
+ </ScrollView>
+
+</LinearLayout>
+
diff --git a/res/layout/equalizer_bar.xml b/res/layout/equalizer_bar.xml new file mode 100644 index 00000000..c34d1108 --- /dev/null +++ b/res/layout/equalizer_bar.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/equalizer.frequency"
+ android:textSize="12sp"
+ android:textColor="#c0c0c0"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_alignParentLeft="true"
+ />
+
+ <TextView
+ android:id="@+id/equalizer.level"
+ android:text="0 dB"
+ android:textSize="12sp"
+ android:textColor="#c0c0c0"
+ android:gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/equalizer.frequency"
+ />
+
+ <SeekBar
+ android:id="@+id/equalizer.bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/equalizer.frequency"
+ />
+
+
+</RelativeLayout>
+
diff --git a/res/layout/help.xml b/res/layout/help.xml new file mode 100644 index 00000000..f22dee37 --- /dev/null +++ b/res/layout/help.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <LinearLayout android:id="@+id/help_buttons" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_alignParentBottom="true" + android:padding="4dip" + android:gravity="center_horizontal" + android:background="#ffcccccc"> + + <Button android:id="@+id/help_back" + android:text="@string/help.back" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:layout_marginRight="5dip" + android:paddingLeft="25dip" + android:paddingRight="25dip"/> + + <Button android:id="@+id/help_close" + android:text="@string/help.close" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:layout_marginLeft="5dip" + android:paddingLeft="25dip" + android:paddingRight="25dip"/> + </LinearLayout> + + + <WebView + android:id="@+id/help_contents" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@id/help_buttons" + android:layout_weight="1" + android:fadingEdge="vertical" + android:fadingEdgeLength="12dip"/> + + </RelativeLayout> diff --git a/res/layout/home.xml b/res/layout/home.xml new file mode 100644 index 00000000..018061fa --- /dev/null +++ b/res/layout/home.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/home_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <ListView
+ android:id="@+id/main_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0px"
+ android:layout_weight="1"/>
+
+ <View android:id="@+id/main_dummy"
+ android:layout_width="0px"
+ android:layout_height="0px"/>
+</LinearLayout>
+
diff --git a/res/layout/jukebox_volume.xml b/res/layout/jukebox_volume.xml new file mode 100644 index 00000000..e124734b --- /dev/null +++ b/res/layout/jukebox_volume.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/toast_layout_root" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@drawable/toast_frame"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/download.jukebox_volume" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="#ffffffff" + android:shadowColor="#bb000000" + android:shadowRadius="2.75" + android:paddingLeft="32dp" + android:paddingRight="32dp" + android:paddingBottom="12dp" + /> + + <ProgressBar android:id="@+id/jukebox_volume_progress_bar" + style="@android:style/Widget.ProgressBar.Horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingBottom="3dp" + /> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/lyrics.xml b/res/layout/lyrics.xml new file mode 100644 index 00000000..4307d8dd --- /dev/null +++ b/res/layout/lyrics.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <include layout="@layout/tab_progress"/>
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <TextView
+ android:id="@+id/lyrics_artist"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingTop="10dip"
+ android:paddingBottom="4dip"
+ />
+
+ <TextView
+ android:id="@+id/lyrics_title"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ />
+
+ <TextView
+ android:id="@+id/lyrics_text"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+ <include layout="@layout/button_bar"/>
+
+</LinearLayout>
+
diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100644 index 00000000..f1509db6 --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,81 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center_horizontal" + android:orientation="vertical" > + + <android.support.v4.view.ViewPager + android:id="@+id/pager" + android:layout_width="fill_parent" + android:layout_height="0px" + android:layout_weight="1" > + </android.support.v4.view.ViewPager> + + <View + android:layout_width="fill_parent" + android:layout_height="1px" + android:background="@color/dividerColor"/> + + <LinearLayout + android:id="@+id/bottom_bar" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@drawable/media_button" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/album_art" + android:layout_width="50dip" + android:layout_height="50dip" + android:layout_gravity="left|center" + android:scaleType="fitStart" + android:src="@drawable/unknown_album"/> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical" + android:paddingLeft="8dip"> + + <TextView + android:id="@+id/track_name" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:textColor="?android:textColorPrimary" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="13sp" + android:text="@string/search.artists"/> + + <TextView + android:id="@+id/artist_name" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:textColor="?android:textColorSecondary" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="12sp" + android:text="@string/search.albums"/> + </LinearLayout> + + <ImageButton + style="@style/PlaybackControl.Small" + android:id="@+id/download_previous" + android:src="?attr/media_button_backward" + android:layout_centerVertical="true"/> + + <ImageButton + style="@style/PlaybackControl.Small" + android:id="@+id/download_start" + android:src="?attr/media_button_start" + android:layout_centerVertical="true"/> + + <ImageButton + style="@style/PlaybackControl.Small" + android:id="@+id/download_next" + android:src="?attr/media_button_forward" + android:layout_centerVertical="true"/> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/main_buttons.xml b/res/layout/main_buttons.xml new file mode 100644 index 00000000..1e60838d --- /dev/null +++ b/res/layout/main_buttons.xml @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/main_select_server"
+ android:orientation="horizontal"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="6dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:src="@drawable/main_select_server"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/main.select_server_1"
+ android:text="@string/main.select_server"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView android:id="@+id/main.select_server_2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/main_offline"
+ android:text="@string/main.offline"
+ android:drawablePadding="12dip"
+ android:drawableLeft="?attr/offline_icon"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dp"
+ android:paddingBottom="4dp"
+ android:minHeight="50dip"/>
+
+ <TextView
+ android:id="@+id/main_albums"
+ android:text="@string/main.albums_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/cyan"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dp"
+ android:textAllCaps="true"
+ android:textStyle="bold"
+ android:textSize="16sp"/>
+
+ <TextView
+ android:id="@+id/main_albums_newest"
+ android:text="@string/main.albums_newest"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_recent"
+ android:text="@string/main.albums_recent"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_frequent"
+ android:text="@string/main.albums_frequent"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_highest"
+ android:text="@string/main.albums_highest"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_starred"
+ android:text="@string/main.albums_starred"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_genres"
+ android:text="@string/main.albums_genres"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+ <TextView
+ android:id="@+id/main_albums_random"
+ android:text="@string/main.albums_random"
+ android:drawableRight="@drawable/list_item_more"
+ android:drawablePadding="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center_vertical"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"
+ android:minHeight="50dip"/>
+
+</LinearLayout>
+
diff --git a/res/layout/notification.xml b/res/layout/notification.xml new file mode 100644 index 00000000..22e2cb63 --- /dev/null +++ b/res/layout/notification.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/statusbar" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="horizontal" > + + <ImageView + android:id="@+id/notification_image" + android:layout_width="64.0dip" + android:layout_height="64.0dip" + android:layout_weight="0.0" + android:gravity="center" /> + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" + android:paddingLeft="11.0dip"> + + <TextView + android:id="@+id/notification_title" + style="@android:style/TextAppearance.StatusBar.EventContent.Title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:ellipsize="marquee" + android:focusable="true" + android:singleLine="true" /> + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="horizontal" > + + <LinearLayout + android:layout_width="0.0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1.0" + android:orientation="vertical"> + + <TextView + android:id="@+id/notification_artist" + style="@android:style/TextAppearance.StatusBar.EventContent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:ellipsize="end" + android:scrollHorizontally="true" + android:singleLine="true" /> + + <TextView + android:id="@+id/notification_album" + style="@android:style/TextAppearance.StatusBar.EventContent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:ellipsize="end" + android:scrollHorizontally="true" + android:singleLine="true" /> + </LinearLayout> + + <ImageButton + android:id="@+id/control_previous" + android:src="@drawable/notification_prev" + android:background="@drawable/btn_bg" + android:layout_width="34dip" + android:layout_height="34dip" + android:layout_gravity="center|right" + android:layout_marginRight="10dip" + android:layout_marginTop="2dip" + android:layout_weight="0.0" + android:scaleType="fitXY"/> + + <ImageButton + android:id="@+id/control_pause" + android:src="@drawable/notification_pause" + android:background="@drawable/btn_bg" + android:layout_width="34dip" + android:layout_height="34dip" + android:layout_gravity="center|right" + android:layout_marginRight="10dip" + android:layout_marginTop="2dip" + android:layout_weight="0.0" + android:scaleType="fitXY"/> + + <ImageButton + android:id="@+id/control_next" + android:src="@drawable/notification_next" + android:background="@drawable/btn_bg" + android:layout_width="34dip" + android:layout_height="34dip" + android:layout_gravity="center|right" + android:layout_marginRight="10dip" + android:layout_marginTop="2dip" + android:layout_weight="0.0" + android:scaleType="fitXY"/> + </LinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/res/layout/notification_expanded.xml b/res/layout/notification_expanded.xml new file mode 100644 index 00000000..70e7269c --- /dev/null +++ b/res/layout/notification_expanded.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/statusbar" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="horizontal" > + + <ImageView + android:id="@+id/notification_image" + android:layout_width="128dp" + android:layout_height="128dp" + android:gravity="center" /> + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="0.0" + android:orientation="vertical" + android:paddingLeft="11.0dip" > + + <TextView + android:id="@+id/notification_title" + style="@android:style/TextAppearance.StatusBar.EventContent.Title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:ellipsize="marquee" + android:focusable="true" + android:singleLine="true" /> + + <TextView + android:id="@+id/notification_artist" + style="@android:style/TextAppearance.StatusBar.EventContent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:ellipsize="end" + android:scrollHorizontally="true" + android:singleLine="true" /> + + <TextView + android:id="@+id/notification_album" + style="@android:style/TextAppearance.StatusBar.EventContent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left" + android:ellipsize="end" + android:scrollHorizontally="true" + android:singleLine="true" /> + + <TextView + android:id="@+id/textView1" + android:layout_width="wrap_content" + android:layout_height="fill_parent" /> + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="center|fill" + android:gravity="center_horizontal" + android:orientation="horizontal" > + + <ImageButton + android:id="@+id/control_previous" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_weight="0.0" + android:background="@drawable/btn_bg" + android:scaleType="fitXY" + android:src="@drawable/notification_prev" /> + + <ImageButton + android:id="@+id/control_pause" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:layout_weight="0.0" + android:background="@drawable/btn_bg" + android:scaleType="fitXY" + android:src="@drawable/notification_pause" /> + + <ImageButton + android:id="@+id/control_next" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_weight="0.0" + android:background="@drawable/btn_bg" + android:scaleType="fitXY" + android:src="@drawable/notification_next" /> + </LinearLayout> + + </LinearLayout> + +</LinearLayout> diff --git a/res/layout/play_video.xml b/res/layout/play_video.xml new file mode 100644 index 00000000..6a9f3f74 --- /dev/null +++ b/res/layout/play_video.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <WebView + android:id="@+id/play_video_contents" + android:layout_width="fill_parent" + android:layout_height="fill_parent"/> + +</FrameLayout> diff --git a/res/layout/playlist_list_item.xml b/res/layout/playlist_list_item.xml new file mode 100644 index 00000000..1ec5753f --- /dev/null +++ b/res/layout/playlist_list_item.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/playlist_name" + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceMedium" + android:gravity="left|center_vertical" + android:paddingLeft="6dip" + android:paddingRight="6dip" + android:minHeight="50dip"/> + + <ImageView + android:id="@+id/playlist_more" + android:src="@drawable/list_item_more" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:layout_gravity="right|center_vertical" + android:paddingRight="6dip" + android:background="@drawable/menubar_button"/> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/progress.xml b/res/layout/progress.xml new file mode 100644 index 00000000..4a693cb3 --- /dev/null +++ b/res/layout/progress.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:padding="10dp">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_marginRight="10dp"/>
+
+ <TextView
+ android:id="@+id/progress_message"
+ android:text="@string/progress.wait"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"/>
+
+</LinearLayout>
\ No newline at end of file diff --git a/res/layout/save_playlist.xml b/res/layout/save_playlist.xml new file mode 100644 index 00000000..43f1827a --- /dev/null +++ b/res/layout/save_playlist.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/save_playlist_root"
+ android:padding="10dip"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <EditText
+ android:id="@+id/save_playlist_name"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text"
+ android:singleLine="true"/>
+
+ <CheckBox
+ android:id="@+id/save_playlist_overwrite"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/playlist.overwrite"
+ android:layout_marginLeft="4dp"
+ android:checked="false"
+ android:visibility="gone"/>
+
+</LinearLayout>
+
diff --git a/res/layout/search.xml b/res/layout/search.xml new file mode 100644 index 00000000..d1c5c84c --- /dev/null +++ b/res/layout/search.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <ListView
+ android:id="@+id/search_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"
+ />
+</LinearLayout>
\ No newline at end of file diff --git a/res/layout/search_buttons.xml b/res/layout/search_buttons.xml new file mode 100644 index 00000000..10b72166 --- /dev/null +++ b/res/layout/search_buttons.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/search_search"
+ android:text="@string/search.search"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="0dp"
+ android:drawableLeft="@drawable/search"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center"
+ android:padding="12dp"/>
+
+ <TextView
+ android:id="@+id/search_artists"
+ android:text="@string/search.artists"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_albums"
+ android:text="@string/search.albums"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_songs"
+ android:text="@string/search.songs"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#EFEFEF"
+ android:textStyle="bold"
+ android:background="#A5A5A5"
+ android:gravity="center_vertical"
+ android:paddingLeft="4dp"/>
+
+ <TextView
+ android:id="@+id/search_more_artists"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/search_more_albums"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/search_more_songs"
+ android:text="@string/search.more"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:gravity="center"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+</LinearLayout>
+
diff --git a/res/layout/select_album.xml b/res/layout/select_album.xml new file mode 100644 index 00000000..01df495a --- /dev/null +++ b/res/layout/select_album.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_album_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <TextView
+ android:id="@+id/select_album_empty"
+ android:text="@string/select_album.empty"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"/>
+
+ <com.mobeta.android.dslv.DragSortListView
+ style="@style/DragDropListView"
+ android:id="@+id/select_album_entries"
+ android:textFilterEnabled="true"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+</LinearLayout>
\ No newline at end of file diff --git a/res/layout/select_album_footer.xml b/res/layout/select_album_footer.xml new file mode 100644 index 00000000..c1a30a1a --- /dev/null +++ b/res/layout/select_album_footer.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:background="@android:color/transparent"
+ android:paddingTop="6dp"
+ android:paddingBottom="0dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <Button android:id="@+id/select_album_more"
+ android:text="@string/select_album.more"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"
+ android:layout_marginLeft="6dp"
+ android:layout_marginRight="6dp"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="fill_parent"/>
+
+</LinearLayout>
+
diff --git a/res/layout/select_album_header.xml b/res/layout/select_album_header.xml new file mode 100644 index 00000000..2bf74110 --- /dev/null +++ b/res/layout/select_album_header.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/select_album_header" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/select_album_art" + android:src="@drawable/unknown_album_large" + android:layout_width="120dip" + android:layout_height="120dip" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_marginRight="10dip" + android:scaleType="fitCenter" + android:contentDescription="@null"/> + + <TextView + android:text="This is the album title" + android:id="@+id/select_album_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toRightOf="@+id/select_album_art" + android:paddingTop="20dip" + android:paddingBottom="8dip" + android:paddingRight="4dip" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + android:singleLine="true" + android:ellipsize="end"/> + + <TextView + android:text="This is the artist name" + android:id="@+id/select_album_artist" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/select_album_title" + android:layout_toRightOf="@+id/select_album_art" + android:paddingBottom="2dip" + android:paddingRight="4dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:singleLine="true" + android:ellipsize="end"/> + + <TextView + android:text="XX SONGS" + android:id="@+id/select_album_song_count" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/select_album_artist" + android:layout_toRightOf="@+id/select_album_art" + android:paddingRight="4dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="10sp" + android:singleLine="true" + android:ellipsize="none"/> + + <TextView + android:text="0:00" + android:id="@+id/select_album_song_length" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/select_album_song_count" + android:layout_toRightOf="@+id/select_album_art" + android:paddingRight="4dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textSize="10sp" + android:singleLine="true" + android:ellipsize="none"/> +</RelativeLayout> + + diff --git a/res/layout/select_artist.xml b/res/layout/select_artist.xml new file mode 100644 index 00000000..fef51d3c --- /dev/null +++ b/res/layout/select_artist.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_artist_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <ListView android:id="@+id/select_artist_list"
+ android:textFilterEnabled="true"
+ android:fastScrollEnabled="true"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"/>
+</LinearLayout>
+
diff --git a/res/layout/select_artist_header.xml b/res/layout/select_artist_header.xml new file mode 100644 index 00000000..0b3d151b --- /dev/null +++ b/res/layout/select_artist_header.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/select_artist_folder"
+ android:orientation="horizontal"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:paddingLeft="6dp"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight">
+
+ <ImageView
+ android:src="@drawable/main_select_server"
+ android:layout_gravity="center_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView android:id="@+id/select_artist_folder_1"
+ android:text="@string/select_artist.folder"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView android:id="@+id/select_artist_folder_2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file diff --git a/res/layout/select_genres.xml b/res/layout/select_genres.xml new file mode 100644 index 00000000..95f9d415 --- /dev/null +++ b/res/layout/select_genres.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/select_genre_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" > + + <include layout="@layout/tab_progress" /> + + <TextView + android:id="@+id/select_genre_empty" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="10dip" + android:text="@string/select_genre.empty" + android:visibility="gone" /> + + <ListView + android:id="@+id/select_genre_list" + android:layout_width="fill_parent" + android:layout_height="0dip" + android:layout_weight="1.0" + android:textFilterEnabled="true" + android:fastScrollEnabled="true"/> + </LinearLayout> +</FrameLayout>
\ No newline at end of file diff --git a/res/layout/select_playlist.xml b/res/layout/select_playlist.xml new file mode 100644 index 00000000..e18283bd --- /dev/null +++ b/res/layout/select_playlist.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/select_playlist_layout"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1px"
+ android:background="@color/dividerColor"/>
+
+ <include layout="@layout/tab_progress"/>
+
+ <TextView
+ android:id="@+id/select_playlist_empty"
+ android:text="@string/select_playlist.empty"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dip"
+ android:visibility="gone"/>
+
+ <ListView android:id="@+id/select_playlist_list"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1.0"
+ android:fastScrollEnabled="true"/>
+
+</LinearLayout>
+
diff --git a/res/layout/select_podcasts.xml b/res/layout/select_podcasts.xml new file mode 100644 index 00000000..ea4fb07c --- /dev/null +++ b/res/layout/select_podcasts.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/select_podcasts_layout" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" > + + <View + android:layout_width="fill_parent" + android:layout_height="1px" + android:background="@color/dividerColor"/> + + <include layout="@layout/tab_progress" /> + + <TextView + android:id="@+id/select_podcasts_empty" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="10dip" + android:text="@string/select_podcasts.empty" + android:visibility="gone" /> + + <ListView + android:id="@+id/select_podcasts_list" + android:layout_width="fill_parent" + android:layout_height="0dip" + android:layout_weight="1.0" + android:fastScrollEnabled="true"/> +</LinearLayout> diff --git a/res/layout/shuffle_dialog.xml b/res/layout/shuffle_dialog.xml new file mode 100644 index 00000000..295f57cb --- /dev/null +++ b/res/layout/shuffle_dialog.xml @@ -0,0 +1,80 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/start_year_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/shuffle.startYear" /> + <EditText + android:id="@+id/start_year" + android:inputType="number" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:hint="@string/shuffle.startYear" /> + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/end_year_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/shuffle.endYear" /> + <EditText + android:id="@+id/end_year" + android:inputType="number" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:hint="@string/shuffle.endYear" /> + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/genre_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/shuffle.genre" /> + <EditText + android:id="@+id/genre" + android:inputType="text" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:hint="@string/shuffle.genre"/> + + <Button + android:id="@+id/genre_combo" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:text="@string/shuffle.genre" + style="?android:attr/spinnerStyle"/> + </LinearLayout> +</LinearLayout> diff --git a/res/layout/song_list_item.xml b/res/layout/song_list_item.xml new file mode 100644 index 00000000..90060894 --- /dev/null +++ b/res/layout/song_list_item.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/drag_handle"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+ <CheckedTextView
+ android:id="@+id/song_check"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:gravity="center_vertical"
+ android:checkMark="@drawable/btn_check"
+ android:paddingLeft="3dip"/>
+
+ <LinearLayout android:orientation="vertical"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/song_title"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:drawablePadding="6dip"
+ android:paddingLeft="6dip"
+ android:paddingRight="6dip"/>
+
+ <ImageButton
+ android:id="@+id/song_star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:src="@drawable/ic_stat_star"
+ android:background="@null"
+ android:focusable="false"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/song_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:drawablePadding="1dip"
+ android:paddingRight="6dip"/>
+ </LinearLayout>
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/song_artist"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="left|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="middle"
+ android:paddingLeft="6dip"/>
+
+ <TextView
+ android:id="@+id/song_duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|center_vertical"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:paddingLeft="3dip"
+ android:paddingRight="4dip"/>
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/artist_more"
+ android:src="@drawable/list_item_more"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_gravity="right|center_vertical"
+ android:paddingRight="6dip"
+ android:background="@drawable/menubar_button"/>
+</LinearLayout>
diff --git a/res/layout/start_timer.xml b/res/layout/start_timer.xml new file mode 100644 index 00000000..3b607a44 --- /dev/null +++ b/res/layout/start_timer.xml @@ -0,0 +1,27 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/timer_length_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/download.timer_length" /> + <EditText + android:id="@+id/timer_length" + android:inputType="number" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:hint="@string/download.timer_length" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/sync_dialog.xml b/res/layout/sync_dialog.xml new file mode 100644 index 00000000..5133b753 --- /dev/null +++ b/res/layout/sync_dialog.xml @@ -0,0 +1,12 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + <CheckBox + android:id="@+id/sync_default" + style="?android:attr/textAppearanceMedium" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="5dp" + android:checked="false" + android:text="@string/offline.sync_dialog_default"/> +</FrameLayout>
\ No newline at end of file diff --git a/res/layout/tab_progress.xml b/res/layout/tab_progress.xml new file mode 100644 index 00000000..6a88600c --- /dev/null +++ b/res/layout/tab_progress.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tab_progress"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:padding="10dp"
+ android:layout_gravity="top"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ProgressBar
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginRight="6dp"/>
+
+ <TextView
+ android:id="@+id/tab_progress_message"
+ android:text="@string/progress.wait"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"/>
+
+</LinearLayout>
\ No newline at end of file diff --git a/res/layout/update_playlist.xml b/res/layout/update_playlist.xml new file mode 100644 index 00000000..7354ef5c --- /dev/null +++ b/res/layout/update_playlist.xml @@ -0,0 +1,70 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/get_playlist_name_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/common.name" /> + <EditText + android:id="@+id/get_playlist_name" + android:inputType="text" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:hint="@string/common.name" /> + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/get_playlist_comment_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/common.comment" /> + <EditText + android:id="@+id/get_playlist_comment" + android:inputType="text" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:hint="@string/common.comment" /> + </LinearLayout> + + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/get_playlist_public_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:textSize="20dp" + android:text="@string/common.public" /> + <CheckBox + android:id="@+id/get_playlist_public" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginLeft="4dp" + android:checked="false"/> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/res/menu/chat.xml b/res/menu/chat.xml new file mode 100644 index 00000000..e0f9a718 --- /dev/null +++ b/res/menu/chat.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/action_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu>
\ No newline at end of file diff --git a/res/menu/empty.xml b/res/menu/empty.xml new file mode 100644 index 00000000..b6db96aa --- /dev/null +++ b/res/menu/empty.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> +</menu> diff --git a/res/menu/main.xml b/res/menu/main.xml new file mode 100644 index 00000000..c9420236 --- /dev/null +++ b/res/menu/main.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_search" + android:icon="@drawable/action_search" + android:title="@string/menu.search" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/menu.shuffle" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_help" + android:icon="@drawable/action_help" + android:title="@string/menu.help"/> + + <item + android:id="@+id/menu_about" + android:icon="@drawable/action_help" + android:title="@string/menu.about"/> + + <item + android:id="@+id/menu_log" + android:title="@string/menu.log"/> + + <item + android:id="@+id/menu_changelog" + android:title="@string/changelog_full_title"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/action_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> + +</menu> diff --git a/res/menu/nowplaying.xml b/res/menu/nowplaying.xml new file mode 100644 index 00000000..572c5bac --- /dev/null +++ b/res/menu/nowplaying.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/download.menu_shuffle" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_remove_all" + android:icon="@drawable/action_remove_all" + android:title="@string/download.menu_remove_all" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_save_playlist" + android:icon="@drawable/action_save" + android:title="@string/download.menu_save" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_screen_on_off" + android:icon="@drawable/action_screen_on_off" + android:title="@string/download.menu_screen_on" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_toggle_timer" + android:title="@string/download.start_timer"/> + + <item + android:id="@+id/menu_toggle_now_playing" + android:title="@string/download.show_downloading"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu> diff --git a/res/menu/nowplaying_context.xml b/res/menu/nowplaying_context.xml new file mode 100644 index 00000000..f42c3644 --- /dev/null +++ b/res/menu/nowplaying_context.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/menu_show_album" + android:title="@string/download.menu_show_album"/> + + <item + android:id="@+id/menu_lyrics" + android:title="@string/download.menu_lyrics"/> + + <item + android:id="@+id/menu_remove" + android:title="@string/download.menu_remove"/> + + <item + android:id="@+id/menu_delete" + android:title="@string/download.menu_delete"/> + + <item + android:id="@+id/menu_star" + android:title="@string/common.star"/> + + <item + android:id="@+id/menu_add_playlist" + android:title="@string/menu.add_playlist"/> +</menu> diff --git a/res/menu/nowplaying_context_offline.xml b/res/menu/nowplaying_context_offline.xml new file mode 100644 index 00000000..1446353f --- /dev/null +++ b/res/menu/nowplaying_context_offline.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/menu_show_album" + android:title="@string/download.menu_show_album"/> + + <item + android:id="@+id/menu_remove" + android:title="@string/download.menu_remove"/> + + <item + android:id="@+id/menu_delete" + android:title="@string/download.menu_delete"/> + + <item + android:id="@+id/menu_star" + android:title="@string/common.star"/> +</menu> diff --git a/res/menu/nowplaying_downloading.xml b/res/menu/nowplaying_downloading.xml new file mode 100644 index 00000000..9376731a --- /dev/null +++ b/res/menu/nowplaying_downloading.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_remove_all" + android:icon="@drawable/action_remove_all" + android:title="@string/download.menu_remove_all" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_screen_on_off" + android:icon="@drawable/action_screen_on_off" + android:title="@string/download.menu_screen_on" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_toggle_timer" + android:title="@string/download.start_timer"/> + + <item + android:id="@+id/menu_toggle_now_playing" + android:title="@string/download.show_now_playing"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu> diff --git a/res/menu/nowplaying_offline.xml b/res/menu/nowplaying_offline.xml new file mode 100644 index 00000000..e3e85040 --- /dev/null +++ b/res/menu/nowplaying_offline.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/download.menu_shuffle" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_remove_all" + android:icon="@drawable/action_remove_all" + android:title="@string/download.menu_remove_all" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_screen_on_off" + android:icon="@drawable/action_screen_on_off" + android:title="@string/download.menu_screen_on" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_toggle_timer" + android:title="@string/download.start_timer"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu> diff --git a/res/menu/search.xml b/res/menu/search.xml new file mode 100644 index 00000000..b9cdecac --- /dev/null +++ b/res/menu/search.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_search" + android:icon="@drawable/action_search" + android:title="@string/menu.search" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_help" + android:icon="@drawable/ic_menu_help" + android:title="@string/menu.help"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/ic_menu_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/ic_menu_exit" + android:title="@string/menu.exit"/> + +</menu> diff --git a/res/menu/select_album.xml b/res/menu/select_album.xml new file mode 100644 index 00000000..5ca9c537 --- /dev/null +++ b/res/menu/select_album.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_play_now" + android:icon="@drawable/action_play_all" + android:title="@string/menu.play" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/menu.shuffle" + android:showAsAction="ifRoom|withText"/> +</menu> diff --git a/res/menu/select_album_context.xml b/res/menu/select_album_context.xml new file mode 100644 index 00000000..00fe7993 --- /dev/null +++ b/res/menu/select_album_context.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/album_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/album_menu_play_shuffled" + android:title="@string/common.play_shuffled" + /> + + <item + android:id="@+id/album_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/album_menu_download" + android:title="@string/common.download" + /> + + <item + android:id="@+id/album_menu_pin" + android:title="@string/common.pin" + /> + + <item + android:id="@+id/album_menu_delete" + android:title="@string/common.delete"/> + + <item + android:id="@+id/album_menu_star" + android:title="@string/common.star"/> + +</menu> diff --git a/res/menu/select_album_context_offline.xml b/res/menu/select_album_context_offline.xml new file mode 100644 index 00000000..70cf9da9 --- /dev/null +++ b/res/menu/select_album_context_offline.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/album_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/album_menu_play_shuffled" + android:title="@string/common.play_shuffled" + /> + + <item + android:id="@+id/album_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/album_menu_delete" + android:title="@string/common.delete"/> + + <item + android:id="@+id/album_menu_star" + android:title="@string/common.star"/> +</menu>
\ No newline at end of file diff --git a/res/menu/select_album_list.xml b/res/menu/select_album_list.xml new file mode 100644 index 00000000..b6db96aa --- /dev/null +++ b/res/menu/select_album_list.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> +</menu> diff --git a/res/menu/select_artist.xml b/res/menu/select_artist.xml new file mode 100644 index 00000000..a7b988a5 --- /dev/null +++ b/res/menu/select_artist.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/menu.shuffle" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_search" + android:icon="@drawable/action_search" + android:title="@string/menu.search" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/action_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu> diff --git a/res/menu/select_artist_context.xml b/res/menu/select_artist_context.xml new file mode 100644 index 00000000..23d64c4e --- /dev/null +++ b/res/menu/select_artist_context.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/artist_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/artist_menu_play_shuffled" + android:title="@string/common.play_shuffled" + /> + + <item + android:id="@+id/artist_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/artist_menu_download" + android:title="@string/common.download" + /> + + <item + android:id="@+id/artist_menu_pin" + android:title="@string/common.pin" + /> + + <item + android:id="@+id/artist_menu_delete" + android:title="@string/common.delete"/> +</menu> diff --git a/res/menu/select_artist_context_offline.xml b/res/menu/select_artist_context_offline.xml new file mode 100644 index 00000000..c80db020 --- /dev/null +++ b/res/menu/select_artist_context_offline.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/artist_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/artist_menu_play_shuffled" + android:title="@string/common.play_shuffled" + /> + + <item + android:id="@+id/artist_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/artist_menu_delete" + android:title="@string/common.delete"/> +</menu> diff --git a/res/menu/select_genres.xml b/res/menu/select_genres.xml new file mode 100644 index 00000000..e0f9a718 --- /dev/null +++ b/res/menu/select_genres.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/action_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu>
\ No newline at end of file diff --git a/res/menu/select_playlist.xml b/res/menu/select_playlist.xml new file mode 100644 index 00000000..a68e6da0 --- /dev/null +++ b/res/menu/select_playlist.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_search" + android:icon="@drawable/action_search" + android:title="@string/menu.search" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/action_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu> diff --git a/res/menu/select_playlist_context.xml b/res/menu/select_playlist_context.xml new file mode 100644 index 00000000..6d844a16 --- /dev/null +++ b/res/menu/select_playlist_context.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/playlist_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/playlist_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/playlist_menu_play_shuffled" + android:title="@string/common.play_shuffled" + /> + + <item + android:id="@+id/playlist_menu_download" + android:title="@string/common.download" + /> + + <item + android:id="@+id/playlist_menu_pin" + android:title="@string/common.pin" + /> + + <item + android:id="@+id/playlist_update_info" + android:title="@string/playlist.update_info" + /> + + <item + android:id="@+id/playlist_menu_delete" + android:title="@string/common.delete" + /> + +</menu>
\ No newline at end of file diff --git a/res/menu/select_playlist_context_offline.xml b/res/menu/select_playlist_context_offline.xml new file mode 100644 index 00000000..644df2d3 --- /dev/null +++ b/res/menu/select_playlist_context_offline.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/playlist_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/playlist_menu_play_shuffled" + android:title="@string/common.play_shuffled" + /> +</menu>
\ No newline at end of file diff --git a/res/menu/select_podcast_episode.xml b/res/menu/select_podcast_episode.xml new file mode 100644 index 00000000..ff5898e2 --- /dev/null +++ b/res/menu/select_podcast_episode.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_delete" + android:icon="@drawable/action_remove_all" + android:title="@string/common.delete"/> +</menu> diff --git a/res/menu/select_podcast_episode_context.xml b/res/menu/select_podcast_episode_context.xml new file mode 100644 index 00000000..25c83989 --- /dev/null +++ b/res/menu/select_podcast_episode_context.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/song_menu_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/song_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/song_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/song_menu_download" + android:title="@string/common.download" + /> + + <item + android:id="@+id/song_menu_delete" + android:title="@string/common.delete"/> + + <item + android:id="@+id/song_menu_server_download" + android:title="@string/select_podcasts.server_download"/> + + <item + android:id="@+id/song_menu_server_delete" + android:title="@string/select_podcasts.server_delete"/> +</menu> diff --git a/res/menu/select_podcast_episode_context_offline.xml b/res/menu/select_podcast_episode_context_offline.xml new file mode 100644 index 00000000..38c4569b --- /dev/null +++ b/res/menu/select_podcast_episode_context_offline.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/song_menu_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/song_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/song_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/song_menu_delete" + android:title="@string/common.delete"/> +</menu> diff --git a/res/menu/select_podcast_episode_offline.xml b/res/menu/select_podcast_episode_offline.xml new file mode 100644 index 00000000..9bbc2d92 --- /dev/null +++ b/res/menu/select_podcast_episode_offline.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_delete" + android:icon="@drawable/action_remove_all" + android:title="@string/common.delete"/> +</menu> diff --git a/res/menu/select_podcasts.xml b/res/menu/select_podcasts.xml new file mode 100644 index 00000000..e77b43db --- /dev/null +++ b/res/menu/select_podcasts.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_add_podcast" + android:icon="@drawable/action_exit" + android:title="@string/menu.add_podcast"/> + + <item + android:id="@+id/menu_check" + android:icon="@drawable/action_refresh" + android:title="@string/menu.check_podcasts"/> + + <item + android:id="@+id/menu_settings" + android:icon="@drawable/action_settings" + android:title="@string/menu.settings"/> + + <item + android:id="@+id/menu_exit" + android:icon="@drawable/action_exit" + android:title="@string/menu.exit"/> +</menu>
\ No newline at end of file diff --git a/res/menu/select_podcasts_context.xml b/res/menu/select_podcasts_context.xml new file mode 100644 index 00000000..af4edb55 --- /dev/null +++ b/res/menu/select_podcasts_context.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/podcast_channel_info" + android:title="@string/common.info"/> + <item + android:id="@+id/podcast_channel_delete" + android:title="@string/common.delete"/> +</menu>
\ No newline at end of file diff --git a/res/menu/select_song.xml b/res/menu/select_song.xml new file mode 100644 index 00000000..3a55fee0 --- /dev/null +++ b/res/menu/select_song.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_play_now" + android:icon="@drawable/action_play_all" + android:title="@string/menu.play" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/menu.shuffle" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_select" + android:icon="@drawable/action_select" + android:title="@string/menu.select" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_download" + android:icon="@drawable/action_save" + android:title="@string/common.download" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_cache" + android:icon="@drawable/action_save" + android:title="@string/common.pin" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_delete" + android:icon="@drawable/action_remove_all" + android:title="@string/common.delete" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_add_playlist" + android:title="@string/menu.add_playlist"/> + + <item + android:id="@+id/menu_remove_playlist" + android:title="@string/menu.remove_playlist"/> + + <item + android:id="@+id/menu_play_last" + android:icon="@drawable/action_play_all" + android:title="@string/menu.play_last" + android:showAsAction="ifRoom|withText"/> +</menu> diff --git a/res/menu/select_song_context.xml b/res/menu/select_song_context.xml new file mode 100644 index 00000000..4db229f2 --- /dev/null +++ b/res/menu/select_song_context.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/song_menu_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/song_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/song_menu_play_next" + android:title="@string/common.play_next" + /> + + <item + android:id="@+id/song_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/song_menu_download" + android:title="@string/common.download" + /> + + <item + android:id="@+id/song_menu_pin" + android:title="@string/common.pin" + /> + + <item + android:id="@+id/song_menu_delete" + android:title="@string/common.delete"/> + + <item + android:id="@+id/song_menu_add_playlist" + android:title="@string/menu.add_playlist"/> + + <item + android:id="@+id/song_menu_remove_playlist" + android:title="@string/menu.remove_playlist"/> + + <item + android:id="@+id/song_menu_star" + android:title="@string/common.star"/> + +</menu> diff --git a/res/menu/select_song_context_offline.xml b/res/menu/select_song_context_offline.xml new file mode 100644 index 00000000..d19eaa70 --- /dev/null +++ b/res/menu/select_song_context_offline.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/song_menu_info" + android:title="@string/common.info" + /> + + <item + android:id="@+id/song_menu_play_now" + android:title="@string/common.play_now" + /> + + <item + android:id="@+id/song_menu_play_next" + android:title="@string/common.play_next" + /> + + <item + android:id="@+id/song_menu_play_last" + android:title="@string/common.play_last" + /> + + <item + android:id="@+id/song_menu_delete" + android:title="@string/common.delete"/> + + <item + android:id="@+id/song_menu_star" + android:title="@string/common.star"/> +</menu> diff --git a/res/menu/select_song_offline.xml b/res/menu/select_song_offline.xml new file mode 100644 index 00000000..6ed43b71 --- /dev/null +++ b/res/menu/select_song_offline.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_play_now" + android:icon="@drawable/action_play_all" + android:title="@string/menu.play" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_refresh" + android:icon="@drawable/action_refresh" + android:title="@string/menu.refresh" + android:showAsAction="always|withText"/> + + <item + android:id="@+id/menu_shuffle" + android:icon="@drawable/action_shuffle" + android:title="@string/menu.shuffle" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_select" + android:icon="@drawable/action_select" + android:title="@string/menu.select" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_delete" + android:icon="@drawable/action_remove_all" + android:title="@string/common.delete" + android:showAsAction="ifRoom|withText"/> + + <item + android:id="@+id/menu_play_last" + android:icon="@drawable/action_play_all" + android:title="@string/menu.play_last" + android:showAsAction="ifRoom|withText"/> +</menu> diff --git a/res/menu/select_video_context.xml b/res/menu/select_video_context.xml new file mode 100644 index 00000000..5926f8a5 --- /dev/null +++ b/res/menu/select_video_context.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/song_menu_info" + android:title="@string/common.info"/> + + <item + android:id="@+id/song_menu_stream_external" + android:title="@string/common.stream_external"/> + + <item + android:id="@+id/song_menu_play_external" + android:title="@string/common.play_external"/> + + <item + android:id="@+id/song_menu_download" + android:title="@string/common.download" + /> + + <item + android:id="@+id/song_menu_delete" + android:title="@string/common.delete"/> +</menu> diff --git a/res/menu/select_video_context_offline.xml b/res/menu/select_video_context_offline.xml new file mode 100644 index 00000000..fc354119 --- /dev/null +++ b/res/menu/select_video_context_offline.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/song_menu_info" + android:title="@string/common.info"/> + + <item + android:id="@+id/song_menu_play_external" + android:title="@string/common.play_external"/> + + <item + android:id="@+id/song_menu_delete" + android:title="@string/common.delete"/> +</menu> diff --git a/res/raw/changelog.xml b/res/raw/changelog.xml new file mode 100644 index 00000000..54f41c0f --- /dev/null +++ b/res/raw/changelog.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="utf-8"?> +<changelog> + <release version="4.1.2" versioncode="59" releasedate="7/24/2013"> + <change>Added option to clear cache from settings</change> + <change>Added cloud settings backup so when you reinstall on the same device your settings are still there</change> + <change>Fixed Android 4.3 crash</change> + <change>Performance enhancements</change> + </release> + <release version="4.1.1" versioncode="58" releasedate="7/18/2013"> + <change>Fix some podcasts causing errors</change> + </release> + <release version="4.1.0" versioncode="57" releasedate="7/17/2013"> + <change>Added Podcast Tab (4.5+)</change> + <change>Add/Delete Podcast Channels. Manage server status of Podcast Episodes (4.8+)</change> + <change>Double press pause on headset to skip to next song</change> + <change>Added HLS as a option under external video players (4.8+). Skipping doesn't seem to work for me.</change> + <change>Fix pressing play from widget from sometimes starting song over</change> + <change>Various minor UI tweaks to make things look nicer</change> + <change>Gapless Playback setting: if off now acts more like base Subsonic app to hopefully fix some issues</change> + </release> + <release version="4.0.7" versioncode="56" releasedate="7/2/2013"> + <change>Added offline starring to library view instead of just now playing</change> + <change>Remove * to show downloading, go off of whether arrow is blue or green for cached/perma cached</change> + <change>Go back to always showing bottom bar so downloading list is accessible </change> + <change>Fix offline mode matching first letters against ignore list (ie: the), instead of first word</change> + <change>Add prompt for removing a server</change> + <change>Fix some cases where list would incorrectly show up blank</change> + </release> + <release version="4.0.6" versioncode="55" releasedate="6/25/2013"> + <change>Scrobble and star songs and sync changes back when going online (has trouble when tags don't match folders)</change> + <change>Fix cases where operations didn't work in online mode when originally added in offline mode and vice versa</change> + <change>Added blank option to genre picker in the shuffle dialog</change> + <change>Added option to show track # in front of song (off by default)</change> + <change>Separate cached playlists from different servers in separate folders so they don't interfere with each other</change> + <change>Fix for some music files which throw errors at the end not proceeding to the next song</change> + <change>Fix flash preference not being obeyed for the Play External option</change> + <change>As songs are downloaded in background list, automatically remove them</change> + <change>Fix low quality album artwork in large widgets</change> + <change>Fix a rare case that can cause a song to be played twice</change> + <change>Fix for some who listen to untranscoded flac songs</change> + <change>Remove bottom bar if nothing is in the queue</change> + <change>Use .nomedia file instead of folder for more compatibility</change> + <change>Clean some sensitive info from the logs</change> + </release> + <release version="4.0.5" versioncode="54" releasedate="6/7/2013"> + <change>Fix album art on old Subsonic/MusicCabinet servers</change> + </release> + <release version="4.0.4" versioncode="53" releasedate="6/6/2013"> + <change>Added Genre parsing (thanks archrival)</change> + <change>Changed Genre to combo selection on 4.8+ servers</change> + <change>Added video choice similar to Subsonic (Raw is the same as MX but you can choose which player to use)</change> + <change>Added 4x2, 4x3, 4x4 widgets (thanks archrival)</change> + <change>Add option to create new playlist when adding song to playlists</change> + <change>Added option to overwrite existing playlist on 4.7+ servers</change> + <change>Fix when removing the current server</change> + <change>Fix edge case in new sort</change> + </release> + <release version="4.0.3" versioncode="52" releasedate="5/31/2013"> + <change>Sort by disc number if specified in tags</change> + <change>Show starred artists in starred list</change> + <change>Change folder.jpg to albumart.jpg which galleries shouldn't display</change> + <change>Fix Show Album</change> + <change>Added support for server Ignored Articles (future server version) + defaults to server's defaults</change> + <change>On network error return to front of the app instead of exiting all the way</change> + <change>Fix occasional crash when going back into app after running for a while</change> + <change>Various minor bugfixes</change> + </release> + <release version="4.0.2" versioncode="51" releasedate="5/24/2013"> + <change>Fix if you set chat refresh rate to 0, will just not refresh</change> + <change>Revert dark theme modification</change> + <change>New Theme called black which is the pure black background</change> + <change>Option to disable chat menu, need to exit app and reenter for now</change> + </release> + + <release version="4.0.1" versioncode="50" releasedate="5/23/2013"> + <change>New: Chat Tab (Set chat auto refresh rate from settings)</change> + <change>New: Dynamic servers, add as many, or remove all but the ones you are using</change> + <change>New: Added separate setting for songs to preload for Wifi/Mobile</change> + <change>Improvement: The infinite playlist while shuffling is now persistent between startups</change> + <change>Theme: White is now more white, got rid of blue text for white theme only</change> + <change>Theme: Black is now a flat black due to popular request</change> + <change>Theme: Apply the current theme to settings screen</change> + <change>Fix: Don't stretch album art on bottom of main tabs</change> + <change>Fix: Possible fix for some who were having crash on starting EQ</change> + </release> + + <release version="4.0.0" versioncode="48" releasedate="5/16/2013"> + <change>Converted everything to fragments!</change> + <change>Swipe to switch tabs</change> + <change>Breadcrumb trail when going down several levels</change> + <change>Require double tapping back to exit app</change> + <change>Change log dialog for new versions</change> + <change>Endless loading on album lists (ie: Random, Recently Added, etc...) instead of pressing more</change> + <change>Look at what is now playing from main tabs</change> + <change>Added Playing: Track/Total to Now Playing action bar</change> + <change>When clicking on a album in search, the parent is also added to the back stack</change> + <change>Added total time to playlist/album headers</change> + <change>Fixed a lot of the menu items not working when using search</change> + <change>Update to Light/Dark themes</change> + </release> +</changelog>
\ No newline at end of file diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml new file mode 100644 index 00000000..a2e1f660 --- /dev/null +++ b/res/values-fr/strings.xml @@ -0,0 +1,208 @@ +<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">Subsonic</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Enregistrer</string>
+ <string name="common.cancel">Annuler</string>
+
+ <string name="main.welcome_title">Bienvenue!</string>
+ <string name="main.welcome_text">Bienvenue dans Subsonic! L\'application est configurée pour utiliser le serveur démo de Subsonic.
+ Après avoir configuré votre serveur personnel (disponible à partir de <b>subsonic.org</b>), veuillez accéder aux <b>Paramètres</b> et modifier la configuration pour vous y connecter.</string>
+ <string name="main.select_server">Sélectionner un serveur</string>
+ <string name="main.shuffle">Lecture aléatoire</string>
+ <string name="main.offline">Hors-ligne</string>
+ <string name="main.settings">Paramètres</string>
+ <string name="main.albums_title">Albums</string>
+ <string name="main.albums_newest">Plus récents</string>
+ <string name="main.albums_random">Aléatoire</string>
+ <string name="main.albums_highest">Mieux cotés</string>
+ <string name="main.albums_recent">Récemment joués</string>
+ <string name="main.albums_frequent">Fréquemment joués</string>
+
+ <!--<string name="menu.exit">TODO: Exit</string>-->
+ <!--<string name="menu.settings">TODO: Settings</string>-->
+ <!--<string name="menu.help">TODO: Help</string>-->
+
+ <string name="playlist.label">Playlists</string>
+
+ <string name="help.label">Aide</string>
+ <string name="help.title">Bienvenue dans Subsonic!</string>
+ <string name="help.back">Retour</string>
+ <string name="help.close">Fermer</string>
+ <string name="help.url">file:///android_asset/html/fr/index.html</string>
+ <string name="help.loading">Chargement...</string>
+
+ <string name="play_video.loading">Chargement de la vidéo...</string>
+ <string name="play_video.noplugin">Veuillez installer Adobe Flash Player à partir du marché Android.</string>
+
+ <string name="search.label">Recherche</string>
+ <string name="search.title">Recherche</string>
+ <string name="search.search">Cliquer pour rechercher</string>
+ <string name="search.no_match">Aucun résultat, veuillez essayer à nouveau</string>
+ <string name="search.artists">Artistes</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Pièces</string>
+ <string name="search.more">Afficher plus</string>
+
+ <string name="progress.wait">Veuillez patienter...</string>
+
+ <string name="music_library.label">Bibliothèque musicale</string>
+ <string name="music_library.label_offline">Musique hors-ligne</string>
+
+ <string name="select_album.empty">Aucune musique trouvée</string>
+ <string name="select_album.select">Tout sélectionner</string>
+ <!--<string name="select_album.n_selected">TODO: %d tracks selected.</string>-->
+ <!--<string name="select_album.n_unselected">TODO: %d tracks unselected.</string>-->
+ <string name="select_album.more">Plus</string>
+ <string name="select_album.offline">Hors-ligne</string>
+ <string name="select_album.searching">Recherche en cours...</string>
+ <string name="select_album.no_sdcard">Erreur: Aucune carte SD disponible.</string>
+ <string name="select_album.no_network">Avis: Aucun réseau disponible.</string>
+ <string name="select_album.not_licensed">Serveur sans licence. %d jours d\'essai restant.</string>
+ <string name="select_album.donate_dialog_message">Obtenez des téléchargements illimités en donnant à Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Maintenant</string>
+ <string name="select_album.donate_dialog_later">Plus tard</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">La période d\'essai est terminée</string>
+
+ <string name="select_playlist.empty">Aucune playlist sur le serveur</string>
+
+ <string name="download.empty">Playlist vide</string>
+ <string name="download.playerstate_downloading">Téléchargement - %s</string>
+ <string name="download.playerstate_buffering">Mise en tampon</string>
+ <string name="download.playerstate_playing_shuffle">En jeu aléatoire</string>
+ <string name="download.menu_show_album">Afficher l\'album</string>
+ <string name="download.menu_lyrics">Paroles</string>
+ <string name="download.menu_remove">Retirer la pièce</string>
+ <string name="download.menu_remove_all">Retirer tout</string>
+ <string name="download.menu_shuffle">Mélanger</string>
+ <string name="download.menu_save">Enregistrer la playlist</string>
+ <string name="download.menu_shuffle_notification">Playlist mélangée</string>
+ <string name="download.playlist_title">Enregistrer la playlist</string>
+ <string name="download.playlist_name">Saisissez le nom de la playlist:</string>
+ <string name="download.playlist_saving">Enregistrement de la playlist \"%s\"...</string>
+ <string name="download.playlist_done">Playlist enregistrée avec succès.</string>
+ <string name="download.playlist_error">Échec de l\'enregistrement de la playlist, veuillez réessayer plus tard.</string>
+
+ <string name="song_details.all">%2$s, %1$s</string>
+ <string name="song_details.kbps">%d Kb/s</string>
+
+ <string name="lyrics.nomatch">Aucune parole trouvée</string>
+
+ <string name="error.label">Erreur</string>
+
+ <string name="settings.title">Paramètres de Subsonic</string>
+ <string name="settings.test_connection_title">Tester la connexion</string>
+ <string name="settings.servers_title">Serveurs</string>
+ <string name="settings.server_unused1">Inutilisé 1</string>
+ <string name="settings.server_unused2">Inutilisé 2</string>
+ <string name="settings.server_name">Nom</string>
+ <string name="settings.server_address">Adresse du serveur</string>
+ <string name="settings.server_username">Nom d\'usager</string>
+ <string name="settings.server_password">Mot de passe</string>
+ <string name="settings.cache_title">Cache musicale</string>
+ <string name="settings.preload">Pièces à pré-charger</string>
+ <string name="settings.cache_size">Taille de la cache</string>
+ <string name="settings.testing_connection">Connexion en cours de test...</string>
+ <string name="settings.testing_ok">Connexion correcte</string>
+ <string name="settings.testing_unlicensed">Connection correcte. Serveur sans licence.</string>
+ <string name="settings.connection_failure">Connection échouée.</string>
+ <string name="settings.invalid_url">Veuillez spécifier un URL valide.</string>
+ <string name="settings.invalid_username">Veuillez spécifier un nom d\'usager valide (sans espace à la fin).</string>
+ <string name="settings.appearance_title">Apparence</string>
+ <string name="settings.theme_title">Thème</string>
+ <string name="settings.theme_wheat">Blé</string>
+ <string name="settings.theme_light">Clair</string>
+ <string name="settings.theme_dark">Sombre</string>
+ <string name="settings.network_title">Réseau</string>
+ <string name="settings.max_bitrate_wifi">Débit maximal - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Débit maximal - Mobile</string>
+ <string name="settings.max_bitrate_32">32 Kb/s</string>
+ <string name="settings.max_bitrate_64">64 Kb/s</string>
+ <string name="settings.max_bitrate_80">80 Kb/s</string>
+ <string name="settings.max_bitrate_96">96 Kb/s</string>
+ <string name="settings.max_bitrate_112">112 Kb/s</string>
+ <string name="settings.max_bitrate_128">128 Kb/s</string>
+ <string name="settings.max_bitrate_160">160 Kb/s</string>
+ <string name="settings.max_bitrate_192">192 Kb/s</string>
+ <string name="settings.max_bitrate_256">256 Kb/s</string>
+ <string name="settings.max_bitrate_320">320 Kb/s</string>
+ <string name="settings.max_bitrate_unlimited">Illimité</string>
+ <string name="settings.preload_1">1 pièce</string>
+ <string name="settings.preload_2">2 pièces</string>
+ <string name="settings.preload_3">3 pièces</string>
+ <string name="settings.preload_5">5 pièces</string>
+ <string name="settings.preload_10">10 pièces</string>
+ <string name="settings.preload_unlimited">Illimité</string>
+ <string name="settings.cache_size_100">100 Mo</string>
+ <string name="settings.cache_size_200">200 Mo</string>
+ <string name="settings.cache_size_500">500 Mo</string>
+ <string name="settings.cache_size_1000">1 Go</string>
+ <string name="settings.cache_size_2000">2 Go</string>
+ <string name="settings.cache_size_5000">5 Go</string>
+ <string name="settings.cache_size_10000">10 Go</string>
+ <string name="settings.cache_size_20000">20 Go</string>
+ <string name="settings.cache_size_unlimited">Illimité</string>
+ <string name="settings.clear_search_history">Effacer l\'historique des recherches</string>
+ <string name="settings.search_history_cleared">Historique des recherches effacé</string>
+ <string name="settings.other_title">Autres paramètres</string>
+ <!--<string name="settings.scrobble_title">TODO: Scrobble to Last.fm</string>-->
+ <!--<string name="settings.scrobble_summary">TODO: Remember to set up your Last.fm user and password on the Subsonic server</string>-->
+ <string name="settings.hide_media_title">Masquer aux autres</string>
+ <string name="settings.hide_media_summary">Masquer les fichiers musicaux et les couvertures d\'album aux autres applis (Gallerie, Musique, etc.)</string>
+ <string name="settings.hide_media_toast">Prendra effet la prochaine fois qu\'Android recensera les médias disponibles sur l\'appareil.</string>
+ <string name="settings.media_button_title">Boutons média</string>
+ <string name="settings.media_button_summary">Répondre au boutons média de l\'appareil, du casque et du Bluetooth</string>
+ <!--<string name="settings.screen_lit_title">TODO: Keep screen on</string>-->
+ <!--<string name="settings.screen_lit_summary">TODO: Keeping the screen on when downloading may improve download speed</string>-->
+
+ <string name="music_service.retry">Une erreur de réseau s\'est produite. Essai %1$d de %2$d.</string>
+
+ <string name="background_task.wait">Veuillez patienter...</string>
+ <string name="background_task.loading">Chargement.</string>
+ <string name="background_task.no_network">Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile.</string>
+ <string name="background_task.network_error">Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard.</string>
+ <string name="background_task.not_found">Ressource non trouvée. Veuillez vérifier l\'adresse du serveur.</string>
+ <string name="background_task.parse_error">Réplique incomprise. Veuillez vérifier l\'adresse du serveur.</string>
+
+ <string name="service.connecting">Contact du serveur, veuillez patienter.</string>
+
+ <string name="parser.reading">Lecture du serveur.</string>
+ <string name="parser.reading_done">Lecture du serveur. Terminé!</string>
+ <string name="parser.upgrade_client">Versions incompatible. Veuillez mette à jour l\'application Android Subsonic.</string>
+ <string name="parser.upgrade_server">Versions incompatible. Veuillez mette à jour le serveur Subsonic.</string>
+ <string name="parser.not_authenticated">Mauvais nom d\'usager ou mot de passe.</string>
+ <string name="parser.artist_count">%d artistes récupérés.</string>
+
+ <string name="select_artist.refresh">Rafraîchir</string>
+ <string name="select_artist.folder">Sélectionner le dossier</string>
+ <string name="select_artist.all_folders">Tous les dossiers</string>
+
+ <string name="widget.initial_text">Touchez pour sélectionner une pièce</string>
+ <string name="widget.sdcard_busy">Carte SD non disponible</string>
+ <string name="widget.sdcard_missing">Aucune carte SD</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 Go</string>
+ <string name="util.bytes_format.megabyte">0.00 Mo</string>
+ <string name="util.bytes_format.kilobyte">0 Ko</string>
+ <string name="util.bytes_format.byte">0 o</string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Aucune pièce</item>
+ <item quantity="one">Une pièce</item>
+ <item quantity="other">%d pièces</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">Une pièce prévue pour téléchargement.</item>
+ <item quantity="other">%d pièces prévues pour téléchargement.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">Une pièce ajoutée à la file de lecture.</item>
+ <item quantity="other">%d pièces ajoutées à la file de lecture.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">Un jour restant à la période d\'essai</item>
+ <item quantity="other">%d jours restant à la période d\'essai</item>
+ </plurals>
+
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml new file mode 100644 index 00000000..1748ce30 --- /dev/null +++ b/res/values-ru/strings.xml @@ -0,0 +1,343 @@ +<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Сохранить</string>
+ <string name="common.cancel">Отмена</string>
+ <string name="common.play_now">Воспроизвести сейчас</string>
+ <string name="common.play_shuffled">Случайное воспроизведение</string>
+ <string name="common.play_next">Воспроизвести следующим</string>
+ <string name="common.play_last">Воспроизвести последним</string>
+ <string name="common.download">Скачать</string>
+ <string name="common.pin">Кешировать</string>
+ <string name="common.delete">Удалить</string>
+ <string name="common.star">Добавить в закладки</string>
+ <string name="common.unstar">Удалить из закладок</string>
+ <string name="common.info">Информация</string>
+ <string name="common.name">Название</string>
+ <string name="common.comment">Комментарий</string>
+ <string name="common.public">Общедоступный</string>
+ <string name="common.webview">Воспроизвести в браузере (флэш)</string>
+ <string name="common.play_external">Воспроизвести во внешнем плеере</string>
+ <string name="common.stream_external">Воспроизвести поток во внешнем плеере</string>
+ <string name="common.confirm">Подтверждение</string>
+
+ <string name="button_bar.home">Домой</string>
+ <string name="button_bar.browse">Медиатека</string>
+ <string name="button_bar.search">Поиск</string>
+ <string name="button_bar.playlists">Списки</string>
+ <string name="button_bar.now_playing">Плеер</string>
+
+ <string name="main.welcome_title">Здравствуйте!</string>
+ <string name="main.welcome_text">Добро пожаловать в DSub! Это приложение настроено на работу с демо сервером Subsonic. После настройки Вашего персонального сервера (доступен на <b>subsonic.org</b>), пожалуйста, перейдите в <b>Настройки</b> и измените параметры для подключения.</string>
+
+ <string name="main.about_title">О программе DSub</string>
+ <string name="main.about_text">Автор: Scott Jackson
+ \nEmail: daneren2005@gmail.com
+ \nВерсия: %1$s
+ \nИспользовано места: %2$s из %3$s
+ \nДоступно места: %4$s из %5$s</string>
+ <string name="main.select_server">Выбрать сервер</string>
+ <string name="main.shuffle">Случайное воспроизведение</string>
+ <string name="main.offline">Отключиться</string>
+ <string name="main.online">Подключиться</string>
+ <string name="main.settings">Настройки</string>
+ <string name="main.albums_title">Альбомы</string>
+ <string name="main.albums_newest">Недавно добавленные</string>
+ <string name="main.albums_recent">Недавно прослушанные</string>
+ <string name="main.albums_frequent">Часто прослушиваемые</string>
+ <string name="main.albums_highest">Максимальный рейтинг</string>
+ <string name="main.albums_starred">Закладки</string>
+ <string name="main.albums_random">Случайные</string>
+
+ <string name="menu.search">Поиск</string>
+ <string name="menu.shuffle">Перемешать</string>
+ <string name="menu.refresh">Обновить</string>
+ <string name="menu.select">Выбрать все</string>
+ <string name="menu.play">Воспроизвести</string>
+ <string name="menu.play_last">Воспроизвести последним</string>
+ <string name="menu.exit">Выход</string>
+ <string name="menu.settings">Настройки</string>
+ <string name="menu.help">Помощь</string>
+ <string name="menu.about">О программе</string>
+ <string name="menu.add_playlist">Добавить в список</string>
+ <string name="menu.remove_playlist">Удалить из списка</string>
+ <string name="menu.deleted_playlist">Список воспроизведения %s удален</string>
+ <string name="menu.deleted_playlist_error">Не удалось удалить список %s</string>
+ <string name="menu.log">Отправить журнал событий</string>
+ <string name="menu.set_timer">Установить таймер</string>
+
+ <string name="playlist.label">Списки</string>
+ <string name="playlist.update_info">Изменить информацию</string>
+ <string name="playlist.updated_info">Информация для списка воспроизведения %s обновлена</string>
+ <string name="playlist.updated_info_error">Не удалось обновить информацию для списка воспроизведения %s</string>
+
+ <string name="help.label">Помощь</string>
+ <string name="help.title">Добро пожаловать в DSub!</string>
+ <string name="help.back">Назад</string>
+ <string name="help.close">Закрыть</string>
+ <string name="help.url">file:///android_asset/html/ru/index.html</string>
+ <string name="help.loading">Загрузка...</string>
+
+ <string name="play_video.loading">Загрузка видео...</string>
+ <string name="play_video.noplugin">Пожалуйста, установить Adobe Flash Player из Google Play.</string>
+
+ <string name="search.label">Поиск</string>
+ <string name="search.title">Поиск</string>
+ <string name="search.search">Нажмите для поиска</string>
+ <string name="search.no_match">Ничего не найдено, пожалуйста, попробуйте снова</string>
+ <string name="search.artists">Исполнители</string>
+ <string name="search.albums">Альбомы</string>
+ <string name="search.songs">Композиции</string>
+ <string name="search.more">Показать еще</string>
+
+ <string name="progress.wait">Пожалуйста, подождите...</string>
+
+ <string name="music_library.label">Медиатека</string>
+ <string name="music_library.label_offline">Оффлайн медиа</string>
+
+ <string name="select_album.empty">Медиафайлы не найдены</string>
+ <string name="select_album.select">Выбрать все</string>
+ <string name="select_album.n_selected">%d композиций выбрано.</string>
+ <string name="select_album.n_unselected">Выбор снят с %d композиций.</string>
+ <string name="select_album.more">Еще</string>
+ <string name="select_album.offline">Оффлайн</string>
+ <string name="select_album.searching">Выполняется поиск...</string>
+ <string name="select_album.no_sdcard">Ошибка: SD карта недоступна</string>
+ <string name="select_album.no_network">Внимание: сеть недоступна.</string>
+ <string name="select_album.not_licensed">Сервер не лицензирован. %d дней до окончания пробного периода.</string>
+ <string name="select_album.donate_dialog_message">Осуществите пожертвование для Subsonic и получите возможность неограниченного скачивания.</string>
+ <string name="select_album.donate_dialog_now">Сейчас</string>
+ <string name="select_album.donate_dialog_later">Позже</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Пробный период закончился</string>
+
+ <string name="select_playlist.empty">Нет сохраненных списков воспроизведения на сервере</string>
+
+ <string name="download.empty">Список воспроизведения пуст</string>
+ <string name="download.shuffle_loading">Загружается случайный список...</string>
+ <string name="download.playerstate_downloading">Загрузка - %s</string>
+ <string name="download.playerstate_buffering">Буферизация</string>
+ <string name="download.playerstate_playing_shuffle">Воспроизводится случайно</string>
+ <string name="download.menu_show_album">Показать альбом</string>
+ <string name="download.menu_lyrics">Текст</string>
+ <string name="download.menu_remove">Убрать из очереди</string>
+ <string name="download.menu_delete">Удалить кэш</string>
+ <string name="download.menu_remove_all">Очистить</string>
+ <string name="download.menu_screen_on">Включить подсветку</string>
+ <string name="download.menu_screen_off">Отключать подсветку</string>
+ <string name="download.menu_shuffle">Перемешать</string>
+ <string name="download.menu_toggle">Переключатель</string>
+ <string name="download.menu_save">Сохранить список</string>
+ <string name="download.menu_shuffle_notification">Список воспроизведения был перемешан</string>
+ <string name="download.playlist_title">Сохранение списка воспроизведения</string>
+ <string name="download.playlist_name">Введите название:</string>
+ <string name="download.playlist_saving">Сохранение списка воспроизведения \"%s\"...</string>
+ <string name="download.playlist_done">Список воспроизведения сохранен</string>
+ <string name="download.playlist_error">Не удалось сохранить список воспроизведения, пожалуйста, попробуйте позже.</string>
+ <string name="download.repeat_off">Повторение отключено</string>
+ <string name="download.repeat_all">Повторять все</string>
+ <string name="download.repeat_single">Повторять композицию</string>
+ <string name="download.visualizer_on">Визуализация включена</string>
+ <string name="download.visualizer_off">Визуализация отключена</string>
+ <string name="download.jukebox_on">Удаленное управление включено. Музыка воспроизводится на компьютере.</string>
+ <string name="download.jukebox_off">Удаленное управление отключено. Музыка воспроизводится на устройстве.</string>
+ <string name="download.jukebox_volume">Удаленное управление громкостью</string>
+ <string name="download.jukebox_server_too_old">Удаленное управление не поддерживается. Пожалуйста, обновите Ваш сервер Subsonic.</string>
+ <string name="download.jukebox_offline">Удаленное управление не поддерживается в оффлайн режиме.</string>
+ <string name="download.jukebox_not_authorized">Удаленное управление запрещено. Пожалуйста, активируйте режим jukebox в разделе <b>Настройки > Проигрыватели</b> на вашем сервере Subsonic.</string>
+ <string name="download.show_downloading">Показать закачки</string>
+ <string name="download.show_now_playing">Показать воспроизведение</string>
+ <string name="download.timer_length">Длительность</string>
+ <string name="download.start_timer">Запустить таймер</string>
+ <string name="download.stop_timer">Остановить таймер</string>
+ <string name="download.need_download">Необходимо сначала скачать видео</string>
+ <string name="download.no_streaming_player">Нет плеера для воспроизведения потока</string>
+
+ <string name="starring_content_starred">\"%s\" добавлено в закладки</string>
+ <string name="starring_content_unstarred">\"%s\" удалено из закладок</string>
+ <string name="starring_content_error">Не удалось обновить \"%s\", пожалуйста, попробуйте позже.</string>
+
+ <string name="playlist_error">Не удалось прочитать списки воспроизведения</string>
+ <string name="updated_playlist">Добавлено %1$s композиций в \"%2$s\"</string>
+ <string name="updated_playlist_error">Не удалось обновить \"%s\", пожалуйста, попробуйте позже.</string>
+ <string name="removed_playlist">Удалено %1$s из \"%2$s\" композиций</string>
+ <string name="delete_playlist">Удалить %1$s?</string>
+
+ <string name="song_details.all">%1$s %2$s</string>
+ <string name="song_details.kbps">%d kbps</string>
+
+ <string name="lyrics.nomatch">Текст не найден</string>
+
+ <string name="error.label">Ошибка</string>
+
+ <string name="settings.title">Настройки DSub</string>
+ <string name="settings.test_connection_title">Проверить соединение</string>
+ <string name="settings.servers_title">Серверы</string>
+ <string name="settings.server_unused1">Неиспользованный 1</string>
+ <string name="settings.server_unused2">Неиспользованный 2</string>
+ <string name="settings.server_name">Название</string>
+ <string name="settings.server_address">Адрес сервера</string>
+ <string name="settings.server_username">Имя пользователя</string>
+ <string name="settings.server_password">Пароль</string>
+ <string name="settings.cache_title">Кэш музыки</string>
+ <string name="settings.preload">Композиций для предзагрузки</string>
+ <string name="settings.cache_size">Размер кэша (Мб)</string>
+ <string name="settings.cache_location">Путь кэша</string>
+ <string name="settings.cache_location_error">Некорректный путь. Используем путь по умолчанию.</string>
+ <string name="settings.testing_connection">Проверка соединения...</string>
+ <string name="settings.testing_ok">Подключение прошло успешно!</string>
+ <string name="settings.testing_unlicensed">Подключение прошло успешно. Сервер нелицензирован.</string>
+ <string name="settings.connection_failure">Не удалось подключиться.</string>
+ <string name="settings.invalid_url">Пожалуйста, укажите правильный адрес</string>
+ <string name="settings.invalid_username">Пожалуйста, укажите правильное имя пользователя (не должно быть пробелов в конце)</string>
+ <string name="settings.appearance_title">Внешний вид</string>
+ <string name="settings.theme_title">Тема</string>
+ <string name="settings.theme_light">Светлая</string>
+ <string name="settings.theme_dark">Темная</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_light_fullscreen">Светлая во весь экран</string>
+ <string name="settings.theme_dark_fullscreen">Темная во весь экран</string>
+ <string name="settings.theme_holo_fullscreen">Holo во весь экран</string>
+ <string name="settings.network_title">Сеть</string>
+ <string name="settings.max_bitrate_wifi">Макс. битрейт аудио по Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Макс. битрейт видео по сети</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Макс. битрейт видео по Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Макс. битрейт видео по сети</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Неограничен</string>
+ <string name="settings.wifi_required_title">Поток по Wi-Fi</string>
+ <string name="settings.wifi_required_summary">Потокое воспроизведение будет работать только при подключении через Wi-Fi</string>
+ <string name="settings.network_timeout_title">Таймаут сети</string>
+ <string name="settings.network_timeout_10000">10 секунд</string>
+ <string name="settings.network_timeout_15000">15 секунд</string>
+ <string name="settings.network_timeout_30000">30 секунд</string>
+ <string name="settings.network_timeout_45000">45 секунд</string>
+ <string name="settings.network_timeout_60000">60 секунд</string>
+ <string name="settings.preload_1">1 композиция</string>
+ <string name="settings.preload_2">2 композиции</string>
+ <string name="settings.preload_3">3 композиции</string>
+ <string name="settings.preload_5">5 композиций</string>
+ <string name="settings.preload_10">10 композиций</string>
+ <string name="settings.preload_unlimited">Неограничено</string>
+ <string name="settings.clear_search_history">Очистить историю поиска</string>
+ <string name="settings.search_history_cleared">История поиска очищена</string>
+ <string name="settings.other_title">Другие настройки</string>
+ <string name="settings.scrobble_title">Скробблинг на Last.fm</string>
+ <string name="settings.scrobble_summary">Не забудьте установить логин и пароль от Last.fm на сервере DSub</string>
+ <string name="settings.hide_media_title">Прятать от других</string>
+ <string name="settings.hide_media_summary">Прятать музыкальные файлы от других приложений</string>
+ <string name="settings.hide_media_toast">Изменения вступят в силу при следующем поиске музыки на Вашем устройстве.</string>
+ <string name="settings.media_button_title">Кнопки управления</string>
+ <string name="settings.media_button_summary">Разрешить управление кнопками мультимедиа на устройстве и гарнитуре</string>
+ <string name="settings.screen_lit_title">Держать экран включенным</string>
+ <string name="settings.screen_lit_summary">Оставить экран включенным для повышения скорости при скачивании.</string>
+ <string name="settings.playlist_title">Списки воспроизведения</string>
+ <string name="settings.playlist_random_size_title">Размер случайного списка</string>
+ <string name="settings.buffer_length">Размер буфера (0 = кешировать полностью)</string>
+ <string name="settings.sleep_timer_title">Таймер сна</string>
+ <string name="settings.sleep_timer_duration_title">Продолжительность таймера сна</string>
+ <string name="settings.sleep_timer_off">Выключен</string>
+ <string name="settings.sleep_timer_on">Включен</string>
+ <string name="settings.sleep_timer_always_on">Всегда включен</string>
+ <string name="settings.temp_loss_title">Временная потеря связи</string>
+ <string name="settings.temp_loss_pause">Всегда останавливать</string>
+ <string name="settings.temp_loss_pause_lower">Останавливать, понижать громкость, если требуется</string>
+ <string name="settings.temp_loss_lower">Всегда понижать громкость</string>
+ <string name="settings.temp_loss_nothing">Ничего не делать</string>
+
+ <string name="shuffle.startYear">Год начала:</string>
+ <string name="shuffle.endYear">Год окончания:</string>
+ <string name="shuffle.genre">Жанр:</string>
+
+ <string name="music_service.retry">Ошибка подключения. Попытка %1$d из %2$d.</string>
+
+ <string name="background_task.wait">Пожалуйста, подождите...</string>
+ <string name="background_task.loading">Загрузка</string>
+ <string name="background_task.no_network">Эта программа требует доступ к сети. Пожалуйста, включите Wi-Fi или мобильный интернет</string>
+ <string name="background_task.network_error">Ошибка сети. Пожалуйста, проверьте адрес сервера и попробуйте снова</string>
+ <string name="background_task.not_found">Ресурс не найден. Пожалуйста, проверьте адрес сервера</string>
+ <string name="background_task.parse_error">Неизвестный ответ. Пожалуйста, проверьте адрес сервера</string>
+
+ <string name="service.connecting">Подключение к серверу. Пожалуйста, подождите.</string>
+
+ <string name="parser.reading">Чтение с сервера.</string>
+ <string name="parser.reading_done">Чтение с сервера выполнено!</string>
+ <string name="parser.upgrade_client">Несовместимые версии. Пожалуйста, обновите приложение DSub для Android.</string>
+ <string name="parser.upgrade_server">Несовместимые версии. Пожалуйста, обновите сервер Subsonic.</string>
+ <string name="parser.not_authenticated">Неправильное имя пользователя или пароль.</string>
+ <string name="parser.not_authorized">Не авторизирован. Проверьте права пользователя на сервере Subsonic.</string>
+ <string name="parser.artist_count">Получено %d исполнителей.</string>
+
+ <string name="select_artist.refresh">Обновить</string>
+ <string name="select_artist.folder">Выбрать папку</string>
+ <string name="select_artist.all_folders">Все папки</string>
+
+ <string name="equalizer.label">Эквалайзер</string>
+ <string name="equalizer.enabled">Включен</string>
+ <string name="equalizer.preset">Готовые настройки</string>
+
+ <string name="widget.initial_text">Коснитесь для выбора музыки</string>
+ <string name="widget.sdcard_busy">SD карта недоступна</string>
+ <string name="widget.sdcard_missing">Нет SD карты</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 ГБ</string>
+ <string name="util.bytes_format.megabyte">0.00 МБ</string>
+ <string name="util.bytes_format.kilobyte">0 КБ</string>
+ <string name="util.bytes_format.byte">0 Б</string>
+
+ <string name="button_bar.chat">Чат</string>
+ <string name="main.back_confirm">Нажмите "назад" еще раз для выхода</string>
+ <string name="download.playing_out_of">Воспроизведение: %1$d/%2$d</string>
+ <string name="settings.persistent_title">Постоянное уведомление</string>
+ <string name="settings.persistent_summary">Показывать уведомление даже во время паузы. Остановка воспроизведения уберет это уведомление.</string>
+ <string name="settings.gapless_playback">Непрерывное воспроизведение</string>
+ <string name="settings.gapless_playback_summary">Galaxy S3 может зависать или испытывать прочие трудности с момента начала непрерывного воспроизведения. Выключите эту функцию для исправления данной проблемы.</string>
+ <string name="settings.chat_refresh">Частота обновления чата (сек)</string>
+ <string name="settings.chat_enabled">Чат активен</string>
+ <string name="settings.chat_enabled_summary">Показывать или нет вкладку чата</string>
+ <string name="changelog_full_title">Журнал изменений</string>
+ <string name="changelog_title">Что нового</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">Еще…</string>
+ <string name="chat.send_a_message">Отправить сообщение</string>
+
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">Нет композиций</item>
+ <item quantity="one">1 композиция</item>
+ <item quantity="other">%d композиций</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">1 композиция запланирована для скачивания</item>
+ <item quantity="other">%d композиций запланировано для скачивания</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">1 композиция добавлена в очередь воспроизведения</item>
+ <item quantity="other">%d композиций добавлено в очередь воспроизведения</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">1 день до конца пробного периода</item>
+ <item quantity="other">%d дней до конца пробного периода</item>
+ </plurals>
+
+</resources>
diff --git a/res/values-v11/colors.xml b/res/values-v11/colors.xml new file mode 100644 index 00000000..f5a422bb --- /dev/null +++ b/res/values-v11/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="notificationArtist">#bababa</color> + <color name="notificationTitle">#dddddd</color> +</resources> diff --git a/res/values/arrays.xml b/res/values/arrays.xml new file mode 100644 index 00000000..d6541f9f --- /dev/null +++ b/res/values/arrays.xml @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="themeValues"> + <item>light</item> + <item>dark</item> + <item>black</item> + <item>holo</item> + <item>light_fullscreen</item> + <item>dark_fullscreen</item> + <item>black_fullscreen</item> + <item>holo_fullscreen</item> + </string-array> + + <string-array name="themeNames"> + <item>@string/settings.theme_light</item> + <item>@string/settings.theme_dark</item> + <item>@string/settings.theme_black</item> + <item>@string/settings.theme_holo</item> + <item>@string/settings.theme_light_fullscreen</item> + <item>@string/settings.theme_dark_fullscreen</item> + <item>@string/settings.theme_black_fullscreen</item> + <item>@string/settings.theme_holo_fullscreen</item> + </string-array> + + <string-array name="sleepTimerValues"> + <item>0</item> + <item>1</item> + <item>2</item> + </string-array> + + <string-array name="sleepTimerNames"> + <item>@string/settings.sleep_timer_off</item> + <item>@string/settings.sleep_timer_on</item> + <item>@string/settings.sleep_timer_always_on</item> + </string-array> + + <string-array name="preloadCountValues"> + <item>1</item> + <item>2</item> + <item>3</item> + <item>5</item> + <item>10</item> + <item>-1</item> + </string-array> + + <string-array name="preloadCountNames"> + <item>@string/settings.preload_1</item> + <item>@string/settings.preload_2</item> + <item>@string/settings.preload_3</item> + <item>@string/settings.preload_5</item> + <item>@string/settings.preload_10</item> + <item>@string/settings.preload_unlimited</item> + </string-array> + + <string-array name="maxBitrateValues"> + <item>32</item> + <item>64</item> + <item>80</item> + <item>96</item> + <item>112</item> + <item>128</item> + <item>160</item> + <item>192</item> + <item>256</item> + <item>320</item> + <item>0</item> + </string-array> + + <string-array name="maxBitrateNames"> + <item>@string/settings.max_bitrate_32</item> + <item>@string/settings.max_bitrate_64</item> + <item>@string/settings.max_bitrate_80</item> + <item>@string/settings.max_bitrate_96</item> + <item>@string/settings.max_bitrate_112</item> + <item>@string/settings.max_bitrate_128</item> + <item>@string/settings.max_bitrate_160</item> + <item>@string/settings.max_bitrate_192</item> + <item>@string/settings.max_bitrate_256</item> + <item>@string/settings.max_bitrate_320</item> + <item>@string/settings.max_bitrate_unlimited</item> + </string-array> + + <string-array name="maxVideoBitrateValues"> + <item>200</item> + <item>300</item> + <item>400</item> + <item>500</item> + <item>700</item> + <item>1000</item> + <item>1500</item> + <item>2000</item> + <item>3000</item> + <item>5000</item> + <item>0</item> + </string-array> + + <string-array name="maxVideoBitrateNames"> + <item>@string/settings.max_video_bitrate_200</item> + <item>@string/settings.max_video_bitrate_300</item> + <item>@string/settings.max_video_bitrate_400</item> + <item>@string/settings.max_video_bitrate_500</item> + <item>@string/settings.max_video_bitrate_700</item> + <item>@string/settings.max_video_bitrate_1000</item> + <item>@string/settings.max_video_bitrate_1500</item> + <item>@string/settings.max_video_bitrate_2000</item> + <item>@string/settings.max_video_bitrate_3000</item> + <item>@string/settings.max_video_bitrate_5000</item> + <item>@string/settings.max_bitrate_unlimited</item> + </string-array> + + <string-array name="networkTimeoutValues"> + <item>10000</item> + <item>15000</item> + <item>30000</item> + <item>45000</item> + <item>60000</item> + </string-array> + <string-array name="networkTimeoutNames"> + <item>@string/settings.network_timeout_10000</item> + <item>@string/settings.network_timeout_15000</item> + <item>@string/settings.network_timeout_30000</item> + <item>@string/settings.network_timeout_45000</item> + <item>@string/settings.network_timeout_60000</item> + </string-array> + + <string-array name="tempLossValues"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </string-array> + <string-array name="tempLossNames"> + <item>@string/settings.temp_loss_pause</item> + <item>@string/settings.temp_loss_pause_lower</item> + <item>@string/settings.temp_loss_lower</item> + <item>@string/settings.temp_loss_nothing</item> + </string-array> + + <string-array name="videoPlayerValues"> + <item>raw</item> + <item>hls</item> + <item>transcode</item> + <item>flash</item> + </string-array> + <string-array name="videoPlayerNames"> + <item>@string/settings.video_raw</item> + <item>@string/settings.video_hls</item> + <item>@string/settings.video_transcode</item> + <item>@string/settings.video_flash</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml new file mode 100644 index 00000000..8f669cd2 --- /dev/null +++ b/res/values/attrs.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <attr name="offline_icon" format="reference"/> + <attr name="media_button_backward" format="reference"/> + <attr name="media_button_forward" format="reference"/> + <attr name="media_button_pause" format="reference"/> + <attr name="media_button_repeat_off" format="reference"/> + <attr name="media_button_start" format="reference"/> + <attr name="media_button_stop" format="reference"/> + <attr name="chat" format="reference"/> + <attr name="chat_send" format="reference" /> +</resources> diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 00000000..0ce98fc9 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="lightBackground">#F1F0E6</color> + <color name="dividerColor">#FF33B5E5</color> + <color name="appwidget_text">#FFFFFF</color> + <color name="notificationArtist">#434343</color> + <color name="notificationTitle">#000000</color> + <color name="background_holo_light">#ff33b5e5</color> + <color name="overlayColor">#80000000</color> + <color name="ics_opaque">#8033b5e5</color> + <color name="cyan">#ff0099cc</color> +</resources>
\ No newline at end of file diff --git a/res/values/ids.xml b/res/values/ids.xml new file mode 100644 index 00000000..edb3bbec --- /dev/null +++ b/res/values/ids.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources> + <item name="drag_handle" type="id"/> +</resources>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 00000000..0b2815ec --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,401 @@ +<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="common.appname">DSub</string>
+ <string name="common.ok">OK</string>
+ <string name="common.save">Save</string>
+ <string name="common.cancel">Cancel</string>
+ <string name="common.play_now">Play now</string>
+ <string name="common.play_shuffled">Play shuffled</string>
+ <string name="common.play_next">Play next</string>
+ <string name="common.play_last">Play last</string>
+ <string name="common.download">Cache</string>
+ <string name="common.pin">Permanent Cache</string>
+ <string name="common.delete">Delete</string>
+ <string name="common.star">Star</string>
+ <string name="common.unstar">Unstar</string>
+ <string name="common.info">Details</string>
+ <string name="common.name">Name</string>
+ <string name="common.comment">Comment</string>
+ <string name="common.public">Public</string>
+ <string name="common.play_external">Play Video</string>
+ <string name="common.stream_external">Stream Video</string>
+ <string name="common.confirm">Confirm</string>
+ <string name="common.confirm_message">Do you want to %1$s %2$s?</string>
+
+ <string name="button_bar.home">Home</string>
+ <string name="button_bar.browse">Library</string>
+ <string name="button_bar.search">Search</string>
+ <string name="button_bar.playlists">Playlists</string>
+ <string name="button_bar.now_playing">Playing</string>
+ <string name="button_bar.podcasts">Podcasts</string>
+ <string name="button_bar.chat">Chat</string>
+
+ <string name="main.welcome_title">Welcome!</string>
+ <string name="main.welcome_text">Welcome to DSub! The app is currently configured to use the Subsonic demo server. After you\'ve
+ set up your personal server (available from <b>subsonic.org</b>), please go to <b>Settings</b> and change the configuration to connect to it.</string>
+ <string name="main.about_title">About DSub</string>
+ <string name="main.about_text">Author: Scott Jackson
+ \nEmail: daneren2005@gmail.com
+ \nVersion: %1$s
+ \nUsed Space: %2$s of %3$s
+ \nAvailable Space: %4$s of %5$s</string>
+ <string name="main.select_server">Select server</string>
+ <string name="main.shuffle">Shuffle play</string>
+ <string name="main.offline">Go Offline</string>
+ <string name="main.online">Go Online</string>
+ <string name="main.settings">Settings</string>
+ <string name="main.albums_title">Albums</string>
+ <string name="main.albums_newest">Recently added</string>
+ <string name="main.albums_recent">Recently played</string>
+ <string name="main.albums_frequent">Most played</string>
+ <string name="main.albums_highest">Top rated</string>
+ <string name="main.albums_starred">Starred</string>
+ <string name="main.albums_random">Random</string>
+ <string name="main.albums_genres">Genres</string>
+ <string name="main.back_confirm">Press back again to exit</string>
+
+ <string name="menu.search">Search</string>
+ <string name="menu.shuffle">Shuffle</string>
+ <string name="menu.refresh">Refresh</string>
+ <string name="menu.select">Select All</string>
+ <string name="menu.play">Play</string>
+ <string name="menu.play_last">Play Last</string>
+ <string name="menu.exit">Exit</string>
+ <string name="menu.settings">Settings</string>
+ <string name="menu.help">Help</string>
+ <string name="menu.about">About</string>
+ <string name="menu.add_playlist">Add To Playlist</string>
+ <string name="menu.remove_playlist">Remove From Playlist</string>
+ <string name="menu.deleted_playlist">Deleted playlist %s</string>
+ <string name="menu.deleted_playlist_error">Failed to delete playlist %s</string>
+ <string name="menu.log">Send Log</string>
+ <string name="menu.set_timer">Set Timer</string>
+ <string name="menu.check_podcasts">Check For New Episodes</string>
+ <string name="menu.add_podcast">Add Channel</string>
+
+ <string name="playlist.label">Playlists</string>
+ <string name="playlist.update_info">Update Information</string>
+ <string name="playlist.updated_info">Updated playlist information for %s</string>
+ <string name="playlist.updated_info_error">Failed to update playlist information for %s</string>
+ <string name="playlist.overwrite">Overwrite existing playlist</string>
+ <string name="playlist.add_to">Add to Playlist</string>
+ <string name="playlist.create_new">Create New</string>
+
+ <string name="help.label">Help</string>
+ <string name="help.title">Welcome to DSub!</string>
+ <string name="help.back">Back</string>
+ <string name="help.close">Close</string>
+ <string name="help.url">file:///android_asset/html/en/index.html</string>
+ <string name="help.loading">Loading...</string>
+
+ <string name="play_video.loading">Loading video...</string>
+ <string name="play_video.noplugin">Please install Adobe Flash Player from Android Market.</string>
+
+ <string name="search.label">Search</string>
+ <string name="search.title">Search</string>
+ <string name="search.search">Click to search</string>
+ <string name="search.no_match">No matches, please try again</string>
+ <string name="search.artists">Artists</string>
+ <string name="search.albums">Albums</string>
+ <string name="search.songs">Songs</string>
+ <string name="search.more">Show more</string>
+
+ <string name="progress.wait">Please wait...</string>
+
+ <string name="music_library.label">Media library</string>
+ <string name="music_library.label_offline">Offline media</string>
+
+ <string name="select_album.empty">No media found</string>
+ <string name="select_album.select">Select all</string>
+ <string name="select_album.n_selected">%d tracks selected.</string>
+ <string name="select_album.n_unselected">%d tracks unselected.</string>
+ <string name="select_album.more">More</string>
+ <string name="select_album.offline">Offline</string>
+ <string name="select_album.searching">Searching...</string>
+ <string name="select_album.no_sdcard">Error: No SD card available.</string>
+ <string name="select_album.no_network">Warning: No network available.</string>
+ <string name="select_album.not_licensed">Server not licensed. %d trial days left.</string>
+ <string name="select_album.donate_dialog_message">Get unlimited downloads by donating to Subsonic.</string>
+ <string name="select_album.donate_dialog_now">Now</string>
+ <string name="select_album.donate_dialog_later">Later</string>
+ <string name="select_album.donate_dialog_0_trial_days_left">Trial period is over</string>
+
+ <string name="offline.sync_dialog_title">Offline songs waiting to be synced</string>
+ <string name="offline.sync_dialog_message">Process %1$d offline scrobbles?
+ \nProcess %2$d offline stars?
+ </string>
+ <string name="offline.sync_dialog_default">Use action as default</string>
+ <string name="offline.sync_success">Successfully synced %1$d songs</string>
+ <string name="offline.sync_partial">Successfully synced %1$d of %2$d songs</string>
+ <string name="offline.sync_error">Failed to sync songs</string>
+
+ <string name="select_genre.empty">No genres found</string>
+ <string name="select_genre.blank">Blank</string>
+
+ <string name="select_podcasts.empty">No podcasts found</string>
+ <string name="select_podcasts.error">This podcast had an error while downloading on the server. The server must download it first.</string>
+ <string name="select_podcasts.skipped">This podcast has not been downloaded on the server. The server must download it first.</string>
+ <string name="select_podcasts.initializing">This podcast channel is being initialized on the server. Please reload after a moment.</string>
+ <string name="select_podcasts.server_download">Download on server</string>
+ <string name="select_podcasts.server_delete">Delete from server</string>
+ <string name="select_podcasts.downloading">Now downloading %s on the server</string>
+ <string name="select_podcasts.refreshing">The server is checking for new podcasts now</string>
+ <string name="select_podcasts.deleted">Deleted podcast %s</string>
+ <string name="select_podcasts.deleted_error">Failed to delete podcast %s</string>
+ <string name="select_podcasts.add_url">URL:</string>
+ <string name="select_podcasts.created_error">Failed to add podcast</string>
+ <string name="select_podcasts.invalid_podcast_channel">Invalid podcast channel: %s</string>
+
+ <string name="select_playlist.empty">No saved playlists on server</string>
+
+ <string name="download.empty">Playlist is empty</string>
+ <string name="download.shuffle_loading">Shuffle list is loading...</string>
+ <string name="download.playerstate_downloading">Downloading - %s</string>
+ <string name="download.playerstate_buffering">Buffering</string>
+ <string name="download.playerstate_playing_shuffle">Playing shuffle</string>
+ <string name="download.menu_show_album">Show album</string>
+ <string name="download.menu_lyrics">Lyrics</string>
+ <string name="download.menu_remove">Remove from queue</string>
+ <string name="download.menu_delete">Delete cache</string>
+ <string name="download.menu_remove_all">Remove all</string>
+ <string name="download.menu_screen_on">Screen on</string>
+ <string name="download.menu_screen_off">Screen off</string>
+ <string name="download.menu_shuffle">Shuffle</string>
+ <string name="download.menu_toggle">Toggle</string>
+ <string name="download.menu_save">Save playlist</string>
+ <string name="download.menu_shuffle_notification">Playlist was shuffled</string>
+ <string name="download.playlist_title">Save playlist</string>
+ <string name="download.playlist_name">Enter the playlist name:</string>
+ <string name="download.playlist_saving">Saving playlist \"%s\"...</string>
+ <string name="download.playlist_done">Playlist was successfully saved.</string>
+ <string name="download.playlist_error">Failed to save playlist, please try later.</string>
+ <string name="download.repeat_off">Repeat off</string>
+ <string name="download.repeat_all">Repeat all</string>
+ <string name="download.repeat_single">Repeat song</string>
+ <string name="download.visualizer_on">Turned on visualizer.</string>
+ <string name="download.visualizer_off">Turned off visualizer.</string>
+ <string name="download.jukebox_on">Turned on remote control. Music is played on the computer.</string>
+ <string name="download.jukebox_off">Turned off remote control. Music is played on the phone.</string>
+ <string name="download.jukebox_volume">Remote volume</string>
+ <string name="download.jukebox_server_too_old">Remote control is not supported. Please upgrade your Subsonic server.</string>
+ <string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
+ <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users > Settings</b> on your Subsonic server.</string>
+ <string name="download.show_downloading">Show Downloading</string>
+ <string name="download.show_now_playing">Show Now Playing</string>
+ <string name="download.timer_length">Timer Length</string>
+ <string name="download.start_timer">Start Timer</string>
+ <string name="download.stop_timer">Stop Timer</string>
+ <string name="download.need_download">Video needs to be downloaded first</string>
+ <string name="download.no_streaming_player">No player can play this stream</string>
+ <string name="download.playing_out_of">Playing: %1$d/%2$d</string>
+
+ <string name="starring_content_starred">Starred \"%s\"</string>
+ <string name="starring_content_unstarred">Unstarred \"%s\"</string>
+ <string name="starring_content_error">Failed to update \"%s\", please try later.</string>
+
+ <string name="playlist_error">Failed to grab list of playlists</string>
+ <string name="updated_playlist">Added %1$s songs to \"%2$s\"</string>
+ <string name="updated_playlist_error">Failed to update \"%s\", please try later.</string>
+ <string name="removed_playlist">Removed %1$s songs from \"%2$s\"</string>
+
+ <string name="song_details.all">%1$s %2$s</string>
+ <string name="song_details.kbps">%d kbps</string>
+ <string name="song_details.error">Error</string>
+ <string name="song_details.skipped">Skipped</string>
+ <string name="song_details.downloading">Downloading</string>
+
+ <string name="lyrics.nomatch">No lyrics found</string>
+
+ <string name="error.label">Error</string>
+
+ <string name="settings.title">DSub 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>
+ <string name="settings.servers_title">Servers</string>
+ <string name="settings.server_unused">Unused</string>
+ <string name="settings.server_name">Name</string>
+ <string name="settings.server_address">Server address</string>
+ <string name="settings.server_username">Username</string>
+ <string name="settings.server_password">Password</string>
+ <string name="settings.server_open_browser">Open in browser</string>
+ <string name="settings.cache_title">Music cache</string>
+ <string name="settings.preload_wifi">Songs to preload (Wifi)</string>
+ <string name="settings.preload_mobile">Songs to preload (Mobile)</string>
+ <string name="settings.cache_size">Cache size (MB)</string>
+ <string name="settings.cache_location">Cache location</string>
+ <string name="settings.cache_location_error">Invalid cache location. Using default.</string>
+ <string name="settings.cache_clear">Clear Cache</string>
+ <string name="settings.cache_clear_complete">Finished clearing cache</string>
+ <string name="settings.testing_connection">Testing connection...</string>
+ <string name="settings.testing_ok">Connection is OK</string>
+ <string name="settings.testing_unlicensed">Connection is OK. Server unlicensed.</string>
+ <string name="settings.connection_failure">Connection failure.</string>
+ <string name="settings.invalid_url">Please specify a valid URL.</string>
+ <string name="settings.invalid_username">Please specify a valid username (no trailing spaces).</string>
+ <string name="settings.appearance_title">Appearance</string>
+ <string name="settings.theme_title">Theme</string>
+ <string name="settings.theme_light">Light</string>
+ <string name="settings.theme_dark">Dark</string>
+ <string name="settings.theme_black">Black</string>
+ <string name="settings.theme_holo">Holo</string>
+ <string name="settings.theme_light_fullscreen">Light Fullscreen</string>
+ <string name="settings.theme_dark_fullscreen">Dark Fullscreen</string>
+ <string name="settings.theme_black_fullscreen">Black Fullscreen</string>
+ <string name="settings.theme_holo_fullscreen">Holo Fullscreen</string>
+ <string name="settings.track_title">Display Track #</string>
+ <string name="settings.track_summary">Display Track # in front of songs if one exists</string>
+ <string name="settings.network_title">Network</string>
+ <string name="settings.max_bitrate_wifi">Max Audio bitrate - Wi-Fi</string>
+ <string name="settings.max_bitrate_mobile">Max Audio bitrate - Mobile</string>
+ <string name="settings.max_bitrate_32">32 Kbps</string>
+ <string name="settings.max_bitrate_64">64 Kbps</string>
+ <string name="settings.max_bitrate_80">80 Kbps</string>
+ <string name="settings.max_bitrate_96">96 Kbps</string>
+ <string name="settings.max_bitrate_112">112 Kbps</string>
+ <string name="settings.max_bitrate_128">128 Kbps</string>
+ <string name="settings.max_bitrate_160">160 Kbps</string>
+ <string name="settings.max_bitrate_192">192 Kbps</string>
+ <string name="settings.max_bitrate_256">256 Kbps</string>
+ <string name="settings.max_bitrate_320">320 Kbps</string>
+ <string name="settings.max_video_bitrate_wifi">Max Video bitrate - Wi-Fi</string>
+ <string name="settings.max_video_bitrate_mobile">Max Video bitrate - Mobile</string>
+ <string name="settings.max_video_bitrate_200">200 Kbps</string>
+ <string name="settings.max_video_bitrate_300">300 Kbps</string>
+ <string name="settings.max_video_bitrate_400">400 Kbps</string>
+ <string name="settings.max_video_bitrate_500">500 Kbps</string>
+ <string name="settings.max_video_bitrate_700">700 Kbps</string>
+ <string name="settings.max_video_bitrate_1000">1000 Kbps</string>
+ <string name="settings.max_video_bitrate_1500">1500 Kbps</string>
+ <string name="settings.max_video_bitrate_2000">2000 Kbps</string>
+ <string name="settings.max_video_bitrate_3000">3000 Kbps</string>
+ <string name="settings.max_video_bitrate_5000">5000 Kbps</string>
+ <string name="settings.max_bitrate_unlimited">Unlimited</string>
+ <string name="settings.wifi_required_title">Wi-Fi streaming only</string>
+ <string name="settings.wifi_required_summary">Only stream media if connected to Wi-Fi</string>
+ <string name="settings.network_timeout_title">Network Timeout</string>
+ <string name="settings.network_timeout_10000">10 seconds</string>
+ <string name="settings.network_timeout_15000">15 seconds</string>
+ <string name="settings.network_timeout_30000">30 seconds</string>
+ <string name="settings.network_timeout_45000">45 seconds</string>
+ <string name="settings.network_timeout_60000">60 seconds</string>
+ <string name="settings.preload_1">1 song</string>
+ <string name="settings.preload_2">2 songs</string>
+ <string name="settings.preload_3">3 songs</string>
+ <string name="settings.preload_5">5 songs</string>
+ <string name="settings.preload_10">10 songs</string>
+ <string name="settings.preload_unlimited">Unlimited</string>
+ <string name="settings.clear_search_history">Clear search history</string>
+ <string name="settings.search_history_cleared">Search history cleared</string>
+ <string name="settings.other_title">Other settings</string>
+ <string name="settings.scrobble_title">Scrobble to Last.fm</string>
+ <string name="settings.scrobble_summary">Remember to set up your Last.fm user and password on the DSub server</string>
+ <string name="settings.hide_media_title">Hide from other</string>
+ <string name="settings.hide_media_summary">Hide music files from other apps.</string>
+ <string name="settings.hide_media_toast">Takes effect next time Android scans your phone for music.</string>
+ <string name="settings.media_button_title">Media buttons</string>
+ <string name="settings.media_button_summary">Respond to phone, headset and Bluetooth media buttons</string>
+ <string name="settings.screen_lit_title">Keep screen on</string>
+ <string name="settings.screen_lit_summary">Keeping the screen on while downloading improves download speed.</string>
+ <string name="settings.playlist_title">Playlists</string>
+ <string name="settings.playlist_random_size_title">Random Size</string>
+ <string name="settings.buffer_length">Buffer Length (0 = when fully cached)</string>
+ <string name="settings.sleep_timer_title">Sleep Timer</string>
+ <string name="settings.sleep_timer_duration_title">Sleep Timer Duration</string>
+ <string name="settings.sleep_timer_off">Off</string>
+ <string name="settings.sleep_timer_on">On</string>
+ <string name="settings.sleep_timer_always_on">Always On</string>
+ <string name="settings.temp_loss_title">Temporary Loss of Focus</string>
+ <string name="settings.temp_loss_pause">Always Pause</string>
+ <string name="settings.temp_loss_pause_lower">Pause, lower volume when requested</string>
+ <string name="settings.temp_loss_lower">Always lower volume</string>
+ <string name="settings.temp_loss_nothing">Do Nothing</string>
+ <string name="settings.persistent_title">Persistent Notification</string>
+ <string name="settings.persistent_summary">Show the notification even after pausing. Press the stop button to clear it away.</string>
+ <string name="settings.gapless_playback">Gapless Playback</string>
+ <string name="settings.gapless_playback_summary">The Galaxy S3 seems to be experiencing freezes/other weird issue since the introduction of gapless playback. Turn this off to fix the issue.</string>
+ <string name="settings.chat_refresh">Chat Refresh Rate (Secs)</string>
+ <string name="settings.chat_enabled">Chat Enabled</string>
+ <string name="settings.chat_enabled_summary">Whether or not to display the chat tab. Restart app after changing.</string>
+ <string name="settings.video_title">Video</string>
+ <string name="settings.video_player">Video Player</string>
+ <string name="settings.video_raw">Raw (Requires Subsonic 4.8+)</string>
+ <string name="settings.video_hls">HTTP Live Stream (HLS) (Requires Subsonic 4.8+)</string>
+ <string name="settings.video_transcode">Direct Transcode (Requires video -> mp4 or similar setup on Server)</string>
+ <string name="settings.video_flash">Flash (Requires Plugin)</string>
+
+ <string name="shuffle.title">Shuffle By</string>
+ <string name="shuffle.startYear">Start Year:</string>
+ <string name="shuffle.endYear">End Year:</string>
+ <string name="shuffle.genre">Genre:</string>
+ <string name="shuffle.pick_genre">Pick a genre</string>
+
+ <string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>
+
+ <string name="background_task.wait">Please wait...</string>
+ <string name="background_task.loading">Loading.</string>
+ <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
+ <string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
+ <string name="background_task.not_found">Resource not found. Please check the server address.</string>
+ <string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string>
+
+ <string name="service.connecting">Contacting server, please wait.</string>
+
+ <string name="parser.reading">Reading from server.</string>
+ <string name="parser.reading_done">Reading from server. Done!</string>
+ <string name="parser.upgrade_client">Incompatible versions. Please upgrade DSub Android app.</string>
+ <string name="parser.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string>
+ <string name="parser.not_authenticated">Wrong username or password.</string>
+ <string name="parser.not_authorized">Not authorized. Check user permissions in Subsonic server.</string>
+ <string name="parser.artist_count">Got %d artists.</string>
+
+ <string name="select_artist.refresh">Refresh</string>
+ <string name="select_artist.folder">Select folder</string>
+ <string name="select_artist.all_folders">All folders</string>
+
+ <string name="equalizer.label">Equalizer</string>
+ <string name="equalizer.enabled">Enabled</string>
+ <string name="equalizer.preset">Select preset</string>
+
+ <string name="widget.4x1">DSub (4x1)</string>
+ <string name="widget.4x2">DSub (4x2)</string>
+ <string name="widget.4x3">DSub (4x3)</string>
+ <string name="widget.4x4">DSub (4x4)</string>
+ <string name="widget.initial_text">Touch to select music</string>
+ <string name="widget.sdcard_busy">SD card unavailable</string>
+ <string name="widget.sdcard_missing">No SD card</string>
+
+ <string name="util.bytes_format.gigabyte">0.00 GB</string>
+ <string name="util.bytes_format.megabyte">0.00 MB</string>
+ <string name="util.bytes_format.kilobyte">0 KB</string>
+ <string name="util.bytes_format.byte">0 B</string>
+
+ <string name="changelog_full_title">Change Log</string>
+ <string name="changelog_title">What\'s New</string>
+ <string name="changelog_ok_button">OK</string>
+ <string name="changelog_show_full">More…</string>
+
+ <string name="chat.send_a_message">Send a message</string>
+
+ <string name="changelog_version_format" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">Version <xliff:g id="version_name">%s</xliff:g></string>
+
+ <plurals name="select_album_n_songs">
+ <item quantity="zero">No songs</item>
+ <item quantity="one">One song</item>
+ <item quantity="other">%d songs</item>
+ </plurals>
+ <plurals name="select_album_n_songs_downloading">
+ <item quantity="one">One song scheduled for download.</item>
+ <item quantity="other">%d songs scheduled for download.</item>
+ </plurals>
+ <plurals name="select_album_n_songs_added">
+ <item quantity="one">One song added to play queue.</item>
+ <item quantity="other">%d songs added to play queue.</item>
+ </plurals>
+ <plurals name="select_album_donate_dialog_n_trial_days_left">
+ <item quantity="one">One day left of trial period</item>
+ <item quantity="other">%d days left of trial period</item>
+ </plurals>
+
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 00000000..4dcce34f --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="PlaybackControl"> + <item name="android:background">@drawable/media_button</item> + <item name="android:scaleType">fitCenter</item> + <item name="android:padding">6dip</item> + <item name="android:layout_marginLeft">4dip</item> + <item name="android:layout_marginRight">4dip</item> + <item name="android:layout_width">54dip</item> + <item name="android:layout_height">54dip</item> + <item name="android:contentDescription">@null</item> + </style> + + <style name="PlaybackControl.Small" parent="@style/PlaybackControl"> + <item name="android:padding">4dip</item> + <item name="android:layout_width">46dip</item> + <item name="android:layout_height">46dip</item> + </style> + + <style name="MenuBarButton"> + <item name="android:layout_width">0dip</item> + <item name="android:layout_height">45dip</item> + <item name="android:layout_weight">1</item> + <item name="android:textSize">14sp</item> + <item name="android:textStyle">bold</item> + <item name="android:background">@drawable/menubar_button</item> + <item name="android:textColor">?android:textColorPrimary</item> + </style> + + <style name="DragDropListView"> + <item name="drag_enabled">true</item> + <item name="collapsed_height">1dp</item> + <item name="drag_scroll_start">1.0</item> + <item name="max_drag_scroll_speed">2.0</item> + <item name="float_alpha">0.6</item> + <item name="slide_shuffle_speed">0.3</item> + <item name="track_drag_sort">false</item> + <item name="use_default_controller">true</item> + <item name="drag_handle_id">@id/drag_handle</item> + <item name="sort_enabled">true</item> + <item name="remove_enabled">false</item> + <item name="remove_mode">flingRemove</item> + <item name="drag_start_mode">onLongPress</item> + </style> +</resources>
\ No newline at end of file diff --git a/res/values/themes.xml b/res/values/themes.xml new file mode 100644 index 00000000..33dd2de7 --- /dev/null +++ b/res/values/themes.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="Theme.DSub.Light" parent="Theme.Sherlock.Light"> + <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Light</item> + <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Light</item> + <item name="offline_icon">@drawable/main_offline_light</item> + <item name="media_button_backward">@drawable/media_backward_light</item> + <item name="media_button_forward">@drawable/media_forward_light</item> + <item name="media_button_pause">@drawable/media_pause_light</item> + <item name="media_button_repeat_off">@drawable/media_repeat_off_light</item> + <item name="media_button_start">@drawable/media_start_light</item> + <item name="media_button_stop">@drawable/media_stop_light</item> + <item name="chat">@drawable/ic_menu_chat_light</item> + <item name="chat_send">@drawable/ic_menu_chat_send_light</item> + </style> + <style name="Theme.DSub.Dark" parent="Theme.Sherlock"> + <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</item> + <item name="media_button_backward">@drawable/media_backward</item> + <item name="media_button_forward">@drawable/media_forward</item> + <item name="media_button_pause">@drawable/media_pause</item> + <item name="media_button_repeat_off">@drawable/media_repeat_off</item> + <item name="media_button_start">@drawable/media_start</item> + <item name="media_button_stop">@drawable/media_stop</item> + <item name="chat">@drawable/ic_menu_chat_dark</item> + <item name="chat_send">@drawable/ic_menu_chat_send_dark</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="Theme.Sherlock"> + <item name="actionBarStyle">@style/Widget.DSub.ActionBarStyle.Holo</item> + <item name="android:actionBarStyle">@style/Widget.DSub.ActionBarStyle.Holo</item> + <item name="android:textColorSecondary">@color/cyan</item> + <item name="android:windowBackground">@drawable/background</item> + <item name="offline_icon">@drawable/main_offline</item> + <item name="media_button_backward">@drawable/media_backward</item> + <item name="media_button_forward">@drawable/media_forward</item> + <item name="media_button_pause">@drawable/media_pause</item> + <item name="media_button_repeat_off">@drawable/media_repeat_off</item> + <item name="media_button_start">@drawable/media_start</item> + <item name="media_button_stop">@drawable/media_stop</item> + <item name="chat">@drawable/ic_menu_chat_dark</item> + <item name="chat_send">@drawable/ic_menu_chat_send_dark</item> + </style> + + <style name="Theme.DSub.Light.Fullscreen" parent="Theme.DSub.Light"> + <item name="android:windowFullscreen">true</item> + </style> + <style name="Theme.DSub.Dark.Fullscreen" parent="Theme.DSub.Dark"> + <item name="android:windowFullscreen">true</item> + </style> + <style name="Theme.DSub.Black.Fullscreen" parent="Theme.DSub.Black"> + <item name="android:windowFullscreen">true</item> + </style> + <style name="Theme.DSub.Holo.Fullscreen" parent="Theme.DSub.Holo"> + <item name="android:windowFullscreen">true</item> + </style> + + <style name="Widget.DSub.ActionBarStyle.Light" parent="Widget.Sherlock.Light.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.Dark" parent="Widget.Sherlock.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.Sherlock.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> +</resources> diff --git a/res/xml/appwidget4x1.xml b/res/xml/appwidget4x1.xml new file mode 100644 index 00000000..65f47dba --- /dev/null +++ b/res/xml/appwidget4x1.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:minWidth="272dip" + android:minHeight="56dip" + android:updatePeriodMillis="0" + android:resizeMode="horizontal|vertical" + android:initialLayout="@layout/appwidget4x1"/>
\ No newline at end of file diff --git a/res/xml/appwidget4x2.xml b/res/xml/appwidget4x2.xml new file mode 100644 index 00000000..f40204a7 --- /dev/null +++ b/res/xml/appwidget4x2.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:minWidth="272dip" + android:minHeight="110dip" + android:updatePeriodMillis="0" + android:resizeMode="horizontal|vertical" + android:initialLayout="@layout/appwidget4x2"/>
\ No newline at end of file diff --git a/res/xml/appwidget4x3.xml b/res/xml/appwidget4x3.xml new file mode 100644 index 00000000..51ae97ed --- /dev/null +++ b/res/xml/appwidget4x3.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:minWidth="272dip" + android:minHeight="180dp" + android:updatePeriodMillis="0" + android:resizeMode="horizontal|vertical" + android:initialLayout="@layout/appwidget4x3"/>
\ No newline at end of file diff --git a/res/xml/appwidget4x4.xml b/res/xml/appwidget4x4.xml new file mode 100644 index 00000000..40956dcf --- /dev/null +++ b/res/xml/appwidget4x4.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:minWidth="272dip" + android:minHeight="250dp" + android:updatePeriodMillis="0" + android:resizeMode="horizontal|vertical" + android:initialLayout="@layout/appwidget4x4"/>
\ No newline at end of file diff --git a/res/xml/changelog.xml b/res/xml/changelog.xml new file mode 100644 index 00000000..7bc0bddc --- /dev/null +++ b/res/xml/changelog.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<changelog></changelog> diff --git a/res/xml/searchable.xml b/res/xml/searchable.xml new file mode 100644 index 00000000..a3713aa3 --- /dev/null +++ b/res/xml/searchable.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<searchable xmlns:android="http://schemas.android.com/apk/res/android" + android:label="@string/common.appname" + android:hint="@string/search.title" + android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" + android:voiceLanguageModel="web_search" + android:searchSuggestAuthority="github.daneren2005.dsub.provider.DSubSearchProvider" + android:searchSuggestSelection=" ?" > +</searchable>
\ No newline at end of file diff --git a/res/xml/settings.xml b/res/xml/settings.xml new file mode 100644 index 00000000..eb139b83 --- /dev/null +++ b/res/xml/settings.xml @@ -0,0 +1,209 @@ +<?xml version="1.0" encoding="utf-8"?> + +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + android:title="@string/settings.title"> + + <PreferenceCategory + android:key="server" + android:title="@string/settings.servers_title"> + + <Preference + android:key="serverAdd" + android:title="@string/settings.servers_add"/> + + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/settings.appearance_title"> + + <ListPreference + android:title="@string/settings.theme_title" + android:key="theme" + android:defaultValue="holo" + android:entryValues="@array/themeValues" + android:entries="@array/themeNames"/> + + <CheckBoxPreference + android:title="@string/settings.track_title" + android:summary="@string/settings.track_summary" + android:key="displayTrack" + android:defaultValue="false"/> + + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/settings.video_title"> + + <ListPreference + android:title="@string/settings.video_player" + android:key="videoPlayer" + android:defaultValue="raw" + android:entryValues="@array/videoPlayerValues" + android:entries="@array/videoPlayerNames"/> + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/settings.network_title"> + + <ListPreference + android:title="@string/settings.max_bitrate_wifi" + android:key="maxBitrateWifi" + android:defaultValue="0" + android:entryValues="@array/maxBitrateValues" + android:entries="@array/maxBitrateNames"/> + + <ListPreference + android:title="@string/settings.max_bitrate_mobile" + android:key="maxBitrateMobile" + android:defaultValue="0" + android:entryValues="@array/maxBitrateValues" + android:entries="@array/maxBitrateNames"/> + + <ListPreference + android:title="@string/settings.max_video_bitrate_wifi" + android:key="maxVideoBitrateWifi" + android:defaultValue="0" + android:entryValues="@array/maxVideoBitrateValues" + android:entries="@array/maxVideoBitrateNames"/> + + <ListPreference + android:title="@string/settings.max_video_bitrate_mobile" + android:key="maxVideoBitrateMobile" + android:defaultValue="0" + android:entryValues="@array/maxVideoBitrateValues" + android:entries="@array/maxVideoBitrateNames"/> + + <CheckBoxPreference + android:title="@string/settings.wifi_required_title" + android:summary="@string/settings.wifi_required_summary" + android:key="wifiRequiredForDownload" + android:defaultValue="false"/> + + <ListPreference + android:title="@string/settings.network_timeout_title" + android:key="networkTimeout" + android:defaultValue="15000" + android:entryValues="@array/networkTimeoutValues" + android:entries="@array/networkTimeoutNames"/> + + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/settings.playlist_title"> + + <EditTextPreference + android:title="@string/settings.buffer_length" + android:key="bufferLength" + android:defaultValue="5" + android:digits="0123456789"/> + + <EditTextPreference + android:title="@string/settings.playlist_random_size_title" + android:key="randomSize" + android:defaultValue="20" + android:digits="0123456789"/> + + <ListPreference + android:title="@string/settings.temp_loss_title" + android:key="tempLoss" + android:defaultValue="1" + android:entryValues="@array/tempLossValues" + android:entries="@array/tempLossNames"/> + + <CheckBoxPreference + android:title="@string/settings.persistent_title" + android:summary="@string/settings.persistent_summary" + android:key="persistentNotification" + android:defaultValue="false"/> + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/settings.cache_title"> + + <EditTextPreference + android:title="@string/settings.cache_size" + android:key="cacheSize" + android:defaultValue="2000" + android:digits="0123456789"/> + + <EditTextPreference + android:title="@string/settings.cache_location" + android:key="cacheLocation"/> + + <ListPreference + android:title="@string/settings.preload_wifi" + android:key="preloadCountWifi" + android:defaultValue="3" + android:entryValues="@array/preloadCountValues" + android:entries="@array/preloadCountNames"/> + + <ListPreference + android:title="@string/settings.preload_mobile" + android:key="preloadCountMobile" + android:defaultValue="3" + android:entryValues="@array/preloadCountValues" + android:entries="@array/preloadCountNames"/> + + <Preference + android:key="clearCache" + android:title="@string/settings.cache_clear" + android:persistent="false"/> + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/button_bar.chat"> + + <CheckBoxPreference + android:title="@string/settings.chat_enabled" + android:summary="@string/settings.chat_enabled_summary" + android:key="chatEnabled" + android:defaultValue="true"/> + + <EditTextPreference + android:title="@string/settings.chat_refresh" + android:key="chatRefreshRate" + android:defaultValue="30" + android:digits="0123456789"/> + </PreferenceCategory> + + <PreferenceCategory + android:title="@string/settings.other_title"> + + <CheckBoxPreference + android:title="@string/settings.scrobble_title" + android:summary="@string/settings.scrobble_summary" + android:key="scrobble" + android:defaultValue="false"/> + + <CheckBoxPreference + android:title="@string/settings.hide_media_title" + android:summary="@string/settings.hide_media_summary" + android:key="hideMedia" + android:defaultValue="false"/> + + <CheckBoxPreference + android:title="@string/settings.media_button_title" + android:summary="@string/settings.media_button_summary" + android:key="mediaButtons" + android:defaultValue="true"/> + + <CheckBoxPreference + android:title="@string/settings.screen_lit_title" + android:summary="@string/settings.screen_lit_summary" + android:key="screenLitOnDownload" + android:defaultValue="true"/> + + <CheckBoxPreference + android:title="@string/settings.gapless_playback" + android:summary="@string/settings.gapless_playback_summary" + android:key="gaplessPlayback" + android:defaultValue="true"/> + + <Preference + android:key="clearSearchHistory" + android:title="@string/settings.clear_search_history" + android:persistent="false"/> + + </PreferenceCategory> + +</PreferenceScreen> diff --git a/src/github/daneren2005/dsub/activity/DownloadActivity.java b/src/github/daneren2005/dsub/activity/DownloadActivity.java new file mode 100644 index 00000000..bfdc9eb9 --- /dev/null +++ b/src/github/daneren2005/dsub/activity/DownloadActivity.java @@ -0,0 +1,96 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.activity; + +import github.daneren2005.dsub.R; +import android.os.Bundle; +import android.view.MotionEvent; +import github.daneren2005.dsub.fragments.DownloadFragment; +import android.app.Dialog; +import android.view.LayoutInflater; +import android.widget.EditText; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.util.Log; +import android.view.View; +import com.actionbarsherlock.view.MenuItem; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; +import github.daneren2005.dsub.util.SilentBackgroundTask; +import github.daneren2005.dsub.util.Util; +import java.util.LinkedList; +import java.util.List; + +public class DownloadActivity extends SubsonicActivity { + private static final String TAG = DownloadActivity.class.getSimpleName(); + private EditText playlistNameView; + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.download_activity); + + if (findViewById(R.id.download_container) != null && savedInstanceState == null) { + currentFragment = new DownloadFragment(); + currentFragment.setPrimaryFragment(true); + getSupportFragmentManager().beginTransaction().add(R.id.download_container, currentFragment, currentFragment.getSupportTag() + "").commit(); + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(item.getItemId() == android.R.id.home) { + Intent i = new Intent(); + i.setClass(this, MainActivity.class); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + if(currentFragment != null) { + return ((DownloadFragment)currentFragment).getGestureDetector().onTouchEvent(me); + } else { + return false; + } + } + + @Override + public void onBackPressed() { + if(onBackPressedSupport()) { + super.onBackPressed(); + } + } +} diff --git a/src/github/daneren2005/dsub/activity/EqualizerActivity.java b/src/github/daneren2005/dsub/activity/EqualizerActivity.java new file mode 100644 index 00000000..d9605fc5 --- /dev/null +++ b/src/github/daneren2005/dsub/activity/EqualizerActivity.java @@ -0,0 +1,278 @@ +/* + 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 2011 (C) Sindre Mehus + */ +package github.daneren2005.dsub.activity; + +import java.util.HashMap; +import java.util.Map; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.media.audiofx.Equalizer; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.audiofx.EqualizerController; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; + +/** + * Equalizer controls. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class EqualizerActivity extends Activity { + private static final String TAG = EqualizerActivity.class.getSimpleName(); + + private static final int MENU_GROUP_PRESET = 100; + + private final Map<Short, SeekBar> bars = new HashMap<Short, SeekBar>(); + private EqualizerController equalizerController; + private Equalizer equalizer; + private short masterLevel = 0; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + setContentView(R.layout.equalizer); + equalizerController = DownloadServiceImpl.getInstance().getEqualizerController(); + equalizer = equalizerController.getEqualizer(); + + initEqualizer(); + + final View presetButton = findViewById(R.id.equalizer_preset); + registerForContextMenu(presetButton); + presetButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + presetButton.showContextMenu(); + } + }); + + CheckBox enabledCheckBox = (CheckBox) findViewById(R.id.equalizer_enabled); + enabledCheckBox.setChecked(equalizer.getEnabled()); + enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + setEqualizerEnabled(b); + } + }); + } + + @Override + protected void onPause() { + super.onPause(); + equalizerController.saveSettings(); + + if(!equalizer.getEnabled()) { + equalizerController.release(); + } + } + + @Override + protected void onResume() { + super.onResume(); + equalizerController = DownloadServiceImpl.getInstance().getEqualizerController(); + equalizer = equalizerController.getEqualizer(); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, view, menuInfo); + + short currentPreset; + try { + currentPreset = equalizer.getCurrentPreset(); + } catch (Exception x) { + currentPreset = -1; + } + + for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++) { + MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset)); + if (preset == currentPreset) { + menuItem.setChecked(true); + } + } + menu.setGroupCheckable(MENU_GROUP_PRESET, true, true); + } + + @Override + public boolean onContextItemSelected(MenuItem menuItem) { + short preset = (short) menuItem.getItemId(); + equalizer.usePreset(preset); + updateBars(false); + return true; + } + + private void setEqualizerEnabled(boolean enabled) { + SharedPreferences prefs = Util.getPreferences(EqualizerActivity.this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_EQUALIZER_ON, enabled); + editor.commit(); + equalizer.setEnabled(enabled); + updateBars(true); + } + + private void updateBars(boolean changedEnabled) { + boolean isEnabled = equalizer.getEnabled(); + short minEQLevel = equalizer.getBandLevelRange()[0]; + short maxEQLevel = equalizer.getBandLevelRange()[1]; + for (Map.Entry<Short, SeekBar> entry : bars.entrySet()) { + short band = entry.getKey(); + SeekBar bar = entry.getValue(); + bar.setEnabled(isEnabled); + if(band >= (short)0) { + short setLevel; + if(changedEnabled) { + setLevel = (short)(equalizer.getBandLevel(band) - masterLevel); + bar.setProgress(equalizer.getBandLevel(band) - minEQLevel); + } else { + bar.setProgress(equalizer.getBandLevel(band) - minEQLevel); + setLevel = (short)(equalizer.getBandLevel(band) + masterLevel); + } + if(setLevel < minEQLevel) { + setLevel = minEQLevel; + } else if(setLevel > maxEQLevel) { + setLevel = maxEQLevel; + } + equalizer.setBandLevel(band, setLevel); + } else if(!isEnabled) { + bar.setProgress(-minEQLevel); + } + } + + if(!isEnabled) { + masterLevel = 0; + SharedPreferences prefs = Util.getPreferences(EqualizerActivity.this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel); + editor.commit(); + } + } + + private void initEqualizer() { + LinearLayout layout = (LinearLayout) findViewById(R.id.equalizer_layout); + + final short minEQLevel = equalizer.getBandLevelRange()[0]; + final short maxEQLevel = equalizer.getBandLevelRange()[1]; + + // Setup Pregain + SharedPreferences prefs = Util.getPreferences(this); + masterLevel = (short)prefs.getInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, 0); + initPregain(layout, minEQLevel, maxEQLevel); + + for (short i = 0; i < equalizer.getNumberOfBands(); i++) { + final short band = i; + + View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null); + TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency); + final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level); + SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar); + + freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz"); + + bars.put(band, bar); + bar.setMax(maxEQLevel - minEQLevel); + short level = equalizer.getBandLevel(band); + if(equalizer.getEnabled()) { + level = (short) (level - masterLevel); + } + bar.setProgress(level - minEQLevel); + bar.setEnabled(equalizer.getEnabled()); + updateLevelText(levelTextView, level); + + bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + short level = (short) (progress + minEQLevel); + if (fromUser) { + equalizer.setBandLevel(band, (short)(level + masterLevel)); + } + updateLevelText(levelTextView, level); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + layout.addView(bandBar); + } + } + + private void initPregain(LinearLayout layout, final short minEQLevel, final short maxEQLevel) { + View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null); + TextView freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency); + final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level); + SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar); + + freqTextView.setText("Master"); + + bars.put((short)-1, bar); + bar.setMax(maxEQLevel - minEQLevel); + bar.setProgress(masterLevel - minEQLevel); + bar.setEnabled(equalizer.getEnabled()); + updateLevelText(levelTextView, masterLevel); + + bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + masterLevel = (short) (progress + minEQLevel); + if (fromUser) { + SharedPreferences prefs = Util.getPreferences(EqualizerActivity.this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_EQUALIZER_SETTINGS, masterLevel); + editor.commit(); + for (short i = 0; i < equalizer.getNumberOfBands(); i++) { + short level = (short) ((bars.get(i).getProgress() + minEQLevel) + masterLevel); + equalizer.setBandLevel(i, level); + } + } + updateLevelText(levelTextView, masterLevel); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + layout.addView(bandBar); + } + + private void updateLevelText(TextView levelTextView, short level) { + levelTextView.setText((level > 0 ? "+" : "") + level / 100 + " dB"); + } + +} diff --git a/src/github/daneren2005/dsub/activity/HelpActivity.java b/src/github/daneren2005/dsub/activity/HelpActivity.java new file mode 100644 index 00000000..6dc516bf --- /dev/null +++ b/src/github/daneren2005/dsub/activity/HelpActivity.java @@ -0,0 +1,117 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ + +package github.daneren2005.dsub.activity; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Button; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.Util; + +/** + * An HTML-based help screen with Back and Done buttons at the bottom. + * + * @author Sindre Mehus + */ +public final class HelpActivity extends Activity { + + private WebView webView; + private Button backButton; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.help); + + webView = (WebView) findViewById(R.id.help_contents); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new HelpClient()); + if (bundle != null) { + webView.restoreState(bundle); + } else { + webView.loadUrl(getResources().getString(R.string.help_url)); + } + + backButton = (Button) findViewById(R.id.help_back); + backButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + webView.goBack(); + } + }); + + Button doneButton = (Button) findViewById(R.id.help_close); + doneButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + protected void onSaveInstanceState(Bundle state) { + webView.saveState(state); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (webView.canGoBack()) { + webView.goBack(); + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + private final class HelpClient extends WebViewClient { + @Override + public void onLoadResource(WebView webView, String url) { + setProgressBarIndeterminateVisibility(true); + setTitle(getResources().getString(R.string.help_loading)); + super.onLoadResource(webView, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + setProgressBarIndeterminateVisibility(false); + setTitle(view.getTitle()); + backButton.setEnabled(view.canGoBack()); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + Util.toast(HelpActivity.this, description); + } + } +} diff --git a/src/github/daneren2005/dsub/activity/MainActivity.java b/src/github/daneren2005/dsub/activity/MainActivity.java new file mode 100644 index 00000000..c077c87c --- /dev/null +++ b/src/github/daneren2005/dsub/activity/MainActivity.java @@ -0,0 +1,320 @@ +package github.daneren2005.dsub.activity; + +import android.app.AlertDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.view.Menu; +import android.support.v4.view.ViewPager; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.fragments.ChatFragment; +import github.daneren2005.dsub.fragments.MainFragment; +import github.daneren2005.dsub.fragments.SelectArtistFragment; +import github.daneren2005.dsub.fragments.SelectDirectoryFragment; +import github.daneren2005.dsub.fragments.SelectPlaylistFragment; +import github.daneren2005.dsub.fragments.SelectPodcastsFragment; +import github.daneren2005.dsub.fragments.SubsonicFragment; +import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.updates.Updater; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.SilentBackgroundTask; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.view.ChangeLog; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class MainActivity extends SubsonicActivity { + private static final String TAG = MainActivity.class.getSimpleName(); + private static boolean infoDialogDisplayed; + private ScheduledExecutorService executorService; + private View bottomBar; + private View coverArtView; + private TextView trackView; + private TextView artistView; + private ImageButton startButton; + private long lastBackPressTime = 0; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT)) { + stopService(new Intent(this, DownloadServiceImpl.class)); + finish(); + } else if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD)) { + getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD); + Intent intent = new Intent(); + intent.setClass(this, DownloadActivity.class); + startActivity(intent); + } + setContentView(R.layout.main); + loadSettings(); + + bottomBar = findViewById(R.id.bottom_bar); + bottomBar.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + Intent intent = new Intent(); + intent.setClass(v.getContext(), DownloadActivity.class); + startActivity(intent); + } + }); + coverArtView = bottomBar.findViewById(R.id.album_art); + trackView = (TextView) bottomBar.findViewById(R.id.track_name); + artistView = (TextView) bottomBar.findViewById(R.id.artist_name); + + ImageButton previousButton = (ImageButton) findViewById(R.id.download_previous); + previousButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(MainActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if(getDownloadService() == null) { + return null; + } + + getDownloadService().previous(); + return null; + } + + @Override + protected void done(Void result) { + update(); + } + }.execute(); + } + }); + + startButton = (ImageButton) findViewById(R.id.download_start); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(MainActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + PlayerState state = getDownloadService().getPlayerState(); + if(state == PlayerState.STARTED) { + getDownloadService().pause(); + } else { + getDownloadService().start(); + } + + return null; + } + + @Override + protected void done(Void result) { + update(); + } + }.execute(); + } + }); + + ImageButton nextButton = (ImageButton) findViewById(R.id.download_next); + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SilentBackgroundTask<Void>(MainActivity.this) { + @Override + protected Void doInBackground() throws Throwable { + if(getDownloadService() == null) { + return null; + } + + if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) { + getDownloadService().next(); + } + return null; + } + + @Override + protected void done(Void result) { + update(); + } + }.execute(); + } + }); + + viewPager = (ViewPager) findViewById(R.id.pager); + viewPager.setOffscreenPageLimit(4); + pagerAdapter = new TabPagerAdapter(this, viewPager); + viewPager.setAdapter(pagerAdapter); + viewPager.setOnPageChangeListener(pagerAdapter); + + addTab(R.string.button_bar_home, MainFragment.class, null); + addTab(R.string.button_bar_browse, SelectArtistFragment.class, null); + addTab(R.string.button_bar_playlists, SelectPlaylistFragment.class, null); + addTab(R.string.button_bar_podcasts, SelectPodcastsFragment.class, null); + SharedPreferences prefs = Util.getPreferences(this); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_CHAT_ENABLED, true)) { + addTab(R.string.button_bar_chat, ChatFragment.class, null); + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + getSupportActionBar().setHomeButtonEnabled(false); + getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + } + + @Override + protected void onPostCreate(Bundle bundle) { + super.onPostCreate(bundle); + + showInfoDialog(); + checkUpdates(); + + ChangeLog changeLog = new ChangeLog(this, Util.getPreferences(this)); + if(changeLog.isFirstRun()) { + changeLog.getLogDialog().show(); + } + } + + @Override + public void onResume() { + super.onResume(); + + final Handler handler = new Handler(); + Runnable runnable = new Runnable() { + @Override + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }; + + if(getIntent().hasExtra(Constants.INTENT_EXTRA_VIEW_ALBUM)) { + viewPager.setCurrentItem(1); + + int fragmentID = R.id.select_artist_layout; + if(getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID)) { + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID)); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_PARENT_NAME)); + fragment.setArguments(args); + + pagerAdapter.queueFragment(fragment, R.id.select_artist_layout); + fragmentID = fragment.getRootId(); + } + + SubsonicFragment fragment = new SelectDirectoryFragment(); + Bundle args = new Bundle(); + args.putString(Constants.INTENT_EXTRA_NAME_ID, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); + args.putString(Constants.INTENT_EXTRA_NAME_NAME, getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_NAME)); + fragment.setArguments(args); + + pagerAdapter.queueFragment(fragment, fragmentID); + getIntent().removeExtra(Constants.INTENT_EXTRA_VIEW_ALBUM); + } + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS); + } + + @Override + public void onPause() { + super.onPause(); + executorService.shutdown(); + } + + @Override + public void onBackPressed() { + if(onBackPressedSupport()) { + if(lastBackPressTime < (System.currentTimeMillis() - 4000)) { + lastBackPressTime = System.currentTimeMillis(); + Util.toast(this, R.string.main_back_confirm); + } else { + finish(); + } + } + } + + private void update() { + if (getDownloadService() == null) { + return; + } + + DownloadFile current = getDownloadService().getCurrentPlaying(); + if(current == null) { + 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[] {(getDownloadService().getPlayerState() == PlayerState.STARTED) ? R.attr.media_button_pause : R.attr.media_button_start}; + TypedArray typedArray = this.obtainStyledAttributes(attrs); + Drawable drawable = typedArray.getDrawable(0); + startButton.setImageDrawable(drawable); + typedArray.recycle(); + } + + public void checkUpdates() { + try { + String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + int ver = Integer.parseInt(version.replace(".", "")); + Updater updater = new Updater(ver); + updater.checkUpdates(this); + } + catch(Exception e) { + + } + } + + private void loadSettings() { + PreferenceManager.setDefaultValues(this, R.xml.settings, false); + SharedPreferences prefs = Util.getPreferences(this); + if (!prefs.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory().getPath()); + editor.commit(); + } + + if (!prefs.contains(Constants.PREFERENCES_KEY_OFFLINE)) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, false); + + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + 1, "Demo Server"); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + 1, "http://demo.subsonic.org"); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + 1, "android-guest"); + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + 1, "guest"); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + editor.commit(); + } + if(!prefs.contains(Constants.PREFERENCES_KEY_SERVER_COUNT)) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 3); + editor.commit(); + } + } + + private void showInfoDialog() { + if (!infoDialogDisplayed) { + infoDialogDisplayed = true; + Log.i(TAG, Util.getRestUrl(this, null)); + if (Util.getRestUrl(this, null).contains("demo.subsonic.org")) { + Util.info(this, R.string.main_welcome_title, R.string.main_welcome_text); + } + } + } +} diff --git a/src/github/daneren2005/dsub/activity/QueryReceiverActivity.java b/src/github/daneren2005/dsub/activity/QueryReceiverActivity.java new file mode 100644 index 00000000..15d0c6a6 --- /dev/null +++ b/src/github/daneren2005/dsub/activity/QueryReceiverActivity.java @@ -0,0 +1,56 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ + +package github.daneren2005.dsub.activity; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.provider.SearchRecentSuggestions; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.provider.DSubSearchProvider; + +/** + * Receives search queries and forwards to the SelectAlbumActivity. + * + * @author Sindre Mehus + */ +public class QueryReceiverActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String query = getIntent().getStringExtra(SearchManager.QUERY); + + if (query != null) { + SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, DSubSearchProvider.AUTHORITY, + DSubSearchProvider.MODE); + suggestions.saveRecentQuery(query, null); + + Intent intent = new Intent(QueryReceiverActivity.this, SearchActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query); + Util.startActivityWithoutTransition(QueryReceiverActivity.this, intent); + } + finish(); + Util.disablePendingTransition(this); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/activity/SearchActivity.java b/src/github/daneren2005/dsub/activity/SearchActivity.java new file mode 100644 index 00000000..aeddcf4f --- /dev/null +++ b/src/github/daneren2005/dsub/activity/SearchActivity.java @@ -0,0 +1,91 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ + +package github.daneren2005.dsub.activity; + +import github.daneren2005.dsub.R; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import github.daneren2005.dsub.fragments.SearchFragment; +import github.daneren2005.dsub.util.Constants; +import com.actionbarsherlock.view.MenuItem; + +public class SearchActivity extends SubsonicActivity { + private static final String TAG = SearchActivity.class.getSimpleName(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.download_activity); + + if (findViewById(R.id.download_container) != null && savedInstanceState == null) { + currentFragment = new SearchFragment(); + currentFragment.setPrimaryFragment(true); + getSupportFragmentManager().beginTransaction().add(R.id.download_container, currentFragment, currentFragment.getSupportTag() + "").commit(); + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + if(currentFragment != null && currentFragment instanceof SearchFragment) { + String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY); + boolean autoplay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false); + boolean requestsearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false); + + if (query != null) { + ((SearchFragment)currentFragment).search(query, autoplay); + } else { + ((SearchFragment)currentFragment).populateList(); + if (requestsearch) { + onSearchRequested(); + } + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(item.getItemId() == android.R.id.home) { + Intent i = new Intent(); + i.setClass(this, MainActivity.class); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + public void onSupportNewIntent(Intent intent) { + onNewIntent(intent); + } + + @Override + public void onBackPressed() { + if(onBackPressedSupport()) { + super.onBackPressed(); + } + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/activity/SettingsActivity.java b/src/github/daneren2005/dsub/activity/SettingsActivity.java new file mode 100644 index 00000000..fc56281e --- /dev/null +++ b/src/github/daneren2005/dsub/activity/SettingsActivity.java @@ -0,0 +1,539 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.activity; + +import 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.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; +import android.provider.SearchRecentSuggestions; +import android.text.InputType; +import android.util.Log; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.provider.DSubSearchProvider; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.LoadingTask; +import github.daneren2005.dsub.view.ErrorDialog; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.Util; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; + +public class SettingsActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = SettingsActivity.class.getSimpleName(); + private final Map<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 cacheSize; + private EditTextPreference cacheLocation; + private ListPreference preloadCountWifi; + private ListPreference preloadCountMobile; + private EditTextPreference randomSize; + private ListPreference tempLoss; + private EditTextPreference bufferLength; + private Preference addServerPreference; + private PreferenceCategory serversCategory; + private EditTextPreference chatRefreshRate; + private ListPreference videoPlayer; + + private int serverCount = 3; + private SharedPreferences settings; + + @Override + public void onCreate(Bundle savedInstanceState) { + applyTheme(); + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings); + + theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME); + maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI); + maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE); + 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); + cacheSize = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE); + 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); + randomSize = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_RANDOM_SIZE); + tempLoss = (ListPreference) findPreference(Constants.PREFERENCES_KEY_TEMP_LOSS); + bufferLength = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH); + addServerPreference = (Preference) findPreference(Constants.PREFERENCES_KEY_SERVER_ADD); + serversCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_SERVER_KEY); + chatRefreshRate = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH); + videoPlayer = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER); + + settings = Util.getPreferences(this); + serverCount = settings.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 3); + + findPreference("clearSearchHistory").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SearchRecentSuggestions suggestions = new SearchRecentSuggestions(SettingsActivity.this, DSubSearchProvider.AUTHORITY, DSubSearchProvider.MODE); + suggestions.clearHistory(); + Util.toast(SettingsActivity.this, R.string.settings_search_history_cleared); + return false; + } + }); + findPreference("clearCache").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Util.confirmDialog(SettingsActivity.this, R.string.common_delete, "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); + 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.removePreference(addServerPreference); + serversCategory.addPreference(addServer(serverCount)); + serversCategory.addPreference(addServerPreference); + + SharedPreferences.Editor editor = settings.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_COUNT, serverCount); + editor.commit(); + + serverSettings.put(instance, new ServerSettings(instance)); + + return true; + } + }); + + serversCategory.removePreference(addServerPreference); + for (int i = 1; i <= serverCount; i++) { + String instance = String.valueOf(i); + serversCategory.addPreference(addServer(i)); + serverSettings.put(instance, new ServerSettings(instance)); + } + serversCategory.addPreference(addServerPreference); + + SharedPreferences prefs = Util.getPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + + update(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + SharedPreferences prefs = Util.getPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + Log.d(TAG, "Preference changed: " + key); + update(); + + if (Constants.PREFERENCES_KEY_HIDE_MEDIA.equals(key)) { + setHideMedia(sharedPreferences.getBoolean(key, false)); + } + else if (Constants.PREFERENCES_KEY_MEDIA_BUTTONS.equals(key)) { + setMediaButtonsEnabled(sharedPreferences.getBoolean(key, true)); + } + else if (Constants.PREFERENCES_KEY_CACHE_LOCATION.equals(key)) { + setCacheLocation(sharedPreferences.getString(key, "")); + } + else if (Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION.equals(key)){ + DownloadService downloadService = DownloadServiceImpl.getInstance(); + downloadService.setSleepTimerDuration(Integer.parseInt(sharedPreferences.getString(key, "60"))); + } + + 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()); + cacheSize.setSummary(cacheSize.getText()); + cacheLocation.setSummary(cacheLocation.getText()); + preloadCountWifi.setSummary(preloadCountWifi.getEntry()); + preloadCountMobile.setSummary(preloadCountMobile.getEntry()); + randomSize.setSummary(randomSize.getText()); + tempLoss.setSummary(tempLoss.getEntry()); + bufferLength.setSummary(bufferLength.getText() + " seconds"); + chatRefreshRate.setSummary(chatRefreshRate.getText()); + videoPlayer.setSummary(videoPlayer.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); + + 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); + + if (serverUrlPreference.getText() == null) { + serverUrlPreference.setText("http://yourhost"); + } + + serverUrlPreference.setSummary(serverUrlPreference.getText()); + + screen.setSummary(serverUrlPreference.getText()); + + final EditTextPreference serverUsernamePreference = new EditTextPreference(this); + serverUsernamePreference.setKey(Constants.PREFERENCES_KEY_USERNAME + instance); + serverUsernamePreference.setTitle(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 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(serverUsernamePreference); + screen.addPreference(serverPasswordPreference); + screen.addPreference(serverRemoveServerPreference); + screen.addPreference(serverTestConnectionPreference); + screen.addPreference(serverOpenBrowser); + + return screen; + } + + private void applyTheme() { + String activeTheme = Util.getTheme(this); + if ("dark".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Dark); + } else if ("black".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Black); + } else if ("light".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Light); + } else if ("dark_fullscreen".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Dark_Fullscreen); + } else if ("black_fullscreen".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Black_Fullscreen); + } else if ("light_fullscreen".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Light_Fullscreen); + } else if("holo".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Holo); + } else if("holo_fullscreen".equals(activeTheme)) { + setTheme(R.style.Theme_DSub_Holo_Fullscreen); + }else { + setTheme(R.style.Theme_DSub_Holo); + } + } + + private void setHideMedia(boolean hide) { + File nomediaDir = new File(FileUtil.getSubsonicDirectory(), ".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); + } + } else if (nomediaDir.exists()) { + if (!nomediaDir.delete()) { + Log.w(TAG, "Failed to delete " + nomediaDir); + } + } + Util.toast(this, R.string.settings_hide_media_toast, false); + } + + private void setMediaButtonsEnabled(boolean enabled) { + if (enabled) { + Util.registerMediaButtonEventReceiver(this); + } else { + Util.unregisterMediaButtonEventReceiver(this); + } + } + + private void setCacheLocation(String path) { + File dir = new File(path); + if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) { + Util.toast(this, R.string.settings_cache_location_error, false); + + // Reset it to the default. + String defaultPath = FileUtil.getDefaultMusicDirectory().getPath(); + if (!defaultPath.equals(path)) { + SharedPreferences prefs = Util.getPreferences(this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultPath); + editor.commit(); + cacheLocation.setSummary(defaultPath); + cacheLocation.setText(defaultPath); + } + + // Clear download queue. + DownloadService downloadService = DownloadServiceImpl.getInstance(); + downloadService.clear(); + } + } + + private void testConnection(final int instance) { + 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 + protected void cancel() { + super.cancel(); + Util.setActiveServer(SettingsActivity.this, previousInstance); + } + + @Override + protected void error(Throwable error) { + Log.w(TAG, error.toString(), error); + new ErrorDialog(SettingsActivity.this, getResources().getString(R.string.settings_connection_failure) + + " " + getErrorMessage(error), false); + } + }; + task.execute(); + } + + private void openInBrowser(final int instance) { + SharedPreferences prefs = Util.getPreferences(this); + String url = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + 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 username; + private PreferenceScreen screen; + + private ServerSettings(String instance) { + + screen = (PreferenceScreen) findPreference("server" + instance); + serverName = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_NAME + instance); + serverUrl = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_SERVER_URL + instance); + username = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_USERNAME + instance); + + serverUrl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + try { + String url = (String) value; + new URL(url); + if (!url.equals(url.trim()) || url.contains("@") || url.contains("_")) { + throw new Exception(); + } + } catch (Exception x) { + new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_url, false); + return false; + } + return true; + } + }); + + username.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String username = (String) value; + if (username == null || !username.equals(username.trim())) { + new ErrorDialog(SettingsActivity.this, R.string.settings_invalid_username, false); + return false; + } + return true; + } + }); + } + + public void update() { + serverName.setSummary(serverName.getText()); + serverUrl.setSummary(serverUrl.getText()); + username.setSummary(username.getText()); + screen.setSummary(serverUrl.getText()); + screen.setTitle(serverName.getText()); + } + } +} diff --git a/src/github/daneren2005/dsub/activity/SubsonicActivity.java b/src/github/daneren2005/dsub/activity/SubsonicActivity.java new file mode 100644 index 00000000..d8158f7d --- /dev/null +++ b/src/github/daneren2005/dsub/activity/SubsonicActivity.java @@ -0,0 +1,641 @@ +package github.daneren2005.dsub.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.ActionBar.Tab;
+import com.actionbarsherlock.app.ActionBar.TabListener;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.app.SherlockFragment;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SubsonicActivity extends SherlockFragmentActivity implements OnItemSelectedListener {
+ private static final String TAG = SubsonicActivity.class.getSimpleName();
+ private static ImageLoader IMAGE_LOADER;
+ protected static String theme;
+ private boolean destroyed = false;
+ protected TabPagerAdapter pagerAdapter;
+ protected ViewPager viewPager;
+ protected List<SubsonicFragment> backStack = new ArrayList<SubsonicFragment>();
+ protected SubsonicFragment currentFragment;
+ Spinner actionBarSpinner;
+ ArrayAdapter<CharSequence> spinnerAdapter;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ setUncaughtExceptionHandler();
+ applyTheme();
+ super.onCreate(bundle);
+ startService(new Intent(this, DownloadServiceImpl.class));
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ View actionbar = getLayoutInflater().inflate(R.layout.actionbar_spinner, null);
+ actionBarSpinner = (Spinner)actionbar.findViewById(R.id.spinner);
+ spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ actionBarSpinner.setOnItemSelectedListener(this);
+ actionBarSpinner.setAdapter(spinnerAdapter);
+
+ getSupportActionBar().setCustomView(actionbar);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Util.registerMediaButtonEventReceiver(this);
+
+ // Make sure to update theme
+ if (theme != null && !theme.equals(Util.getTheme(this))) {
+ restart();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ destroyed = true;
+ getImageLoader().clear();
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ Util.disablePendingTransition(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ super.onSaveInstanceState(savedInstanceState);
+ if(viewPager == null) {
+ String[] ids = new String[backStack.size() + 1];
+ ids[0] = currentFragment.getTag();
+ int i = 1;
+ for(SubsonicFragment frag: backStack) {
+ ids[i] = frag.getTag();
+ i++;
+ }
+ savedInstanceState.putStringArray(Constants.MAIN_BACK_STACK, ids);
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_SIZE, backStack.size() + 1);
+ } else {
+ pagerAdapter.onSaveInstanceState(savedInstanceState);
+ }
+ }
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ if(viewPager == null) {
+ super.onRestoreInstanceState(savedInstanceState);
+ int size = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_SIZE);
+ String[] ids = savedInstanceState.getStringArray(Constants.MAIN_BACK_STACK);
+ FragmentManager fm = getSupportFragmentManager();
+ currentFragment = (SubsonicFragment)fm.findFragmentByTag(ids[0]);
+ currentFragment.setPrimaryFragment(true);
+ invalidateOptionsMenu();
+ for(int i = 1; i < size; i++) {
+ SubsonicFragment frag = (SubsonicFragment)fm.findFragmentByTag(ids[i]);
+ backStack.add(frag);
+ }
+ recreateSpinner();
+ } else {
+ pagerAdapter.onRestoreInstanceState(savedInstanceState);
+ super.onRestoreInstanceState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ com.actionbarsherlock.view.MenuInflater menuInflater = getSupportMenuInflater();
+ if(pagerAdapter != null) {
+ pagerAdapter.onCreateOptionsMenu(menu, menuInflater);
+ } else if(currentFragment != null) {
+ currentFragment.onCreateOptionsMenu(menu, menuInflater);
+ }
+ return true;
+ }
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(pagerAdapter != null) {
+ return pagerAdapter.onOptionsItemSelected(item);
+ } else if(currentFragment != null) {
+ return currentFragment.onOptionsItemSelected(item);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN;
+ boolean isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP;
+ boolean isVolumeAdjust = isVolumeDown || isVolumeUp;
+ boolean isJukebox = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+
+ if (isVolumeAdjust && isJukebox) {
+ getDownloadService().adjustJukeboxVolume(isVolumeUp);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+ if(pagerAdapter != null) {
+ pagerAdapter.recreateSpinner();
+ } else {
+ recreateSpinner();
+ }
+ }
+ public void setSubtitle(CharSequence title) {
+ getSupportActionBar().setSubtitle(title);
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int top = spinnerAdapter.getCount() - 1;
+ if(position < top) {
+ for(int i = top; i > position; i--) {
+ if(pagerAdapter != null) {
+ pagerAdapter.removeCurrent();
+ } else {
+ removeCurrent();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+
+ public boolean onBackPressedSupport() {
+ if(pagerAdapter != null) {
+ return pagerAdapter.onBackPressed();
+ } else {
+ if(backStack.size() > 0) {
+ removeCurrent();
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ public void replaceFragment(SubsonicFragment fragment, int id, int tag) {
+ if(pagerAdapter != null) {
+ pagerAdapter.replaceCurrent(fragment, id, tag);
+ } else {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ backStack.add(currentFragment);
+
+ currentFragment = fragment;
+ currentFragment.setPrimaryFragment(true);
+ invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.add(id, fragment, tag + "");
+ trans.commit();
+ recreateSpinner();
+ }
+ }
+ private void removeCurrent() {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ Fragment oldFrag = (Fragment)currentFragment;
+
+ currentFragment = (SubsonicFragment) backStack.remove(backStack.size() - 1);
+ currentFragment.setPrimaryFragment(true);
+ invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.remove(oldFrag);
+ trans.commit();
+ recreateSpinner();
+ }
+
+ private void recreateSpinner() {
+ if(backStack.size() > 0) {
+ spinnerAdapter.clear();
+ for(int i = 0; i < backStack.size(); i++) {
+ spinnerAdapter.add(backStack.get(i).getTitle());
+ }
+ spinnerAdapter.add(currentFragment.getTitle());
+ spinnerAdapter.notifyDataSetChanged();
+ actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1);
+ getSupportActionBar().setDisplayShowCustomEnabled(true);
+ } else {
+ getSupportActionBar().setDisplayShowCustomEnabled(false);
+ }
+ }
+
+ protected void addTab(int titleRes, Class fragmentClass, Bundle args) {
+ pagerAdapter.addTab(getString(titleRes), fragmentClass, args);
+ }
+ protected void addTab(CharSequence title, Class fragmentClass, Bundle args) {
+ pagerAdapter.addTab(title, fragmentClass, args);
+ }
+
+ protected void restart() {
+ Intent intent = new Intent(this, this.getClass());
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtras(getIntent());
+ Util.startActivityWithoutTransition(this, intent);
+ }
+
+ private void applyTheme() {
+ theme = Util.getTheme(this);
+ if ("dark".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Dark);
+ } else if ("black".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Black);
+ } else if ("light".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Light);
+ } else if ("dark_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Dark_Fullscreen);
+ } else if ("black_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Black_Fullscreen);
+ } else if ("light_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Light_Fullscreen);
+ } else if("holo".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Holo);
+ } else if("holo_fullscreen".equals(theme)) {
+ setTheme(R.style.Theme_DSub_Holo_Fullscreen);
+ }else {
+ setTheme(R.style.Theme_DSub_Holo);
+ }
+ }
+
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ public synchronized ImageLoader getImageLoader() {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(this);
+ }
+ return IMAGE_LOADER;
+ }
+ public synchronized static ImageLoader getStaticImageLoader(Context context) {
+ if (IMAGE_LOADER == null) {
+ IMAGE_LOADER = new ImageLoader(context);
+ }
+ return IMAGE_LOADER;
+ }
+
+ public DownloadService getDownloadService() {
+ // If service is not available, request it to start and wait for it.
+ for (int i = 0; i < 5; i++) {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService != null) {
+ return downloadService;
+ }
+ Log.w(TAG, "DownloadService not running. Attempting to start it.");
+ startService(new Intent(this, DownloadServiceImpl.class));
+ Util.sleepQuietly(50L);
+ }
+ return DownloadServiceImpl.getInstance();
+ }
+
+ public ViewPager getViewPager() {
+ return viewPager;
+ }
+ public TabPagerAdapter getPagerAdapter() {
+ return pagerAdapter;
+ }
+
+ public static String getThemeName() {
+ return theme;
+ }
+
+ private void setUncaughtExceptionHandler() {
+ Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
+ if (!(handler instanceof SubsonicActivity.SubsonicUncaughtExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new SubsonicActivity.SubsonicUncaughtExceptionHandler(this));
+ }
+ }
+
+ /**
+ * Logs the stack trace of uncaught exceptions to a file on the SD card.
+ */
+ private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private final Thread.UncaughtExceptionHandler defaultHandler;
+ private final Context context;
+
+ private SubsonicUncaughtExceptionHandler(Context context) {
+ this.context = context;
+ defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ File file = null;
+ PrintWriter printWriter = null;
+ try {
+
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.daneren2005.dsub", 0);
+ file = new File(Environment.getExternalStorageDirectory(), "subsonic-stacktrace.txt");
+ printWriter = new PrintWriter(file);
+ printWriter.println("Android API level: " + Build.VERSION.SDK);
+ printWriter.println("Subsonic version name: " + packageInfo.versionName);
+ printWriter.println("Subsonic version code: " + packageInfo.versionCode);
+ printWriter.println();
+ throwable.printStackTrace(printWriter);
+ Log.i(TAG, "Stack trace written to " + file);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to write stack trace to " + file, x);
+ } finally {
+ Util.close(printWriter);
+ if (defaultHandler != null) {
+ defaultHandler.uncaughtException(thread, throwable);
+ }
+
+ }
+ }
+ }
+
+ public class TabPagerAdapter extends FragmentPagerAdapter implements TabListener, ViewPager.OnPageChangeListener {
+ private SherlockFragmentActivity activity;
+ private ViewPager pager;
+ private ActionBar actionBar;
+ private SubsonicFragment currentFragment;
+ private List<TabInfo> tabs = new ArrayList<TabInfo>();
+ private List<List<SubsonicFragment>> frags = new ArrayList<List<SubsonicFragment>>();
+ private List<QueuedFragment> queue = new ArrayList<QueuedFragment>();
+ private int currentPosition;
+
+ public TabPagerAdapter(SherlockFragmentActivity activity, ViewPager pager) {
+ super(activity.getSupportFragmentManager());
+ this.activity = activity;
+ this.actionBar = activity.getSupportActionBar();
+ this.pager = pager;
+ this.currentPosition = 0;
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+ final TabInfo tabInfo = tabs.get(i);
+ SubsonicFragment frag = (SubsonicFragment) Fragment.instantiate(activity, tabInfo.fragmentClass.getName(), tabInfo.args);
+ List<SubsonicFragment> fragStack = new ArrayList<SubsonicFragment>();
+ fragStack.add(frag);
+ while(i > frags.size()) {
+ frags.add(null);
+ }
+ if(i == frags.size()) {
+ frags.add(i, fragStack);
+ } else {
+ frags.set(i, fragStack);
+ }
+ if(currentFragment == null || currentPosition == i) {
+ currentFragment = frag;
+ currentFragment.setPrimaryFragment(true);
+ }
+ return frag;
+ }
+
+ @Override
+ public int getCount() {
+ return tabs.size();
+ }
+
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ if(currentFragment != null) {
+ currentFragment.onCreateOptionsMenu(menu, menuInflater);
+
+ for(QueuedFragment addFragment: queue) {
+ replaceFragment(addFragment.fragment, addFragment.id, currentFragment.getSupportTag());
+ currentFragment = addFragment.fragment;
+ }
+ currentFragment.setPrimaryFragment(true);
+ queue.clear();
+ }
+ }
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(currentFragment != null) {
+ return currentFragment.onOptionsItemSelected(item);
+ } else {
+ return false;
+ }
+ }
+
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ TabInfo tabInfo = (TabInfo) tab.getTag();
+ for (int i = 0; i < tabs.size(); i++) {
+ if ( tabs.get(i) == tabInfo ) {
+ pager.setCurrentItem(i);
+ break;
+ }
+ }
+ }
+
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {}
+
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {}
+
+ public void onPageScrollStateChanged(int arg0) {}
+
+ public void onPageScrolled(int arg0, float arg1, int arg2) {}
+
+ public void onPageSelected(int position) {
+ currentPosition = position;
+ actionBar.setSelectedNavigationItem(position);
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ if(position <= frags.size()) {
+ List<SubsonicFragment> fragStack = frags.get(position);
+ currentFragment = fragStack.get(fragStack.size() - 1);
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(true);
+ }
+ activity.invalidateOptionsMenu();
+ recreateSpinner();
+ }
+ }
+
+ public void addTab(CharSequence title, Class fragmentClass, Bundle args) {
+ final TabInfo tabInfo = new TabInfo(fragmentClass, args);
+
+ Tab tab = actionBar.newTab();
+ tab.setText(title);
+ tab.setTabListener(this);
+ tab.setTag(tabInfo);
+
+ tabs.add(tabInfo);
+
+ actionBar.addTab(tab);
+ notifyDataSetChanged();
+ }
+ public void queueFragment(SubsonicFragment fragment, int id) {
+ QueuedFragment frag = new QueuedFragment();
+ frag.fragment = fragment;
+ frag.id = id;
+ queue.add(frag);
+ }
+ public void replaceCurrent(SubsonicFragment fragment, int id, int tag) {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ fragStack.add(fragment);
+
+ currentFragment = fragment;
+ currentFragment.setPrimaryFragment(true);
+ activity.invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.add(id, fragment, tag + "");
+ trans.commit();
+ recreateSpinner();
+ }
+
+ public void removeCurrent() {
+ if(currentFragment != null) {
+ currentFragment.setPrimaryFragment(false);
+ }
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ Fragment oldFrag = (Fragment)fragStack.remove(fragStack.size() - 1);
+
+ currentFragment = fragStack.get(fragStack.size() - 1);
+ currentFragment.setPrimaryFragment(true);
+ activity.invalidateOptionsMenu();
+
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ trans.remove(oldFrag);
+ trans.commit();
+ }
+
+ public boolean onBackPressed() {
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ if(fragStack.size() > 1) {
+ removeCurrent();
+ recreateSpinner();
+ return false;
+ } else {
+ if(currentPosition == 0) {
+ return true;
+ } else {
+ viewPager.setCurrentItem(0);
+ return false;
+ }
+ }
+ }
+
+ private void recreateSpinner() {
+ if(frags.isEmpty()) {
+ return;
+ }
+
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ if(fragStack.size() > 1) {
+ spinnerAdapter.clear();
+ for(int i = 0; i < fragStack.size(); i++) {
+ SubsonicFragment frag = fragStack.get(i);
+ spinnerAdapter.add(frag.getTitle());
+ }
+ spinnerAdapter.notifyDataSetChanged();
+ actionBarSpinner.setSelection(spinnerAdapter.getCount() - 1);
+ actionBar.setDisplayShowCustomEnabled(true);
+ } else {
+ actionBar.setDisplayShowCustomEnabled(false);
+ }
+ }
+
+ public void invalidate() {
+ FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+ for (int i = 0; i < frags.size(); i++) {
+ List<SubsonicFragment> fragStack = frags.get(i);
+
+ for(int j = fragStack.size() - 1; j > 0; j--) {
+ SubsonicFragment oldFrag = fragStack.remove(j);
+ trans.remove((Fragment)oldFrag);
+ }
+
+ SubsonicFragment frag = (SubsonicFragment)fragStack.get(0);
+ frag.invalidate();
+ }
+ trans.commit();
+ }
+
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ for(int i = 0; i < frags.size(); i++) {
+ List<SubsonicFragment> fragStack = frags.get(i);
+ String[] ids = new String[fragStack.size()];
+
+ for(int j = 0; j < fragStack.size(); j++) {
+ ids[j] = fragStack.get(j).getTag();
+ }
+ savedInstanceState.putStringArray(Constants.MAIN_BACK_STACK + i, ids);
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_SIZE + i, fragStack.size());
+ }
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_TABS, frags.size());
+ savedInstanceState.putInt(Constants.MAIN_BACK_STACK_POSITION, currentPosition);
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ int tabCount = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_TABS);
+ FragmentManager fm = activity.getSupportFragmentManager();
+ for(int i = 0; i < tabCount; i++) {
+ int stackSize = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_SIZE + i);
+ String[] ids = savedInstanceState.getStringArray(Constants.MAIN_BACK_STACK + i);
+ List<SubsonicFragment> fragStack = new ArrayList<SubsonicFragment>();
+
+ for(int j = 0; j < stackSize; j++) {
+ SubsonicFragment frag = (SubsonicFragment)fm.findFragmentByTag(ids[j]);
+ fragStack.add(frag);
+ }
+
+ frags.add(i, fragStack);
+ }
+ currentPosition = savedInstanceState.getInt(Constants.MAIN_BACK_STACK_POSITION);
+ List<SubsonicFragment> fragStack = frags.get(currentPosition);
+ currentFragment = fragStack.get(fragStack.size() - 1);
+ currentFragment.setPrimaryFragment(true);
+ activity.invalidateOptionsMenu();
+ }
+
+ private class TabInfo {
+ public final Class fragmentClass;
+ public final Bundle args;
+ public TabInfo(Class fragmentClass, Bundle args) {
+ this.fragmentClass = fragmentClass;
+ this.args = args;
+ }
+ }
+ private class QueuedFragment {
+ public SubsonicFragment fragment;
+ public int id;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java b/src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java new file mode 100644 index 00000000..5cda9ee5 --- /dev/null +++ b/src/github/daneren2005/dsub/activity/VoiceQueryReceiverActivity.java @@ -0,0 +1,59 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ + +package github.daneren2005.dsub.activity; + +import android.app.Activity; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.provider.SearchRecentSuggestions; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.provider.DSubSearchProvider; + +/** + * Receives voice search queries and forwards to the SearchActivity. + * + * http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html + * + * @author Sindre Mehus + */ +public class VoiceQueryReceiverActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String query = getIntent().getStringExtra(SearchManager.QUERY); + + if (query != null) { + SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, DSubSearchProvider.AUTHORITY, + DSubSearchProvider.MODE); + suggestions.saveRecentQuery(query, null); + + Intent intent = new Intent(VoiceQueryReceiverActivity.this, SearchActivity.class); + intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query); + intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true); + Util.startActivityWithoutTransition(VoiceQueryReceiverActivity.this, intent); + } + finish(); + Util.disablePendingTransition(this); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/audiofx/EqualizerController.java b/src/github/daneren2005/dsub/audiofx/EqualizerController.java new file mode 100644 index 00000000..0dcee863 --- /dev/null +++ b/src/github/daneren2005/dsub/audiofx/EqualizerController.java @@ -0,0 +1,151 @@ +/* + 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 2011 (C) Sindre Mehus + */ +package github.daneren2005.dsub.audiofx; + +import java.io.Serializable; + +import android.content.Context; +import android.media.MediaPlayer; +import android.media.audiofx.Equalizer; +import android.util.Log; +import github.daneren2005.dsub.util.FileUtil; + +/** + * Backward-compatible wrapper for {@link Equalizer}, which is API Level 9. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class EqualizerController { + + private static final String TAG = EqualizerController.class.getSimpleName(); + + private final Context context; + private Equalizer equalizer; + private boolean released = false; + private int audioSessionId = 0; + + // Class initialization fails when this throws an exception. + static { + try { + Class.forName("android.media.audiofx.Equalizer"); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Throws an exception if the {@link Equalizer} class is not available. + */ + public static void checkAvailable() throws Throwable { + // Calling here forces class initialization. + } + + public EqualizerController(Context context, MediaPlayer mediaPlayer) { + this.context = context; + try { + audioSessionId = mediaPlayer.getAudioSessionId(); + equalizer = new Equalizer(0, audioSessionId); + } catch (Throwable x) { + Log.w(TAG, "Failed to create equalizer.", x); + } + } + + public void saveSettings() { + try { + if (isAvailable()) { + FileUtil.serialize(context, new EqualizerSettings(equalizer), "equalizer.dat"); + } + } catch (Throwable x) { + Log.w(TAG, "Failed to save equalizer settings.", x); + } + } + + public void loadSettings() { + try { + if (isAvailable()) { + EqualizerSettings settings = FileUtil.deserialize(context, "equalizer.dat"); + if (settings != null) { + settings.apply(equalizer); + } + } + } catch (Throwable x) { + Log.w(TAG, "Failed to load equalizer settings.", x); + } + } + + public boolean isAvailable() { + return equalizer != null; + } + + public boolean isEnabled() { + return isAvailable() && equalizer.getEnabled(); + } + + public void release() { + if (isAvailable()) { + released = true; + equalizer.release(); + } + } + + public Equalizer getEqualizer() { + if(released) { + released = false; + try { + equalizer = new Equalizer(0, audioSessionId); + } catch (Throwable x) { + equalizer = null; + Log.w(TAG, "Failed to create equalizer.", x); + } + } + return equalizer; + } + + private static class EqualizerSettings implements Serializable { + + private final short[] bandLevels; + private short preset; + private final boolean enabled; + + public EqualizerSettings(Equalizer equalizer) { + enabled = equalizer.getEnabled(); + bandLevels = new short[equalizer.getNumberOfBands()]; + for (short i = 0; i < equalizer.getNumberOfBands(); i++) { + bandLevels[i] = equalizer.getBandLevel(i); + } + try { + preset = equalizer.getCurrentPreset(); + } catch (Exception x) { + preset = -1; + } + } + + public void apply(Equalizer equalizer) { + for (short i = 0; i < bandLevels.length; i++) { + equalizer.setBandLevel(i, bandLevels[i]); + } + if (preset >= 0 && preset < equalizer.getNumberOfPresets()) { + equalizer.usePreset(preset); + } + equalizer.setEnabled(enabled); + } + } +} + diff --git a/src/github/daneren2005/dsub/audiofx/VisualizerController.java b/src/github/daneren2005/dsub/audiofx/VisualizerController.java new file mode 100644 index 00000000..b32245f4 --- /dev/null +++ b/src/github/daneren2005/dsub/audiofx/VisualizerController.java @@ -0,0 +1,104 @@ +/* + 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 2011 (C) Sindre Mehus + */ +package github.daneren2005.dsub.audiofx; + +import android.content.Context; +import android.media.MediaPlayer; +import android.media.audiofx.Visualizer; +import android.util.Log; + +/** + * Backward-compatible wrapper for {@link Visualizer}, which is API Level 9. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class VisualizerController { + + private static final String TAG = VisualizerController.class.getSimpleName(); + private static final int PREFERRED_CAPTURE_SIZE = 128; // Must be a power of two. + + private final Context context; + private Visualizer visualizer; + private boolean released = false; + private int audioSessionId = 0; + + // Class initialization fails when this throws an exception. + static { + try { + Class.forName("android.media.audiofx.Visualizer"); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Throws an exception if the {@link Visualizer} class is not available. + */ + public static void checkAvailable() throws Throwable { + // Calling here forces class initialization. + } + + public VisualizerController(Context context, MediaPlayer mediaPlayer) { + this.context = context; + try { + audioSessionId = mediaPlayer.getAudioSessionId(); + visualizer = new Visualizer(audioSessionId); + } catch (Throwable x) { + Log.w(TAG, "Failed to create visualizer.", x); + } + + if (visualizer != null) { + int[] captureSizeRange = Visualizer.getCaptureSizeRange(); + int captureSize = Math.max(PREFERRED_CAPTURE_SIZE, captureSizeRange[0]); + captureSize = Math.min(captureSize, captureSizeRange[1]); + visualizer.setCaptureSize(captureSize); + } + } + + public boolean isAvailable() { + return visualizer != null; + } + + public boolean isEnabled() { + return isAvailable() && visualizer.getEnabled(); + } + + public void release() { + if (isAvailable()) { + visualizer.release(); + released = true; + } + } + + public Visualizer getVisualizer() { + if(released) { + released = false; + try { + visualizer = new Visualizer(audioSessionId); + } catch (Throwable x) { + visualizer = null; + Log.w(TAG, "Failed to create visualizer.", x); + } + } + + return visualizer; + } +} + diff --git a/src/github/daneren2005/dsub/domain/Artist.java b/src/github/daneren2005/dsub/domain/Artist.java new file mode 100644 index 00000000..e4a9001b --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Artist.java @@ -0,0 +1,78 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Artist implements Serializable { + + private String id; + private String name; + private String index; + private boolean starred; + private int closeness; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public boolean isStarred() { + return starred; + } + + public void setStarred(boolean starred) { + this.starred = starred; + } + + public int getCloseness() { + return closeness; + } + + public void setCloseness(int closeness) { + this.closeness = closeness; + } + + @Override + public String toString() { + return name; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/domain/ChatMessage.java b/src/github/daneren2005/dsub/domain/ChatMessage.java new file mode 100644 index 00000000..471594e9 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/ChatMessage.java @@ -0,0 +1,51 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+public class ChatMessage implements Serializable {
+ private String username;
+ private Long time;
+ private String message;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Long getTime() {
+ return time;
+ }
+
+ public void setTime(Long time) {
+ this.time = time;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Genre.java b/src/github/daneren2005/dsub/domain/Genre.java new file mode 100644 index 00000000..8c705e31 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Genre.java @@ -0,0 +1,29 @@ +package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+public class Genre implements Serializable {
+ private String name;
+ private String index;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/Indexes.java b/src/github/daneren2005/dsub/domain/Indexes.java new file mode 100644 index 00000000..0bc44158 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Indexes.java @@ -0,0 +1,50 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import java.util.List; +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Indexes implements Serializable { + + private final long lastModified; + private final List<Artist> shortcuts; + private final List<Artist> artists; + + public Indexes(long lastModified, List<Artist> shortcuts, List<Artist> artists) { + this.lastModified = lastModified; + this.shortcuts = shortcuts; + this.artists = artists; + } + + public long getLastModified() { + return lastModified; + } + + public List<Artist> getShortcuts() { + return shortcuts; + } + + public List<Artist> getArtists() { + return artists; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/domain/JukeboxStatus.java b/src/github/daneren2005/dsub/domain/JukeboxStatus.java new file mode 100644 index 00000000..7b229f49 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/JukeboxStatus.java @@ -0,0 +1,63 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class JukeboxStatus { + + private Integer positionSeconds; + private Integer currentPlayingIndex; + private Float gain; + private boolean playing; + + public Integer getPositionSeconds() { + return positionSeconds; + } + + public void setPositionSeconds(Integer positionSeconds) { + this.positionSeconds = positionSeconds; + } + + public Integer getCurrentPlayingIndex() { + return currentPlayingIndex; + } + + public void setCurrentIndex(Integer currentPlayingIndex) { + this.currentPlayingIndex = currentPlayingIndex; + } + + public boolean isPlaying() { + return playing; + } + + public void setPlaying(boolean playing) { + this.playing = playing; + } + + public Float getGain() { + return gain; + } + + public void setGain(float gain) { + this.gain = gain; + } +} diff --git a/src/github/daneren2005/dsub/domain/Lyrics.java b/src/github/daneren2005/dsub/domain/Lyrics.java new file mode 100644 index 00000000..feb75cd6 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Lyrics.java @@ -0,0 +1,55 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +/** + * Song lyrics. + * + * @author Sindre Mehus + */ +public class Lyrics { + + private String artist; + private String title; + private String text; + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/src/github/daneren2005/dsub/domain/MusicDirectory.java b/src/github/daneren2005/dsub/domain/MusicDirectory.java new file mode 100644 index 00000000..bb49378a --- /dev/null +++ b/src/github/daneren2005/dsub/domain/MusicDirectory.java @@ -0,0 +1,374 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import android.util.Log; +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; + +/** + * @author Sindre Mehus + */ +public class MusicDirectory { + private static final String TAG = MusicDirectory.class.getSimpleName(); + + private String name; + private String id; + private String parent; + private List<Entry> children = new ArrayList<Entry>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public void addChild(Entry child) { + children.add(child); + } + + public List<Entry> getChildren() { + return getChildren(true, true); + } + + public List<Entry> getChildren(boolean includeDirs, boolean includeFiles) { + if (includeDirs && includeFiles) { + return children; + } + + List<Entry> result = new ArrayList<Entry>(children.size()); + for (Entry child : children) { + if (child.isDirectory() && includeDirs || !child.isDirectory() && includeFiles) { + result.add(child); + } + } + return result; + } + + public int getChildrenSize() { + return children.size(); + } + + public void sortChildren() { + EntryComparator.sort(children); + } + + public static class Entry implements Serializable { + private String id; + private String parent; + private String grandParent; + private boolean directory; + private String title; + private String album; + private String artist; + private Integer track; + private Integer year; + private String genre; + private String contentType; + private String suffix; + private String transcodedContentType; + private String transcodedSuffix; + private String coverArt; + private Long size; + private Integer duration; + private Integer bitRate; + private String path; + private boolean video; + private Integer discNumber; + private boolean starred; + private int closeness; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public String getGrandParent() { + return grandParent; + } + + public void setGrandParent(String grandParent) { + this.grandParent = grandParent; + } + + public boolean isDirectory() { + return directory; + } + + public void setDirectory(boolean directory) { + this.directory = directory; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAlbum() { + return album; + } + + public void setAlbum(String album) { + this.album = album; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public Integer getTrack() { + return track; + } + + public void setTrack(Integer track) { + this.track = track; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public String getTranscodedContentType() { + return transcodedContentType; + } + + public void setTranscodedContentType(String transcodedContentType) { + this.transcodedContentType = transcodedContentType; + } + + public String getTranscodedSuffix() { + return transcodedSuffix; + } + + public void setTranscodedSuffix(String transcodedSuffix) { + this.transcodedSuffix = transcodedSuffix; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public Integer getDuration() { + return duration; + } + + public void setDuration(Integer duration) { + this.duration = duration; + } + + public Integer getBitRate() { + return bitRate; + } + + public void setBitRate(Integer bitRate) { + this.bitRate = bitRate; + } + + public String getCoverArt() { + return coverArt; + } + + public void setCoverArt(String coverArt) { + this.coverArt = coverArt; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isVideo() { + return video; + } + + public void setVideo(boolean video) { + this.video = video; + } + + public Integer getDiscNumber() { + return discNumber; + } + + public void setDiscNumber(Integer discNumber) { + this.discNumber = discNumber; + } + + public boolean isStarred() { + return starred; + } + + public void setStarred(boolean starred) { + this.starred = starred; + } + + public int getCloseness() { + return closeness; + } + + public void setCloseness(int closeness) { + this.closeness = closeness; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Entry entry = (Entry) o; + return id.equals(entry.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return title; + } + } + + public static class EntryComparator implements Comparator<Entry> { + public int compare(Entry lhs, Entry rhs) { + if(lhs.isDirectory() && !rhs.isDirectory()) { + return -1; + } else if(!lhs.isDirectory() && rhs.isDirectory()) { + return 1; + } + + Integer lhsDisc = lhs.getDiscNumber(); + Integer rhsDisc = rhs.getDiscNumber(); + + if(lhsDisc != null && rhsDisc != null) { + if(lhsDisc < rhsDisc) { + return -1; + } else if(lhsDisc > rhsDisc) { + return 1; + } + } else if(lhsDisc != null) { + return -1; + } else if(rhsDisc != null) { + return 1; + } + + Integer lhsTrack = lhs.getTrack(); + Integer rhsTrack = rhs.getTrack(); + if(lhsTrack != null && rhsTrack != null) { + if(lhsTrack < rhsTrack) { + return -1; + } else if(lhsTrack > rhsTrack) { + return 1; + } + } else if(lhsTrack != null) { + return -1; + } else if(rhsTrack != null) { + return 1; + } + + return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + } + + public static void sort(List<Entry> entries) { + try { + Collections.sort(entries, new EntryComparator()); + } catch (Exception e) { + Log.w(TAG, "Failed to sort MusicDirectory"); + } + } + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/domain/MusicFolder.java b/src/github/daneren2005/dsub/domain/MusicFolder.java new file mode 100644 index 00000000..68a22bcc --- /dev/null +++ b/src/github/daneren2005/dsub/domain/MusicFolder.java @@ -0,0 +1,46 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import java.io.Serializable; + +/** + * Represents a top level directory in which music or other media is stored. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class MusicFolder implements Serializable { + + private final String id; + private final String name; + + public MusicFolder(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/github/daneren2005/dsub/domain/PlayerState.java b/src/github/daneren2005/dsub/domain/PlayerState.java new file mode 100644 index 00000000..2b63077b --- /dev/null +++ b/src/github/daneren2005/dsub/domain/PlayerState.java @@ -0,0 +1,46 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import android.media.RemoteControlClient; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public enum PlayerState { + IDLE(RemoteControlClient.PLAYSTATE_STOPPED), + DOWNLOADING(RemoteControlClient.PLAYSTATE_BUFFERING), + PREPARING(RemoteControlClient.PLAYSTATE_BUFFERING), + PREPARED(RemoteControlClient.PLAYSTATE_STOPPED), + STARTED(RemoteControlClient.PLAYSTATE_PLAYING), + STOPPED(RemoteControlClient.PLAYSTATE_STOPPED), + PAUSED(RemoteControlClient.PLAYSTATE_PAUSED), + COMPLETED(RemoteControlClient.PLAYSTATE_STOPPED); + + private final int mRemoteControlClientPlayState; + + private PlayerState(int playState) { + mRemoteControlClientPlayState = playState; + } + + public int getRemoteControlClientPlayState() { + return mRemoteControlClientPlayState; + } +} diff --git a/src/github/daneren2005/dsub/domain/Playlist.java b/src/github/daneren2005/dsub/domain/Playlist.java new file mode 100644 index 00000000..c97659c7 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Playlist.java @@ -0,0 +1,109 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Playlist implements Serializable { + + private String id; + private String name; + private String owner; + private String comment; + private String songCount; + private String created; + private Boolean pub; + + public Playlist(String id, String name) { + this.id = id; + this.name = name; + } + public Playlist(String id, String name, String owner, String comment, String songCount, String created, String pub) { + this.id = id; + this.name = name; + this.owner = (owner == null) ? "" : owner; + this.comment = (comment == null) ? "" : comment; + this.songCount = (songCount == null) ? "" : songCount; + this.created = (created == null) ? "" : created; + this.pub = (pub == null) ? null : (pub.equals("true")); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getComment() { + return this.comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String getSongCount() { + return this.songCount; + } + + public void setSongCount(String songCount) { + this.songCount = songCount; + } + + public String getCreated() { + return this.created; + } + + public void setCreated(String created) { + this.created = created; + } + + public Boolean getPublic() { + return this.pub; + } + public void setPublic(Boolean pub) { + this.pub = pub; + } + + @Override + public String toString() { + return name; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/domain/PodcastChannel.java b/src/github/daneren2005/dsub/domain/PodcastChannel.java new file mode 100644 index 00000000..a39e8d04 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/PodcastChannel.java @@ -0,0 +1,80 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import java.io.Serializable;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastChannel implements Serializable {
+ private String id;
+ private String name;
+ private String url;
+ private String description;
+ private String status;
+ private String errorMessage;
+
+ public PodcastChannel() {
+
+ }
+
+ public String getId() {
+ return id;
+ }
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/PodcastEpisode.java b/src/github/daneren2005/dsub/domain/PodcastEpisode.java new file mode 100644 index 00000000..01821072 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/PodcastEpisode.java @@ -0,0 +1,54 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastEpisode extends MusicDirectory.Entry {
+ private String episodeId;
+ private String date;
+ private String status;
+
+ public PodcastEpisode() {
+ setDirectory(false);
+ }
+
+ public String getEpisodeId() {
+ return episodeId;
+ }
+ public void setEpisodeId(String episodeId) {
+ this.episodeId = episodeId;
+ }
+
+ public String getDate() {
+ return date;
+ }
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ public void setStatus(String status) {
+ this.status = status;
+ }
+}
diff --git a/src/github/daneren2005/dsub/domain/RepeatMode.java b/src/github/daneren2005/dsub/domain/RepeatMode.java new file mode 100644 index 00000000..7139029c --- /dev/null +++ b/src/github/daneren2005/dsub/domain/RepeatMode.java @@ -0,0 +1,28 @@ +package github.daneren2005.dsub.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public enum RepeatMode { + OFF { + @Override + public RepeatMode next() { + return ALL; + } + }, + ALL { + @Override + public RepeatMode next() { + return SINGLE; + } + }, + SINGLE { + @Override + public RepeatMode next() { + return OFF; + } + }; + + public abstract RepeatMode next(); +} diff --git a/src/github/daneren2005/dsub/domain/SearchCritera.java b/src/github/daneren2005/dsub/domain/SearchCritera.java new file mode 100644 index 00000000..20d46aa0 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/SearchCritera.java @@ -0,0 +1,55 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +/** + * The criteria for a music search. + * + * @author Sindre Mehus + */ +public class SearchCritera { + + private final String query; + private final int artistCount; + private final int albumCount; + private final int songCount; + + public SearchCritera(String query, int artistCount, int albumCount, int songCount) { + this.query = query; + this.artistCount = artistCount; + this.albumCount = albumCount; + this.songCount = songCount; + } + + public String getQuery() { + return query; + } + + public int getArtistCount() { + return artistCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public int getSongCount() { + return songCount; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/domain/SearchResult.java b/src/github/daneren2005/dsub/domain/SearchResult.java new file mode 100644 index 00000000..11a56540 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/SearchResult.java @@ -0,0 +1,51 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +import java.util.List; + +/** + * The result of a search. Contains matching artists, albums and songs. + * + * @author Sindre Mehus + */ +public class SearchResult { + + private final List<Artist> artists; + private final List<MusicDirectory.Entry> albums; + private final List<MusicDirectory.Entry> songs; + + public SearchResult(List<Artist> artists, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) { + this.artists = artists; + this.albums = albums; + this.songs = songs; + } + + public List<Artist> getArtists() { + return artists; + } + + public List<MusicDirectory.Entry> getAlbums() { + return albums; + } + + public List<MusicDirectory.Entry> getSongs() { + return songs; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/domain/ServerInfo.java b/src/github/daneren2005/dsub/domain/ServerInfo.java new file mode 100644 index 00000000..43c7319a --- /dev/null +++ b/src/github/daneren2005/dsub/domain/ServerInfo.java @@ -0,0 +1,46 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +/** + * Information about the Subsonic server. + * + * @author Sindre Mehus + */ +public class ServerInfo { + + private boolean isLicenseValid; + private Version restVersion; + + public boolean isLicenseValid() { + return isLicenseValid; + } + + public void setLicenseValid(boolean licenseValid) { + isLicenseValid = licenseValid; + } + + public Version getRestVersion() { + return restVersion; + } + + public void setRestVersion(Version restVersion) { + this.restVersion = restVersion; + } +} diff --git a/src/github/daneren2005/dsub/domain/Share.java b/src/github/daneren2005/dsub/domain/Share.java new file mode 100644 index 00000000..d19496f9 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Share.java @@ -0,0 +1,140 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.domain;
+
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import java.io.Serializable;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class Share implements Serializable {
+ private String id;
+ private String url;
+ private String description;
+ private String username;
+ private Date created;
+ private Date lastVisited;
+ private Date expires;
+ private Long visitCount;
+ private List<Entry> entries;
+
+ public Share() {
+ entries = new ArrayList<Entry>();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(String created) {
+ if (created != null) {
+ try {
+ this.created = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(created);
+ } catch (ParseException e) {
+ this.created = null;
+ }
+ } else {
+ this.created = null;
+ }
+ }
+
+ public Date getLastVisited() {
+ return lastVisited;
+ }
+
+ public void setLastVisited(String lastVisited) {
+ if (lastVisited != null) {
+ try {
+ this.lastVisited = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(lastVisited);
+ } catch (ParseException e) {
+ this.lastVisited = null;
+ }
+ } else {
+ this.lastVisited = null;
+ }
+ }
+
+ public Date getExpires() {
+ return expires;
+ }
+
+ public void setExpires(String expires) {
+ if (expires != null) {
+ try {
+ this.expires = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).parse(expires);
+ } catch (ParseException e) {
+ this.expires = null;
+ }
+ } else {
+ this.expires = null;
+ }
+ }
+
+ public Long getVisitCount() {
+ return visitCount;
+ }
+
+ public void setVisitCount(Long visitCount) {
+ this.visitCount = visitCount;
+ }
+
+ public List<Entry> getEntries() {
+ return this.entries;
+ }
+
+ public void addEntry(Entry entry) {
+ entries.add(entry);
+ }
+ }
diff --git a/src/github/daneren2005/dsub/domain/Version.java b/src/github/daneren2005/dsub/domain/Version.java new file mode 100644 index 00000000..40edf563 --- /dev/null +++ b/src/github/daneren2005/dsub/domain/Version.java @@ -0,0 +1,171 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.domain; + +/** + * Represents the version number of the Subsonic Android app. + * + * @author Sindre Mehus + * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $ + */ +public class Version implements Comparable<Version> { + private int major; + private int minor; + private int beta; + private int bugfix; + + /** + * Creates a new version instance by parsing the given string. + * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". + */ + public Version(String version) { + String[] s = version.split("\\."); + major = Integer.valueOf(s[0]); + minor = Integer.valueOf(s[1]); + + if (s.length > 2) { + if (s[2].contains("beta")) { + beta = Integer.valueOf(s[2].replace("beta", "")); + } else { + bugfix = Integer.valueOf(s[2]); + } + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public String getVersion() { + switch(major) { + case 1: + switch(minor) { + case 0: + return "3.8"; + case 1: + return "3.9"; + case 2: + return "4.0"; + case 3: + return "4.1"; + case 4: + return "4.2"; + case 5: + return "4.3.1"; + case 6: + return "4.5"; + case 7: + return "4.6"; + case 8: + return "4.7"; + case 9: + return "4.8"; + } + } + return ""; + } + + /** + * Return whether this object is equal to another. + * @param o Object to compare to. + * @return Whether this object is equals to another. + */ + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Version version = (Version) o; + + if (beta != version.beta) return false; + if (bugfix != version.bugfix) return false; + if (major != version.major) return false; + return minor == version.minor; + } + + /** + * Returns a hash code for this object. + * @return A hash code for this object. + */ + public int hashCode() { + int result; + result = major; + result = 29 * result + minor; + result = 29 * result + beta; + result = 29 * result + bugfix; + return result; + } + + /** + * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". + * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". + */ + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(major).append('.').append(minor); + if (beta != 0) { + buf.append(".beta").append(beta); + } else if (bugfix != 0) { + buf.append('.').append(bugfix); + } + + return buf.toString(); + } + + /** + * Compares this object with the specified object for order. + * @param version The object to compare to. + * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or + * greater than the specified object. + */ + @Override + public int compareTo(Version version) { + if (major < version.major) { + return -1; + } else if (major > version.major) { + return 1; + } + + if (minor < version.minor) { + return -1; + } else if (minor > version.minor) { + return 1; + } + + if (bugfix < version.bugfix) { + return -1; + } else if (bugfix > version.bugfix) { + return 1; + } + + int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; + int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; + + if (thisBeta < otherBeta) { + return -1; + } else if (thisBeta > otherBeta) { + return 1; + } + + return 0; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/fragments/ChatFragment.java b/src/github/daneren2005/dsub/fragments/ChatFragment.java new file mode 100644 index 00000000..4f373442 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/ChatFragment.java @@ -0,0 +1,223 @@ +package github.daneren2005.dsub.fragments;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ChatAdapter;
+import com.actionbarsherlock.view.Menu;
+import github.daneren2005.dsub.util.Constants;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ChatFragment extends SubsonicFragment {
+ private static final String TAG = ChatFragment.class.getSimpleName();
+ private ListView chatListView;
+ private EditText messageEditText;
+ private ImageButton sendButton;
+ private Long lastChatMessageTime = (long) 0;
+ private ArrayList<ChatMessage> messageList = new ArrayList<ChatMessage>();
+ private ScheduledExecutorService executorService;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.chat, container, false);
+
+ messageEditText = (EditText) rootView.findViewById(R.id.chat_edittext);
+ sendButton = (ImageButton) rootView.findViewById(R.id.chat_send);
+
+ sendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ sendMessage();
+ }
+ });
+
+ chatListView = (ListView) rootView.findViewById(R.id.chat_entries);
+
+ messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
+ messageEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
+ }
+ });
+
+ messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ sendMessage();
+ return true;
+ }
+
+ return false;
+ }
+ });
+
+ invalidated = true;
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(primaryFragment) {
+ load(false);
+ } else {
+ invalidated = true;
+ }
+ }
+ });
+ }
+ };
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ long refreshRate = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CHAT_REFRESH, "30"));
+ if(refreshRate > 0) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, refreshRate * 1000L, refreshRate * 1000L, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if(executorService != null) {
+ executorService.shutdown();
+ executorService = null;
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.chat, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private synchronized void load(final boolean refresh) {
+ Log.i(TAG, "Loading: " + refresh);
+ setTitle(R.string.button_bar_chat);
+ BackgroundTask<List<ChatMessage>> task = new TabBackgroundTask<List<ChatMessage>>(this) {
+ @Override
+ protected List<ChatMessage> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getChatMessages(refresh ? 0L : lastChatMessageTime, context, this);
+ }
+
+ @Override
+ protected void done(List<ChatMessage> result) {
+ if (result != null && !result.isEmpty()) {
+ if(refresh) {
+ messageList.clear();
+ }
+
+ // Reset lastChatMessageTime if we have a newer message
+ for (ChatMessage message : result) {
+ if (message.getTime() > lastChatMessageTime) {
+ lastChatMessageTime = message.getTime();
+ }
+ }
+
+ // Reverse results to show them on the bottom
+ Collections.reverse(result);
+ messageList.addAll(result);
+
+ ChatAdapter chatAdapter = new ChatAdapter(context, messageList);
+ chatListView.setAdapter(chatAdapter);
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ private void sendMessage() {
+ final String message = messageEditText.getText().toString();
+
+ if (!Util.isNullOrWhiteSpace(message)) {
+ messageEditText.setText("");
+ InputMethodManager mgr = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ mgr.hideSoftInputFromWindow(messageEditText.getWindowToken(), 0);
+
+ BackgroundTask<Void> task = new TabBackgroundTask<Void>(this) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.addChatMessage(message, context, this);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ load(false);
+ }
+ };
+
+ task.execute();
+ }
+ }
+}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/fragments/DownloadFragment.java b/src/github/daneren2005/dsub/fragments/DownloadFragment.java new file mode 100644 index 00000000..56accc30 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/DownloadFragment.java @@ -0,0 +1,1169 @@ +package github.daneren2005.dsub.fragments;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.ViewFlipper;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.MenuInflater;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.view.FadeOutAnimation;
+import github.daneren2005.dsub.view.SongView;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.VisualizerView;
+
+import static github.daneren2005.dsub.domain.PlayerState.*;
+import github.daneren2005.dsub.util.*;
+import github.daneren2005.dsub.view.AutoRepeatButton;
+import java.util.ArrayList;
+import java.util.concurrent.ScheduledFuture;
+import com.mobeta.android.dslv.*;
+import github.daneren2005.dsub.activity.EqualizerActivity;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+public class DownloadFragment extends SubsonicFragment implements OnGestureListener {
+ private static final String TAG = DownloadFragment.class.getSimpleName();
+
+ public static final int DIALOG_SAVE_PLAYLIST = 100;
+ private static final int PERCENTAGE_OF_SCREEN_FOR_SWIPE = 10;
+ private static final int COLOR_BUTTON_ENABLED = Color.rgb(51, 181, 229);
+ private static final int COLOR_BUTTON_DISABLED = Color.rgb(206, 213, 211);
+ private static final int INCREMENT_TIME = 5000;
+
+ private ViewFlipper playlistFlipper;
+ private TextView emptyTextView;
+ private TextView songTitleTextView;
+ private ImageView albumArtImageView;
+ private DragSortListView playlistView;
+ private TextView positionTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private SeekBar progressBar;
+ private AutoRepeatButton previousButton;
+ private AutoRepeatButton nextButton;
+ private View pauseButton;
+ private View stopButton;
+ private View startButton;
+ private ImageButton repeatButton;
+ private Button equalizerButton;
+ private Button visualizerButton;
+ private Button jukeboxButton;
+ private View toggleListButton;
+ private ImageButton starButton;
+ private View mainLayout;
+ private ScheduledExecutorService executorService;
+ private DownloadFile currentPlaying;
+ private long currentRevision;
+ private GestureDetector gestureScanner;
+ private int swipeDistance;
+ private int swipeVelocity;
+ private VisualizerView visualizerView;
+ private boolean nowPlaying = true;
+ private ScheduledFuture<?> hideControlsFuture;
+ private SongListAdapter songListAdapter;
+ private SilentBackgroundTask<Void> onProgressChangedTask;
+
+ /**
+ * Called when the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.download, container, false);
+ setTitle(nowPlaying ? "Now Playing" : "Downloading");
+
+ mainLayout = rootView.findViewById(R.id.download_layout);
+ if(!primaryFragment) {
+ mainLayout.setVisibility(View.GONE);
+ }
+
+ WindowManager w = context.getWindowManager();
+ Display d = w.getDefaultDisplay();
+ swipeDistance = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ swipeVelocity = (d.getWidth() + d.getHeight()) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
+ gestureScanner = new GestureDetector(this);
+
+ playlistFlipper = (ViewFlipper)rootView.findViewById(R.id.download_playlist_flipper);
+ emptyTextView = (TextView)rootView.findViewById(R.id.download_empty);
+ songTitleTextView = (TextView)rootView.findViewById(R.id.download_song_title);
+ albumArtImageView = (ImageView)rootView.findViewById(R.id.download_album_art_image);
+ positionTextView = (TextView)rootView.findViewById(R.id.download_position);
+ durationTextView = (TextView)rootView.findViewById(R.id.download_duration);
+ statusTextView = (TextView)rootView.findViewById(R.id.download_status);
+ progressBar = (SeekBar)rootView.findViewById(R.id.download_progress_bar);
+ playlistView = (DragSortListView)rootView.findViewById(R.id.download_list);
+ previousButton = (AutoRepeatButton)rootView.findViewById(R.id.download_previous);
+ nextButton = (AutoRepeatButton)rootView.findViewById(R.id.download_next);
+ pauseButton =rootView.findViewById(R.id.download_pause);
+ stopButton =rootView.findViewById(R.id.download_stop);
+ startButton =rootView.findViewById(R.id.download_start);
+ repeatButton = (ImageButton)rootView.findViewById(R.id.download_repeat);
+ equalizerButton = (Button)rootView.findViewById(R.id.download_equalizer);
+ visualizerButton = (Button)rootView.findViewById(R.id.download_visualizer);
+ jukeboxButton = (Button)rootView.findViewById(R.id.download_jukebox);
+ LinearLayout visualizerViewLayout = (LinearLayout)rootView.findViewById(R.id.download_visualizer_view_layout);
+ toggleListButton =rootView.findViewById(R.id.download_toggle_list);
+
+ starButton = (ImageButton)rootView.findViewById(R.id.download_star);
+ starButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ DownloadFile currentDownload = getDownloadService().getCurrentPlaying();
+ if (currentDownload != null) {
+ MusicDirectory.Entry currentSong = currentDownload.getSong();
+ toggleStarred(currentSong);
+ starButton.setImageResource(currentSong.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ }
+ }
+ });
+
+ View.OnTouchListener touchListener = new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent me) {
+ return gestureScanner.onTouchEvent(me);
+ }
+ };
+ pauseButton.setOnTouchListener(touchListener);
+ stopButton.setOnTouchListener(touchListener);
+ startButton.setOnTouchListener(touchListener);
+ equalizerButton.setOnTouchListener(touchListener);
+ visualizerButton.setOnTouchListener(touchListener);
+ jukeboxButton.setOnTouchListener(touchListener);
+ emptyTextView.setOnTouchListener(touchListener);
+ albumArtImageView.setOnTouchListener(touchListener);
+
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().previous();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ setControlsVisible(true);
+ }
+ });
+ previousButton.setOnRepeatListener(new Runnable() {
+ public void run() {
+ changeProgress(-INCREMENT_TIME);
+ }
+ });
+
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Boolean>(context) {
+ @Override
+ protected Boolean doInBackground() throws Throwable {
+ if (getDownloadService().getCurrentPlayingIndex() < getDownloadService().size() - 1) {
+ getDownloadService().next();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void done(Boolean result) {
+ if(result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }
+ }.execute();
+ setControlsVisible(true);
+ }
+ });
+ nextButton.setOnRepeatListener(new Runnable() {
+ public void run() {
+ changeProgress(INCREMENT_TIME);
+ }
+ });
+
+ pauseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().pause();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ stopButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().reset();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ start();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ });
+
+ repeatButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ RepeatMode repeatMode = getDownloadService().getRepeatMode().next();
+ getDownloadService().setRepeatMode(repeatMode);
+ onDownloadListChanged();
+ switch (repeatMode) {
+ case OFF:
+ Util.toast(context, R.string.download_repeat_off);
+ break;
+ case ALL:
+ Util.toast(context, R.string.download_repeat_all);
+ break;
+ case SINGLE:
+ Util.toast(context, R.string.download_repeat_single);
+ break;
+ default:
+ break;
+ }
+ setControlsVisible(true);
+ }
+ });
+
+ equalizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DownloadService downloadService = getDownloadService();
+ if(downloadService != null && downloadService.getEqualizerController() != null
+ && downloadService.getEqualizerController().getEqualizer() != null) {
+ context.startActivity(new Intent(context, EqualizerActivity.class));
+ setControlsVisible(true);
+ } else {
+ Util.toast(context, "Failed to start equalizer. Try restarting.");
+ }
+ }
+ });
+
+ visualizerButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean active = !visualizerView.isActive();
+ visualizerView.setActive(active);
+ boolean isActive = visualizerView.isActive();
+ getDownloadService().setShowVisualization(isActive);
+ updateButtons();
+ if(active == isActive) {
+ Util.toast(context, active ? R.string.download_visualizer_on : R.string.download_visualizer_off);
+ } else {
+ Util.toast(context, "Failed to start visualizer. Try restarting.");
+ }
+ setControlsVisible(true);
+ }
+ });
+
+ jukeboxButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ boolean jukeboxEnabled = !getDownloadService().isJukeboxEnabled();
+ getDownloadService().setJukeboxEnabled(jukeboxEnabled);
+ updateButtons();
+ Util.toast(context, jukeboxEnabled ? R.string.download_jukebox_on : R.string.download_jukebox_off, false);
+ setControlsVisible(true);
+ }
+ });
+
+ toggleListButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleFullscreenAlbumArt();
+ setControlsVisible(true);
+ }
+ });
+
+ progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(final SeekBar seekBar) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().seekTo(progressBar.getProgress());
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ DownloadFragment.this.onProgressChanged();
+ }
+ }.execute();
+ }
+
+ @Override
+ public void onStartTrackingTouch(final SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) {
+
+ }
+ });
+ playlistView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
+ if(nowPlaying) {
+ warnIfNetworkOrStorageUnavailable();
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().play(position);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ }.execute();
+ }
+ }
+ });
+ playlistView.setDropListener(new DragSortListView.DropListener() {
+ @Override
+ public void drop(int from, int to) {
+ getDownloadService().swap(nowPlaying, from, to);
+ onDownloadListChanged();
+ }
+ });
+ playlistView.setRemoveListener(new DragSortListView.RemoveListener() {
+ @Override
+ public void remove(int which) {
+ getDownloadService().remove(which);
+ onDownloadListChanged();
+ }
+ });
+
+ registerForContextMenu(playlistView);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null && context.getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, false)) {
+ context.getIntent().removeExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE);
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.setShufflePlayEnabled(true);
+ }
+
+ boolean visualizerAvailable = downloadService != null && downloadService.getVisualizerAvailable();
+ boolean equalizerAvailable = downloadService != null && downloadService.getEqualizerAvailable();
+
+ if (!equalizerAvailable) {
+ equalizerButton.setVisibility(View.GONE);
+ }
+ if (!visualizerAvailable) {
+ visualizerButton.setVisibility(View.GONE);
+ } else {
+ visualizerView = new VisualizerView(context);
+ visualizerViewLayout.addView(visualizerView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.FILL_PARENT));
+ }
+
+ // TODO: Extract to utility method and cache.
+ Typeface typeface = Typeface.createFromAsset(context.getAssets(), "fonts/Storopia.ttf");
+ equalizerButton.setTypeface(typeface);
+ visualizerButton.setTypeface(typeface);
+ jukeboxButton.setTypeface(typeface);
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.nowplaying_offline, menu);
+ } else {
+ if(nowPlaying) {
+ menuInflater.inflate(R.menu.nowplaying, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.nowplaying_downloading, menu);
+ }
+
+ if(getDownloadService() != null && getDownloadService().getSleepTimer()) {
+ menu.findItem(R.id.menu_toggle_timer).setTitle(R.string.download_stop_timer);
+ }
+ }
+ if(getDownloadService() != null && getDownloadService().getKeepScreenOn()) {
+ menu.findItem(R.id.menu_screen_on_off).setTitle(R.string.download_menu_screen_off);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if(menuItemSelected(menuItem.getItemId(), null)) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public void onCreateContextMenu(android.view.ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if (view == playlistView) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.nowplaying_context_offline, menu);
+ } else {
+ inflater.inflate(R.menu.nowplaying_context, menu);
+ menu.findItem(R.id.menu_star).setTitle(downloadFile.getSong().isStarred() ? R.string.common_unstar : R.string.common_star);
+ }
+
+ if (downloadFile.getSong().getParent() == null) {
+ menu.findItem(R.id.menu_show_album).setVisible(false);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ DownloadFile downloadFile = (DownloadFile) playlistView.getItemAtPosition(info.position);
+ return menuItemSelected(menuItem.getItemId(), downloadFile) || super.onContextItemSelected(menuItem);
+ }
+
+ private boolean menuItemSelected(int menuItemId, final DownloadFile song) {
+ switch (menuItemId) {
+ case R.id.menu_show_album:
+ MusicDirectory.Entry entry = song.getSong();
+
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_VIEW_ALBUM, true);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, entry.getAlbum());
+
+ if(entry.getGrandParent() != null) {
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.getGrandParent());
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_NAME, entry.getArtist());
+ }
+
+ if(Util.isOffline(context)) {
+ try {
+ // This should only be succesful if this is a online song in offline mode
+ Integer.parseInt(entry.getParent());
+ String root = FileUtil.getMusicDirectory(context).getPath();
+ String id = root + "/" + entry.getPath();
+ id = id.substring(0, id.lastIndexOf("/"));
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, id);
+ id = id.substring(0, id.lastIndexOf("/"));
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, id);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_NAME, entry.getArtist());
+ } catch(Exception e) {
+ // Do nothing, entry.getParent() is fine
+ }
+ }
+
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Util.startActivityWithoutTransition(context, intent);
+ return true;
+ case R.id.menu_lyrics:
+ SubsonicFragment fragment = new LyricsFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ARTIST, song.getSong().getArtist());
+ args.putString(Constants.INTENT_EXTRA_NAME_TITLE, song.getSong().getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.download_layout_container);
+ return true;
+ case R.id.menu_remove:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().remove(song);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onDownloadListChanged();
+ }
+ }.execute();
+ return true;
+ case R.id.menu_delete:
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(1);
+ songs.add(song.getSong());
+ getDownloadService().delete(songs);
+ return true;
+ case R.id.menu_remove_all:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().setShufflePlayEnabled(false);
+ if(nowPlaying) {
+ getDownloadService().clear();
+ }
+ else {
+ getDownloadService().clearBackground();
+ }
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ onDownloadListChanged();
+ }
+ }.execute();
+ return true;
+ case R.id.menu_screen_on_off:
+ if (getDownloadService().getKeepScreenOn()) {
+ context.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(false);
+ } else {
+ context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ getDownloadService().setKeepScreenOn(true);
+ }
+ context.invalidateOptionsMenu();
+ return true;
+ case R.id.menu_shuffle:
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ getDownloadService().shuffle();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_menu_shuffle_notification);
+ }
+ }.execute();
+ return true;
+ case R.id.menu_save_playlist:
+ List<MusicDirectory.Entry> entries = new LinkedList<MusicDirectory.Entry>();
+ for (DownloadFile downloadFile : getDownloadService().getSongs()) {
+ entries.add(downloadFile.getSong());
+ }
+ createNewPlaylist(entries, true);
+ return true;
+ case R.id.menu_star:
+ toggleStarred(song.getSong());
+ return true;
+ case R.id.menu_toggle_now_playing:
+ toggleNowPlaying();
+ context.invalidateOptionsMenu();
+ return true;
+ case R.id.menu_toggle_timer:
+ if(getDownloadService().getSleepTimer()) {
+ getDownloadService().stopSleepTimer();
+ context.invalidateOptionsMenu();
+ } else {
+ startTimer();
+ }
+ return true;
+ case R.id.menu_add_playlist:
+ songs = new ArrayList<MusicDirectory.Entry>(1);
+ songs.add(song.getSong());
+ addToPlaylist(songs);
+ return true;
+ case R.id.menu_info:
+ displaySongInfo(song.getSong());
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ update();
+ }
+ });
+ }
+ };
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0L, 1000L, TimeUnit.MILLISECONDS);
+
+ setControlsVisible(true);
+
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null || downloadService.getCurrentPlaying() == null) {
+ playlistFlipper.setDisplayedChild(1);
+ }
+
+ scrollToCurrent();
+ if (downloadService != null && downloadService.getKeepScreenOn()) {
+ context.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ context.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ if (visualizerView != null && downloadService != null && downloadService.getShowVisualization()) {
+ visualizerView.setActive(true);
+ }
+
+ updateButtons();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ executorService.shutdown();
+ if (visualizerView != null && visualizerView.isActive()) {
+ visualizerView.setActive(false);
+ }
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null) {
+ if(primary) {
+ mainLayout.setVisibility(View.VISIBLE);
+ } else {
+ mainLayout.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void scheduleHideControls() {
+ if (hideControlsFuture != null) {
+ hideControlsFuture.cancel(false);
+ }
+
+ final Handler handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ setControlsVisible(false);
+ }
+ });
+ }
+ };
+ hideControlsFuture = executorService.schedule(runnable, 3000L, TimeUnit.MILLISECONDS);
+ }
+
+ private void setControlsVisible(boolean visible) {
+ try {
+ long duration = 1700L;
+ FadeOutAnimation.createAndStart(rootView.findViewById(R.id.download_overlay_buttons), !visible, duration);
+
+ if (visible) {
+ scheduleHideControls();
+ }
+ } catch(Exception e) {
+
+ }
+ }
+
+ private void updateButtons() {
+ SharedPreferences prefs = Util.getPreferences(context);
+ boolean equalizerOn = prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false);
+ if(equalizerOn && getDownloadService() != null && getDownloadService().getEqualizerController() != null &&
+ getDownloadService().getEqualizerController().isEnabled()) {
+ equalizerButton.setTextColor(COLOR_BUTTON_ENABLED);
+ } else {
+ equalizerButton.setTextColor(COLOR_BUTTON_DISABLED);
+ }
+
+ if (visualizerView != null) {
+ visualizerButton.setTextColor(visualizerView.isActive() ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ boolean jukeboxEnabled = getDownloadService() != null && getDownloadService().isJukeboxEnabled();
+ jukeboxButton.setTextColor(jukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ }
+
+ // Scroll to current playing/downloading.
+ private void scrollToCurrent() {
+ if (getDownloadService() == null || songListAdapter == null) {
+ return;
+ }
+
+ for (int i = 0; i < songListAdapter.getCount(); i++) {
+ if (currentPlaying == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ DownloadFile currentDownloading = getDownloadService().getCurrentDownloading();
+ for (int i = 0; i < songListAdapter.getCount(); i++) {
+ if (currentDownloading == playlistView.getItemAtPosition(i)) {
+ playlistView.setSelectionFromTop(i, 40);
+ return;
+ }
+ }
+ }
+
+ private void update() {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ if (currentRevision != getDownloadService().getDownloadListUpdateRevision()) {
+ onDownloadListChanged();
+ }
+
+ if (currentPlaying != getDownloadService().getCurrentPlaying()) {
+ onCurrentChanged();
+ }
+
+ onProgressChanged();
+ }
+
+ protected void startTimer() {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.start_timer, null);
+ final EditText lengthBox = (EditText)dialogView.findViewById(R.id.timer_length);
+
+ final SharedPreferences prefs = Util.getPreferences(context);
+ lengthBox.setText(prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, ""));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_set_timer)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ String length = lengthBox.getText().toString();
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, length);
+ editor.commit();
+
+ getDownloadService().setSleepTimerDuration(Integer.parseInt(length));
+ getDownloadService().startSleepTimer();
+ context.invalidateOptionsMenu();
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private void toggleFullscreenAlbumArt() {
+ if (playlistFlipper.getDisplayedChild() == 1) {
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_down_out));
+ playlistFlipper.setDisplayedChild(0);
+ } else {
+ scrollToCurrent();
+ playlistFlipper.setInAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_in));
+ playlistFlipper.setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.push_up_out));
+ playlistFlipper.setDisplayedChild(1);
+ }
+ }
+
+ private void start() {
+ DownloadService service = getDownloadService();
+ PlayerState state = service.getPlayerState();
+ if (state == PAUSED || state == COMPLETED || state == STOPPED) {
+ service.start();
+ } else if (state == STOPPED || state == IDLE) {
+ warnIfNetworkOrStorageUnavailable();
+ int current = service.getCurrentPlayingIndex();
+ // TODO: Use play() method.
+ if (current == -1) {
+ service.play(0);
+ } else {
+ service.play(current);
+ }
+ }
+ }
+ private void onDownloadListChanged() {
+ onDownloadListChanged(false);
+ }
+ private void onDownloadListChanged(boolean refresh) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ List<DownloadFile> list;
+ if(nowPlaying) {
+ list = downloadService.getSongs();
+ }
+ else {
+ list = downloadService.getBackgroundDownloads();
+ }
+
+ if(downloadService.isShufflePlayEnabled()) {
+ emptyTextView.setText(R.string.download_shuffle_loading);
+ }
+ else {
+ emptyTextView.setText(R.string.download_empty);
+ }
+
+ if(songListAdapter == null || refresh) {
+ playlistView.setAdapter(songListAdapter = new SongListAdapter(list));
+ } else {
+ songListAdapter.notifyDataSetChanged();
+ }
+ emptyTextView.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
+ currentRevision = downloadService.getDownloadListUpdateRevision();
+
+ switch (downloadService.getRepeatMode()) {
+ case OFF:
+ if("light".equals(SubsonicActivity.getThemeName()) | "light_fullscreen".equals(SubsonicActivity.getThemeName())) {
+ repeatButton.setImageResource(R.drawable.media_repeat_off_light);
+ } else {
+ repeatButton.setImageResource(R.drawable.media_repeat_off);
+ }
+ break;
+ case ALL:
+ repeatButton.setImageResource(R.drawable.media_repeat_all);
+ break;
+ case SINGLE:
+ repeatButton.setImageResource(R.drawable.media_repeat_single);
+ break;
+ default:
+ break;
+ }
+
+ setSubtitle(context.getResources().getString(R.string.download_playing_out_of, downloadService.getCurrentPlayingIndex() + 1, downloadService.size()));
+ }
+
+ private void onCurrentChanged() {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return;
+ }
+
+ currentPlaying = downloadService.getCurrentPlaying();
+ if (currentPlaying != null) {
+ MusicDirectory.Entry song = currentPlaying.getSong();
+ songTitleTextView.setText(song.getTitle());
+ getImageLoader().loadImage(albumArtImageView, song, true, true);
+ starButton.setImageResource(song.isStarred() ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
+ setSubtitle(context.getResources().getString(R.string.download_playing_out_of, downloadService.getCurrentPlayingIndex() + 1, downloadService.size()));
+ } else {
+ songTitleTextView.setText(null);
+ getImageLoader().loadImage(albumArtImageView, null, true, false);
+ starButton.setImageResource(android.R.drawable.btn_star_big_off);
+ setSubtitle(null);
+ }
+ }
+
+ private void onProgressChanged() {
+ // Make sure to only be trying to run one of these at a time
+ if (getDownloadService() == null || onProgressChangedTask != null) {
+ return;
+ }
+
+ onProgressChangedTask = new SilentBackgroundTask<Void>(context) {
+ DownloadService downloadService;
+ boolean isJukeboxEnabled;
+ int millisPlayed;
+ Integer duration;
+ PlayerState playerState;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ downloadService = getDownloadService();
+ isJukeboxEnabled = downloadService.isJukeboxEnabled();
+ millisPlayed = Math.max(0, downloadService.getPlayerPosition());
+ duration = downloadService.getPlayerDuration();
+ playerState = getDownloadService().getPlayerState();
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if (currentPlaying != null) {
+ int millisTotal = duration == null ? 0 : duration;
+
+ positionTextView.setText(Util.formatDuration(millisPlayed / 1000));
+ durationTextView.setText(Util.formatDuration(millisTotal / 1000));
+ progressBar.setMax(millisTotal == 0 ? 100 : millisTotal); // Work-around for apparent bug.
+ progressBar.setProgress(millisPlayed);
+ progressBar.setEnabled(currentPlaying.isWorkDone() || isJukeboxEnabled);
+ } else {
+ positionTextView.setText("0:00");
+ durationTextView.setText("-:--");
+ progressBar.setProgress(0);
+ progressBar.setEnabled(false);
+ }
+
+ switch (playerState) {
+ case DOWNLOADING:
+ long bytes = currentPlaying.getPartialFile().length();
+ statusTextView.setText(context.getResources().getString(R.string.download_playerstate_downloading, Util.formatLocalizedBytes(bytes, context)));
+ break;
+ case PREPARING:
+ statusTextView.setText(R.string.download_playerstate_buffering);
+ break;
+ default:
+ if(currentPlaying != null) {
+ String artist = "";
+ if(currentPlaying.getSong().getArtist() != null) {
+ artist = currentPlaying.getSong().getArtist() + " - ";
+ }
+ statusTextView.setText(artist + currentPlaying.getSong().getAlbum());
+ } else {
+ statusTextView.setText(null);
+ }
+ break;
+ }
+
+ switch (playerState) {
+ case STARTED:
+ pauseButton.setVisibility(View.VISIBLE);
+ stopButton.setVisibility(View.INVISIBLE);
+ startButton.setVisibility(View.INVISIBLE);
+ break;
+ case DOWNLOADING:
+ case PREPARING:
+ pauseButton.setVisibility(View.INVISIBLE);
+ stopButton.setVisibility(View.VISIBLE);
+ startButton.setVisibility(View.INVISIBLE);
+ break;
+ default:
+ pauseButton.setVisibility(View.INVISIBLE);
+ stopButton.setVisibility(View.INVISIBLE);
+ startButton.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ jukeboxButton.setTextColor(isJukeboxEnabled ? COLOR_BUTTON_ENABLED : COLOR_BUTTON_DISABLED);
+ onProgressChangedTask = null;
+ }
+ };
+ onProgressChangedTask.execute();
+ }
+
+ private void changeProgress(final int ms) {
+ final DownloadService downloadService = getDownloadService();
+ if(downloadService == null) {
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ boolean isJukeboxEnabled;
+ int msPlayed;
+ Integer duration;
+ PlayerState playerState;
+ int seekTo;
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ msPlayed = Math.max(0, downloadService.getPlayerPosition());
+ duration = downloadService.getPlayerDuration();
+ playerState = getDownloadService().getPlayerState();
+ int msTotal = duration == null ? 0 : duration;
+ if(msPlayed + ms > msTotal) {
+ seekTo = msTotal;
+ } else {
+ seekTo = msPlayed + ms;
+ }
+ downloadService.seekTo(seekTo);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ progressBar.setProgress(seekTo);
+ }
+ }.execute();
+ }
+
+ private class SongListAdapter extends ArrayAdapter<DownloadFile> {
+ public SongListAdapter(List<DownloadFile> entries) {
+ super(context, android.R.layout.simple_list_item_1, entries);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(context);
+ }
+ DownloadFile downloadFile = getItem(position);
+ view.setSong(downloadFile.getSong(), false);
+ return view;
+ }
+ }
+
+ @Override
+ public boolean onDown(MotionEvent me) {
+ setControlsVisible(true);
+ return false;
+ }
+
+ public GestureDetector getGestureDetector() {
+ return gestureScanner;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService == null) {
+ return false;
+ }
+ Log.d(TAG, "onFling");
+
+ // Right to Left swipe
+ if (e1.getX() - e2.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) {
+ downloadService.next();
+ onCurrentChanged();
+ onProgressChanged();
+ }
+ return true;
+ }
+
+ // Left to Right swipe
+ else if (e2.getX() - e1.getX() > swipeDistance && Math.abs(velocityX) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.previous();
+ onCurrentChanged();
+ onProgressChanged();
+ return true;
+ }
+
+ // Top to Bottom swipe
+ else if (e2.getY() - e1.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.seekTo(downloadService.getPlayerPosition() + 30000);
+ onProgressChanged();
+ return true;
+ }
+
+ // Bottom to Top swipe
+ else if (e1.getY() - e2.getY() > swipeDistance && Math.abs(velocityY) > swipeVelocity) {
+ warnIfNetworkOrStorageUnavailable();
+ downloadService.seekTo(downloadService.getPlayerPosition() - 8000);
+ onProgressChanged();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void toggleNowPlaying() {
+ nowPlaying = !nowPlaying;
+ setTitle(nowPlaying ? "Now Playing" : "Downloading");
+ onDownloadListChanged(true);
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/LyricsFragment.java b/src/github/daneren2005/dsub/fragments/LyricsFragment.java new file mode 100644 index 00000000..0b247986 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/LyricsFragment.java @@ -0,0 +1,81 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ + +package github.daneren2005.dsub.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Lyrics; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; +import github.daneren2005.dsub.util.BackgroundTask; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.TabBackgroundTask; + +/** + * Displays song lyrics. + * + * @author Sindre Mehus + */ +public final class LyricsFragment extends SubsonicFragment { + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + setTitle(R.string.download_menu_lyrics); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + rootView = inflater.inflate(R.layout.lyrics, container, false); + load(); + + return rootView; + } + + private void load() { + BackgroundTask<Lyrics> task = new TabBackgroundTask<Lyrics>(this) { + @Override + protected Lyrics doInBackground() throws Throwable { + String artist = getArguments().getString(Constants.INTENT_EXTRA_NAME_ARTIST); + String title = getArguments().getString(Constants.INTENT_EXTRA_NAME_TITLE); + MusicService musicService = MusicServiceFactory.getMusicService(context); + return musicService.getLyrics(artist, title, context, this); + } + + @Override + protected void done(Lyrics result) { + TextView artistView = (TextView) rootView.findViewById(R.id.lyrics_artist); + TextView titleView = (TextView) rootView.findViewById(R.id.lyrics_title); + TextView textView = (TextView) rootView.findViewById(R.id.lyrics_text); + if (result != null && result.getArtist() != null) { + artistView.setText(result.getArtist()); + titleView.setText(result.getTitle()); + textView.setText(result.getText()); + } else { + artistView.setText(R.string.lyrics_nomatch); + } + } + }; + task.execute(); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/fragments/MainFragment.java b/src/github/daneren2005/dsub/fragments/MainFragment.java new file mode 100644 index 00000000..9a48acd5 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/MainFragment.java @@ -0,0 +1,373 @@ +package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.StatFs;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.view.MergeAdapter;
+import github.daneren2005.dsub.util.Util;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.MenuInflater;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.view.ChangeLog;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class MainFragment extends SubsonicFragment {
+ private static final String TAG = MainFragment.class.getSimpleName();
+ private LayoutInflater inflater;
+
+ private static final int MENU_GROUP_SERVER = 10;
+ private static final int MENU_ITEM_SERVER_BASE = 100;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ this.inflater = inflater;
+ rootView = inflater.inflate(R.layout.home, container, false);
+
+ createLayout();
+
+ return rootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.main, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_log:
+ getLogs();
+ return true;
+ case R.id.menu_about:
+ showAboutDialog();
+ return true;
+ case R.id.menu_changelog:
+ ChangeLog changeLog = new ChangeLog(context, Util.getPreferences(context));
+ changeLog.getFullLogDialog().show();
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ int serverCount = Util.getServerCount(context);
+ int activeServer = Util.getActiveServer(context);
+ for(int i = 1; i <= serverCount; i++) {
+ android.view.MenuItem menuItem = menu.add(MENU_GROUP_SERVER, MENU_ITEM_SERVER_BASE + i, MENU_ITEM_SERVER_BASE + i, Util.getServerName(context, i));
+ if(i == activeServer) {
+ menuItem.setChecked(true);
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_SERVER, true, true);
+ menu.setHeaderTitle(R.string.main_select_server);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ int activeServer = menuItem.getItemId() - MENU_ITEM_SERVER_BASE;
+ setActiveServer(activeServer);
+ context.getPagerAdapter().invalidate();
+ return true;
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ createLayout();
+ }
+
+ private void createLayout() {
+ View buttons = inflater.inflate(R.layout.main_buttons, null);
+
+ final View serverButton = buttons.findViewById(R.id.main_select_server);
+ final TextView serverTextView = (TextView) serverButton.findViewById(R.id.main_select_server_2);
+ final TextView offlineButton = (TextView) buttons.findViewById(R.id.main_offline);
+ offlineButton.setText(Util.isOffline(context) ? R.string.main_online : R.string.main_offline);
+
+ final View albumsTitle = buttons.findViewById(R.id.main_albums);
+ final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
+ final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
+ final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
+ final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
+ final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
+ final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
+ final View albumsGenresButton = buttons.findViewById(R.id.main_albums_genres);
+
+ final View dummyView = rootView.findViewById(R.id.main_dummy);
+
+ int instance = Util.getActiveServer(context);
+ String name = Util.getServerName(context, instance);
+ serverTextView.setText(name);
+
+ ListView list = (ListView) rootView.findViewById(R.id.main_list);
+
+ MergeAdapter adapter = new MergeAdapter();
+ if (!Util.isOffline(context)) {
+ adapter.addViews(Arrays.asList(serverButton), true);
+ }
+ adapter.addView(offlineButton, true);
+ if (!Util.isOffline(context)) {
+ adapter.addView(albumsTitle, false);
+ adapter.addViews(Arrays.asList(albumsNewestButton, albumsRandomButton, albumsHighestButton, albumsStarredButton, albumsGenresButton, albumsRecentButton, albumsFrequentButton), true);
+ }
+ list.setAdapter(adapter);
+ registerForContextMenu(dummyView);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == serverButton) {
+ dummyView.showContextMenu();
+ } else if (view == offlineButton) {
+ toggleOffline();
+ } else if (view == albumsNewestButton) {
+ showAlbumList("newest");
+ } else if (view == albumsRandomButton) {
+ showAlbumList("random");
+ } else if (view == albumsHighestButton) {
+ showAlbumList("highest");
+ } else if (view == albumsRecentButton) {
+ showAlbumList("recent");
+ } else if (view == albumsFrequentButton) {
+ showAlbumList("frequent");
+ } else if (view == albumsStarredButton) {
+ showAlbumList("starred");
+ } else if(view == albumsGenresButton) {
+ showAlbumList("genres");
+ }
+ }
+ });
+ }
+
+ private void setActiveServer(int instance) {
+ if (Util.getActiveServer(context) != instance) {
+ DownloadService service = getDownloadService();
+ if (service != null) {
+ service.clearIncomplete();
+ }
+ Util.setActiveServer(context, instance);
+ }
+ }
+
+ private void toggleOffline() {
+ boolean isOffline = Util.isOffline(context);
+ Util.setOffline(context, !isOffline);
+ context.getPagerAdapter().invalidate();
+
+ if(isOffline) {
+ int scrobblesCount = Util.offlineScrobblesCount(context);
+ int starsCount = Util.offlineStarsCount(context);
+ if(scrobblesCount > 0 || starsCount > 0){
+ showOfflineSyncDialog(scrobblesCount, starsCount);
+ }
+ }
+ }
+
+ private void showAlbumList(String type) {
+ if("genres".equals(type)) {
+ SubsonicFragment fragment = new SelectGenreFragment();
+ replaceFragment(fragment, R.id.home_layout);
+ } else {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.home_layout);
+ }
+ }
+
+ private void showOfflineSyncDialog(final int scrobbleCount, final int starsCount) {
+ String syncDefault = Util.getSyncDefault(context);
+ if(syncDefault != null) {
+ if("sync".equals(syncDefault)) {
+ syncOffline(scrobbleCount, starsCount);
+ return;
+ } else if("delete".equals(syncDefault)) {
+ deleteOffline();
+ return;
+ }
+ }
+
+ View checkBoxView = context.getLayoutInflater().inflate(R.layout.sync_dialog, null);
+ final CheckBox checkBox = (CheckBox)checkBoxView.findViewById(R.id.sync_default);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(R.string.offline_sync_dialog_title)
+ .setMessage(context.getResources().getString(R.string.offline_sync_dialog_message, scrobbleCount, starsCount))
+ .setView(checkBoxView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if(checkBox.isChecked()) {
+ Util.setSyncDefault(context, "sync");
+ }
+ syncOffline(scrobbleCount, starsCount);
+ }
+ }).setNeutralButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ }
+ }).setNegativeButton(R.string.common_delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if(checkBox.isChecked()) {
+ Util.setSyncDefault(context, "delete");
+ }
+ deleteOffline();
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private void syncOffline(final int scrobbleCount, final int starsCount) {
+ new SilentBackgroundTask<Integer>(context) {
+ @Override
+ protected Integer doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.processOfflineSyncs(context, null);
+ }
+
+ @Override
+ protected void done(Integer result) {
+ if(result == scrobbleCount) {
+ Util.toast(context, context.getResources().getString(R.string.offline_sync_success, result));
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.offline_sync_partial, result, scrobbleCount + starsCount));
+ }
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg = context.getResources().getString(R.string.offline_sync_error) + " " + getErrorMessage(error);
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ private void deleteOffline() {
+ SharedPreferences.Editor offline = Util.getOfflineSync(context).edit();
+ offline.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ offline.commit();
+ }
+
+ private void showAboutDialog() {
+ try {
+ File rootFolder = FileUtil.getMusicDirectory(context);
+ StatFs stat = new StatFs(rootFolder.getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+
+ String msg = getResources().getString(R.string.main_about_text,
+ context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName,
+ Util.formatBytes(FileUtil.getUsedSize(context, rootFolder)),
+ Util.formatBytes(Util.getCacheSizeMB(context) * 1024L * 1024L),
+ Util.formatBytes(bytesAvailableFs),
+ Util.formatBytes(bytesTotalFs));
+ Util.info(context, R.string.main_about_title, msg);
+ } catch(Exception e) {
+ Util.toast(context, "Failed to open dialog");
+ }
+ }
+
+ private void getLogs() {
+ try {
+ final String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
+ new LoadingTask<File>(context) {
+ @Override
+ protected File doInBackground() throws Throwable {
+ updateProgress("Gathering Logs");
+ File logcat = new File(FileUtil.getSubsonicDirectory(), "logcat.txt");
+ Process logcatProc = null;
+
+ try {
+ List<String> progs = new ArrayList<String>();
+ progs.add("logcat");
+ progs.add("-v");
+ progs.add("time");
+ progs.add("-d");
+ progs.add("-f");
+ progs.add(logcat.getPath());
+ progs.add("*:I");
+
+ logcatProc = Runtime.getRuntime().exec(progs.toArray(new String[0]));
+ logcatProc.waitFor();
+ } catch(Exception e) {
+ Util.toast(context, "Failed to gather logs");
+ } finally {
+ if(logcatProc != null) {
+ logcatProc.destroy();
+ }
+ }
+
+ return logcat;
+ }
+
+ @Override
+ protected void done(File logcat) {
+ Intent email = new Intent(android.content.Intent.ACTION_SEND);
+ email.setType("text/plain");
+ email.putExtra(Intent.EXTRA_EMAIL, new String[] {"dsub.android@gmail.com"});
+ email.putExtra(Intent.EXTRA_SUBJECT, "DSub " + version + " Error Logs");
+ email.putExtra(Intent.EXTRA_TEXT, "Describe the problem here");
+ Uri attachment = Uri.fromFile(logcat);
+ email.putExtra(Intent.EXTRA_STREAM, attachment);
+ startActivity(email);
+ }
+ }.execute();
+ } catch(Exception e) {}
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SearchFragment.java b/src/github/daneren2005/dsub/fragments/SearchFragment.java new file mode 100644 index 00000000..1c35d38c --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SearchFragment.java @@ -0,0 +1,333 @@ +package github.daneren2005.dsub.fragments;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.MenuItem;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.net.Uri;
+import android.view.ViewGroup;
+import com.actionbarsherlock.view.Menu;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SearchActivity;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.SearchCritera;
+import github.daneren2005.dsub.domain.SearchResult;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.view.ArtistAdapter;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.view.EntryAdapter;
+import github.daneren2005.dsub.view.MergeAdapter;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+
+public class SearchFragment extends SubsonicFragment {
+ private static final int DEFAULT_ARTISTS = 3;
+ private static final int DEFAULT_ALBUMS = 5;
+ private static final int DEFAULT_SONGS = 10;
+
+ private static final int MAX_ARTISTS = 10;
+ private static final int MAX_ALBUMS = 20;
+ private static final int MAX_SONGS = 25;
+ private ListView list;
+
+ private View artistsHeading;
+ private View albumsHeading;
+ private View songsHeading;
+ private TextView searchButton;
+ private View moreArtistsButton;
+ private View moreAlbumsButton;
+ private View moreSongsButton;
+ private SearchResult searchResult;
+ private MergeAdapter mergeAdapter;
+ private ArtistAdapter artistAdapter;
+ private ListAdapter moreArtistsAdapter;
+ private EntryAdapter albumAdapter;
+ private ListAdapter moreAlbumsAdapter;
+ private ListAdapter moreSongsAdapter;
+ private EntryAdapter songAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.search, container, false);
+ setTitle(R.string.search_title);
+
+ View buttons = inflater.inflate(R.layout.search_buttons, null);
+
+ artistsHeading = buttons.findViewById(R.id.search_artists);
+ albumsHeading = buttons.findViewById(R.id.search_albums);
+ songsHeading = buttons.findViewById(R.id.search_songs);
+
+ searchButton = (TextView) buttons.findViewById(R.id.search_search);
+ moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
+ moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
+ moreSongsButton = buttons.findViewById(R.id.search_more_songs);
+
+ list = (ListView) rootView.findViewById(R.id.search_list);
+
+ list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == searchButton) {
+ context.onSearchRequested();
+ } else if (view == moreArtistsButton) {
+ expandArtists();
+ } else if (view == moreAlbumsButton) {
+ expandAlbums();
+ } else if (view == moreSongsButton) {
+ expandSongs();
+ } else {
+ Object item = parent.getItemAtPosition(position);
+ if (item instanceof Artist) {
+ onArtistSelected((Artist) item);
+ } else if (item instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
+ if (entry.isDirectory()) {
+ onAlbumSelected(entry, false);
+ } else if (entry.isVideo()) {
+ onVideoSelected(entry);
+ } else {
+ onSongSelected(entry, false, true, true, false);
+ }
+
+ }
+ }
+ }
+ });
+ registerForContextMenu(list);
+ ((SearchActivity)context).onSupportNewIntent(context.getIntent());
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.search, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ 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 selectedItem = list.getItemAtPosition(info.position);
+ onCreateContextMenu(menu, view, menuInfo, selectedItem);
+ if(selectedItem instanceof MusicDirectory.Entry && !((MusicDirectory.Entry) selectedItem).isVideo() && !Util.isOffline(context)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = list.getItemAtPosition(info.position);
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null && primary) {
+ ((SearchActivity)context).onSupportNewIntent(context.getIntent());
+ }
+ }
+
+ public void search(final String query, final boolean autoplay) {
+ mergeAdapter = new MergeAdapter();
+ list.setAdapter(mergeAdapter);
+
+ BackgroundTask<SearchResult> task = new TabBackgroundTask<SearchResult>(this) {
+ @Override
+ protected SearchResult doInBackground() throws Throwable {
+ SearchCritera criteria = new SearchCritera(query, MAX_ARTISTS, MAX_ALBUMS, MAX_SONGS);
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ return service.search(criteria, context, this);
+ }
+
+ @Override
+ protected void done(SearchResult result) {
+ searchResult = result;
+ populateList();
+ if (autoplay) {
+ autoplay();
+ }
+
+ }
+ };
+ task.execute();
+ }
+
+ public void populateList() {
+ mergeAdapter = new MergeAdapter();
+ mergeAdapter.addView(searchButton, true);
+
+ if (searchResult != null) {
+ List<Artist> artists = searchResult.getArtists();
+ if (!artists.isEmpty()) {
+ mergeAdapter.addView(artistsHeading);
+ List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
+ artistAdapter = new ArtistAdapter(context, displayedArtists);
+ mergeAdapter.addAdapter(artistAdapter);
+ if (artists.size() > DEFAULT_ARTISTS) {
+ moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> albums = searchResult.getAlbums();
+ if (!albums.isEmpty()) {
+ mergeAdapter.addView(albumsHeading);
+ List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
+ albumAdapter = new EntryAdapter(context, getImageLoader(), displayedAlbums, false);
+ mergeAdapter.addAdapter(albumAdapter);
+ if (albums.size() > DEFAULT_ALBUMS) {
+ moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
+ }
+ }
+
+ List<MusicDirectory.Entry> songs = searchResult.getSongs();
+ if (!songs.isEmpty()) {
+ mergeAdapter.addView(songsHeading);
+ List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
+ songAdapter = new EntryAdapter(context, getImageLoader(), displayedSongs, false);
+ mergeAdapter.addAdapter(songAdapter);
+ if (songs.size() > DEFAULT_SONGS) {
+ moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
+ }
+ }
+
+ boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
+ searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
+ }
+
+ list.setAdapter(mergeAdapter);
+ }
+
+ private void expandArtists() {
+ artistAdapter.clear();
+ for (Artist artist : searchResult.getArtists()) {
+ artistAdapter.add(artist);
+ }
+ artistAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreArtistsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandAlbums() {
+ albumAdapter.clear();
+ for (MusicDirectory.Entry album : searchResult.getAlbums()) {
+ albumAdapter.add(album);
+ }
+ albumAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreAlbumsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void expandSongs() {
+ songAdapter.clear();
+ for (MusicDirectory.Entry song : searchResult.getSongs()) {
+ songAdapter.add(song);
+ }
+ songAdapter.notifyDataSetChanged();
+ mergeAdapter.removeAdapter(moreSongsAdapter);
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ private void onArtistSelected(Artist artist) {
+ 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());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.search_layout);
+ }
+
+ private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay) {
+ int id = R.id.search_layout;
+ Bundle args;
+ if(album.getParent() != null) {
+ SubsonicFragment parentFragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getParent());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getArtist());
+ parentFragment.setArguments(args);
+
+ replaceFragment(parentFragment, R.id.search_layout);
+ id = parentFragment.getRootId();
+ }
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, id);
+ }
+
+ private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext) {
+ DownloadService downloadService = getDownloadService();
+ if (downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ downloadService.download(Arrays.asList(song), save, false, playNext, false);
+ if (autoplay) {
+ downloadService.play(downloadService.size() - 1);
+ }
+
+ Util.toast(context, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
+ }
+ }
+
+ private void onVideoSelected(MusicDirectory.Entry entry) {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(maxBitrate, context, entry.getId())));
+ startActivity(intent);
+ }
+
+ private void autoplay() {
+ if (!searchResult.getSongs().isEmpty()) {
+ onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
+ } else if (!searchResult.getAlbums().isEmpty()) {
+ onAlbumSelected(searchResult.getAlbums().get(0), true);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java b/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java new file mode 100644 index 00000000..0a35233e --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SelectArtistFragment.java @@ -0,0 +1,208 @@ +package github.daneren2005.dsub.fragments;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import com.actionbarsherlock.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.ArtistAdapter;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectArtistFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectArtistFragment.class.getSimpleName();
+ private static final int MENU_GROUP_MUSIC_FOLDER = 10;
+
+ private ListView artistList;
+ private View folderButtonParent;
+ private View folderButton;
+ private TextView folderName;
+ private List<MusicFolder> musicFolders = null;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_artist, container, false);
+
+ artistList = (ListView) rootView.findViewById(R.id.select_artist_list);
+ artistList.setOnItemClickListener(this);
+
+ folderButtonParent = inflater.inflate(R.layout.select_artist_header, artistList, false);
+ folderName = (TextView) folderButtonParent.findViewById(R.id.select_artist_folder_2);
+ artistList.addHeaderView(folderButtonParent);
+ folderButton = folderButtonParent.findViewById(R.id.select_artist_folder);
+
+ registerForContextMenu(artistList);
+ if(!primaryFragment) {
+ invalidated = true;
+ } else {
+ refresh(false);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_artist, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ 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 = artistList.getItemAtPosition(info.position);
+
+ if (entry instanceof Artist) {
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ } else if (info.position == 0) {
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ MenuItem menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders);
+ if (musicFolderId == null) {
+ menuItem.setChecked(true);
+ }
+ if (musicFolders != null) {
+ for (int i = 0; i < musicFolders.size(); i++) {
+ MusicFolder musicFolder = musicFolders.get(i);
+ menuItem = menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, musicFolder.getName());
+ if (musicFolder.getId().equals(musicFolderId)) {
+ menuItem.setChecked(true);
+ }
+ }
+ }
+ menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Artist artist = (Artist) artistList.getItemAtPosition(info.position);
+
+ if (artist != null) {
+ return onContextItemSelected(menuItem, artist);
+ } else if (info.position == 0) {
+ MusicFolder selectedFolder = menuItem.getItemId() == -1 ? null : musicFolders.get(menuItem.getItemId());
+ String musicFolderId = selectedFolder == null ? null : selectedFolder.getId();
+ String musicFolderName = selectedFolder == null ? context.getString(R.string.select_artist_all_folders)
+ : selectedFolder.getName();
+ Util.setSelectedMusicFolderId(context, musicFolderId);
+ folderName.setText(musicFolderName);
+ refresh();
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (view == folderButtonParent) {
+ selectFolder();
+ } else {
+ 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());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_artist_layout);
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private void load(final boolean refresh) {
+ setTitle(R.string.search_artists);
+
+ if (Util.isOffline(context)) {
+ folderButton.setVisibility(View.GONE);
+ } else {
+ folderButton.setVisibility(View.VISIBLE);
+ }
+ artistList.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<Indexes> task = new TabBackgroundTask<Indexes>(this) {
+ @Override
+ protected Indexes doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ if (!Util.isOffline(context)) {
+ musicFolders = musicService.getMusicFolders(refresh, context, this);
+ }
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ return musicService.getIndexes(musicFolderId, refresh, context, this);
+ }
+
+ @Override
+ protected void done(Indexes result) {
+ List<Artist> artists = new ArrayList<Artist>(result.getShortcuts().size() + result.getArtists().size());
+ artists.addAll(result.getShortcuts());
+ artists.addAll(result.getArtists());
+ artistList.setAdapter(new ArtistAdapter(context, artists));
+
+ // Display selected music folder
+ if (musicFolders != null) {
+ String musicFolderId = Util.getSelectedMusicFolderId(context);
+ if (musicFolderId == null) {
+ folderName.setText(R.string.select_artist_all_folders);
+ } else {
+ for (MusicFolder musicFolder : musicFolders) {
+ if (musicFolder.getId().equals(musicFolderId)) {
+ folderName.setText(musicFolder.getName());
+ break;
+ }
+ }
+ }
+ }
+ artistList.setVisibility(View.VISIBLE);
+ }
+ };
+ task.execute();
+ }
+
+ private void selectFolder() {
+ folderButton.showContextMenu();
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java b/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java new file mode 100644 index 00000000..c3771492 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SelectDirectoryFragment.java @@ -0,0 +1,818 @@ +package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.view.EntryAdapter;
+import java.util.List;
+import com.mobeta.android.dslv.*;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.SearchActivity;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Pair;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.AlbumListAdapter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectDirectoryFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectDirectoryFragment.class.getSimpleName();
+
+ private DragSortListView entryList;
+ int rootId;
+ private View footer;
+ private View emptyView;
+ private boolean hideButtons = false;
+ private Boolean licenseValid;
+ private boolean showHeader = true;
+ private EntryAdapter entryAdapter;
+ private List<MusicDirectory.Entry> entries;
+
+ String id;
+ String name;
+ String playlistId;
+ String playlistName;
+ String podcastId;
+ String podcastName;
+ String podcastDescription;
+ String albumListType;
+ String albumListExtra;
+ int albumListSize;
+
+
+ public SelectDirectoryFragment() {
+ super();
+ rootId = getNewId();
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ if(bundle != null) {
+ int tmp = bundle.getInt(Constants.FRAGMENT_ID, -1);
+ if(tmp > 0) {
+ rootId = tmp;
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(Constants.FRAGMENT_ID, rootId);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_album, container, false);
+ rootView.setId(rootId);
+
+ entryList = (DragSortListView) rootView.findViewById(R.id.select_album_entries);
+ footer = LayoutInflater.from(context).inflate(R.layout.select_album_footer, entryList, false);
+ entryList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ entryList.setOnItemClickListener(this);
+ entryList.setDropListener(new DragSortListView.DropListener() {
+ @Override
+ public void drop(int from, int to) {
+ int max = entries.size();
+ if(to >= max) {
+ to = max - 1;
+ }
+ else if(to < 0) {
+ to = 0;
+ }
+ entries.add(to, entries.remove(from));
+ entryAdapter.notifyDataSetChanged();
+ }
+ });
+
+ emptyView = rootView.findViewById(R.id.select_album_empty);
+
+ registerForContextMenu(entryList);
+
+ Bundle args = getArguments();
+ if(args != null) {
+ id = args.getString(Constants.INTENT_EXTRA_NAME_ID);
+ name = args.getString(Constants.INTENT_EXTRA_NAME_NAME);
+ playlistId = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID);
+ playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME);
+ podcastId = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_ID);
+ podcastName = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_NAME);
+ podcastDescription = args.getString(Constants.INTENT_EXTRA_NAME_PODCAST_DESCRIPTION);
+ albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE);
+ albumListExtra = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA);
+ albumListSize = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
+ }
+ if(primaryFragment) {
+ load(false);
+ } else {
+ invalidated = true;
+ }
+ if(name != null) {
+ setTitle(name);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(com.actionbarsherlock.view.Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ if(licenseValid == null) {
+ menuInflater.inflate(R.menu.empty, menu);
+ }
+ else if(hideButtons) {
+ if(albumListType != null) {
+ menuInflater.inflate(R.menu.select_album_list, menu);
+ } else {
+ menuInflater.inflate(R.menu.select_album, menu);
+ }
+ } else {
+ if(podcastId == null) {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.select_song_offline, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.select_song, menu);
+
+ if(playlistId == null) {
+ menu.removeItem(R.id.menu_remove_playlist);
+ }
+ }
+ } else {
+ if(Util.isOffline(context)) {
+ menuInflater.inflate(R.menu.select_podcast_episode_offline, menu);
+ }
+ else {
+ menuInflater.inflate(R.menu.select_podcast_episode, menu);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_play_now:
+ playNow(false, false);
+ return true;
+ case R.id.menu_play_last:
+ playNow(false, true);
+ return true;
+ case R.id.menu_shuffle:
+ playNow(true, false);
+ return true;
+ case R.id.menu_select:
+ selectAllOrNone();
+ return true;
+ case R.id.menu_download:
+ downloadBackground(false);
+ selectAll(false, false);
+ return true;
+ case R.id.menu_cache:
+ downloadBackground(true);
+ selectAll(false, false);
+ return true;
+ case R.id.menu_delete:
+ delete();
+ selectAll(false, false);
+ return true;
+ case R.id.menu_add_playlist:
+ addToPlaylist(getSelectedSongs());
+ return true;
+ case R.id.menu_remove_playlist:
+ removeFromPlaylist(playlistId, playlistName, getSelectedIndexes());
+ return true;
+ }
+
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(info.position);
+ onCreateContextMenu(menu, view, menuInfo, entry);
+ if(!entry.isVideo() && !Util.isOffline(context) && playlistId == null && (podcastId == null || Util.isOffline(context) && podcastId != null)) {
+ menu.removeItem(R.id.song_menu_remove_playlist);
+ }
+ if(podcastId != null && !Util.isOffline(context)) {
+ String status = ((PodcastEpisode)entry).getStatus();
+ if("completed".equals(status)) {
+ menu.removeItem(R.id.song_menu_server_download);
+ }
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Object selectedItem = entries.get(showHeader ? (info.position - 1) : info.position);
+
+ if(onContextItemSelected(menuItem, selectedItem)) {
+ return true;
+ }
+
+ switch (menuItem.getItemId()) {
+ case R.id.song_menu_remove_playlist:
+ removeFromPlaylist(playlistId, playlistName, Arrays.<Integer>asList(info.position - 1));
+ break;
+ case R.id.song_menu_server_download:
+ downloadPodcastEpisode((PodcastEpisode)selectedItem);
+ break;
+ case R.id.song_menu_server_delete:
+ deletePodcastEpisode((PodcastEpisode)selectedItem);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (position >= 0) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
+ if (entry.isDirectory()) {
+ int fragId = rootId;
+ if(albumListType != null && entry.getParent() != null) {
+ SubsonicFragment parentFragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getParent());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getArtist());
+ parentFragment.setArguments(args);
+
+ replaceFragment(parentFragment, fragId);
+ fragId = parentFragment.getRootId();
+ }
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ID, entry.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.getTitle());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, fragId);
+ } else if (entry.isVideo()) {
+ playVideo(entry);
+ } else if(entry instanceof PodcastEpisode) {
+ String status = ((PodcastEpisode)entry).getStatus();
+ if("error".equals(status)) {
+ Util.toast(context, R.string.select_podcasts_error);
+ return;
+ } else if(!"completed".equals(status)) {
+ Util.toast(context, R.string.select_podcasts_skipped);
+ return;
+ }
+
+ getDownloadService().clear();
+ List<MusicDirectory.Entry> podcasts = new ArrayList<MusicDirectory.Entry>(1);
+ podcasts.add(entry);
+ getDownloadService().download(podcasts, false, true, true, false);
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ }
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ @Override
+ public int getRootId() {
+ return rootId;
+ }
+
+ private void load(boolean refresh) {
+ entryList.setVisibility(View.INVISIBLE);
+ emptyView.setVisibility(View.INVISIBLE);
+ if (playlistId != null) {
+ getPlaylist(playlistId, playlistName);
+ } else if(podcastId != null) {
+ getPodcast(podcastId, podcastName);
+ } else if (albumListType != null) {
+ getAlbumList(albumListType, albumListSize);
+ } else {
+ getMusicDirectory(id, name, refresh);
+ }
+ }
+
+ private void getMusicDirectory(final String id, final String name, final boolean refresh) {
+ setTitle(name);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getMusicDirectory(id, name, refresh, context, this);
+ }
+ }.execute();
+ }
+
+ private void getPlaylist(final String playlistId, final String playlistName) {
+ setTitle(playlistName);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPlaylist(playlistId, playlistName, context, this);
+ }
+ }.execute();
+ }
+
+ private void getPodcast(final String podcastId, final String podcastName) {
+ setTitle(podcastName);
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ return service.getPodcastEpisodes(podcastId, context, this);
+ }
+ }.execute();
+ }
+
+ 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)) {
+ setTitle(R.string.main_albums_random);
+ } else if ("highest".equals(albumListType)) {
+ setTitle(R.string.main_albums_highest);
+ } else if ("recent".equals(albumListType)) {
+ setTitle(R.string.main_albums_recent);
+ } else if ("frequent".equals(albumListType)) {
+ setTitle(R.string.main_albums_frequent);
+ } else if ("starred".equals(albumListType)) {
+ setTitle(R.string.main_albums_starred);
+ } else if("genres".equals(albumListType)) {
+ setTitle(albumListExtra);
+ }
+
+ new LoadTask() {
+ @Override
+ protected MusicDirectory load(MusicService service) throws Exception {
+ MusicDirectory result;
+ if ("starred".equals(albumListType)) {
+ result = service.getStarredList(context, this);
+ } else if("genres".equals(albumListType)) {
+ result = service.getSongsByGenre(albumListExtra, size, 0, context, this);
+ } else {
+ result = service.getAlbumList(albumListType, size, 0, context, this);
+ }
+ return result;
+ }
+ }.execute();
+ }
+
+ private abstract class LoadTask extends TabBackgroundTask<Pair<MusicDirectory, Boolean>> {
+
+ public LoadTask() {
+ super(SelectDirectoryFragment.this);
+ }
+
+ protected abstract MusicDirectory load(MusicService service) throws Exception;
+
+ @Override
+ protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory dir = load(musicService);
+ boolean valid = musicService.isLicenseValid(context, this);
+ return new Pair<MusicDirectory, Boolean>(dir, valid);
+ }
+
+ @Override
+ protected void done(Pair<MusicDirectory, Boolean> result) {
+ entries = result.getFirst().getChildren();
+
+ int songCount = 0;
+ for (MusicDirectory.Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ }
+ }
+
+ if (songCount > 0) {
+ if(showHeader) {
+ View header = createHeader(entries);
+ if(header != null) {
+ entryList.addHeaderView(header, null, false);
+ }
+ }
+ } else {
+ showHeader = false;
+ hideButtons = true;
+ }
+
+ emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
+ entryAdapter = new EntryAdapter(context, getImageLoader(), entries, (podcastId == null) ? true : false);
+ if(albumListType == null || "starred".equals(albumListType)) {
+ entryList.setAdapter(entryAdapter);
+ } else {
+ entryList.setAdapter(new AlbumListAdapter(context, entryAdapter, albumListType, albumListExtra, albumListSize));
+ }
+ entryList.setVisibility(View.VISIBLE);
+ licenseValid = result.getSecond();
+ context.invalidateOptionsMenu();
+
+ Bundle args = getArguments();
+ boolean playAll = args.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
+ if (playAll && songCount > 0) {
+ playAll(args.getBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, false), false);
+ }
+ }
+ }
+
+ private void playNow(final boolean shuffle, final boolean append) {
+ if(getSelectedSongs().size() > 0) {
+ download(append, false, !append, false, shuffle);
+ selectAll(false, false);
+ }
+ else {
+ playAll(shuffle, append);
+ }
+ }
+ private void playAll(final boolean shuffle, final boolean append) {
+ boolean hasSubFolders = false;
+ for (int i = 0; i < entryList.getCount(); i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && entry.isDirectory()) {
+ hasSubFolders = true;
+ break;
+ }
+ }
+
+ if (hasSubFolders && id != null) {
+ downloadRecursively(id, false, append, !append, shuffle, false);
+ } else {
+ selectAll(true, false);
+ download(append, false, !append, false, shuffle);
+ selectAll(false, false);
+ }
+ }
+
+ private void selectAllOrNone() {
+ boolean someUnselected = false;
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (!entryList.isItemChecked(i) && entryList.getItemAtPosition(i) instanceof MusicDirectory.Entry) {
+ someUnselected = true;
+ break;
+ }
+ }
+ selectAll(someUnselected, true);
+ }
+
+ private void selectAll(boolean selected, boolean toast) {
+ int count = entryList.getCount();
+ int selectedCount = 0;
+ for (int i = 0; i < count; i++) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) entryList.getItemAtPosition(i);
+ if (entry != null && !entry.isDirectory() && !entry.isVideo()) {
+ entryList.setItemChecked(i, selected);
+ selectedCount++;
+ }
+ }
+
+ // Display toast: N tracks selected / N tracks unselected
+ if (toast) {
+ int toastResId = selected ? R.string.select_album_n_selected
+ : R.string.select_album_n_unselected;
+ Util.toast(context, context.getString(toastResId, selectedCount));
+ }
+ }
+
+ private List<MusicDirectory.Entry> getSelectedSongs() {
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ songs.add((MusicDirectory.Entry) entryList.getItemAtPosition(i));
+ }
+ }
+ return songs;
+ }
+
+ private List<Integer> getSelectedIndexes() {
+ List<Integer> indexes = new ArrayList<Integer>();
+
+ int count = entryList.getCount();
+ for (int i = 0; i < count; i++) {
+ if (entryList.isItemChecked(i)) {
+ indexes.add(i - 1);
+ }
+ }
+
+ return indexes;
+ }
+
+ private void download(final boolean append, final boolean save, final boolean autoplay, final boolean playNext, final boolean shuffle) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ final List<MusicDirectory.Entry> songs = getSelectedSongs();
+ Runnable onValid = new Runnable() {
+ @Override
+ public void run() {
+ if (!append) {
+ getDownloadService().clear();
+ }
+
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().download(songs, save, autoplay, playNext, shuffle);
+ if (playlistName != null) {
+ getDownloadService().setSuggestedPlaylistName(playlistName, playlistId);
+ } else {
+ getDownloadService().setSuggestedPlaylistName(null, null);
+ }
+ if (autoplay) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ if(context instanceof SearchActivity) {
+ context.finish();
+ }
+ } else if (save) {
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ } else if (append) {
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_added, songs.size(), songs.size()));
+ }
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+ private void downloadBackground(final boolean save) {
+ List<MusicDirectory.Entry> songs = getSelectedSongs();
+ if(songs.isEmpty()) {
+ selectAll(true, false);
+ songs = getSelectedSongs();
+ }
+ downloadBackground(save, songs);
+ }
+ private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) {
+ if (getDownloadService() == null) {
+ return;
+ }
+
+ Runnable onValid = new Runnable() {
+ @Override
+ public void run() {
+ warnIfNetworkOrStorageUnavailable();
+ getDownloadService().downloadBackground(songs, save);
+
+ Util.toast(context,
+ context.getResources().getQuantityString(R.plurals.select_album_n_songs_downloading, songs.size(), songs.size()));
+ }
+ };
+
+ checkLicenseAndTrialPeriod(onValid);
+ }
+
+ private void delete() {
+ List<MusicDirectory.Entry> songs = getSelectedSongs();
+ if(songs.isEmpty()) {
+ selectAll(true, false);
+ songs = getSelectedSongs();
+ }
+ if (getDownloadService() != null) {
+ getDownloadService().delete(songs);
+ }
+ }
+
+ public void removeFromPlaylist(final String id, final String name, final List<Integer> indexes) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.removeFromPlaylist(id, indexes, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ for(int i = indexes.size() - 1; i >= 0; i--) {
+ entryList.setItemChecked(indexes.get(i) + 1, false);
+ entryAdapter.removeAt(indexes.get(i));
+ }
+ entryAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.removed_playlist, indexes.size(), name));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.updated_playlist_error, name) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void downloadPodcastEpisode(final PodcastEpisode episode) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.downloadPodcastEpisode(episode.getEpisodeId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_downloading, episode.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ public void deletePodcastEpisode(final PodcastEpisode episode) {
+ Util.confirmDialog(context, R.string.common_delete, episode.getTitle(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, true) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePodcastEpisode(episode.getEpisodeId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ entries.remove(episode);
+ entryAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void checkLicenseAndTrialPeriod(Runnable onValid) {
+ if (licenseValid) {
+ onValid.run();
+ return;
+ }
+
+ int trialDaysLeft = Util.getRemainingTrialDays(context);
+ Log.i(TAG, trialDaysLeft + " trial days left.");
+
+ if (trialDaysLeft == 0) {
+ showDonationDialog(trialDaysLeft, null);
+ } else if (trialDaysLeft < Constants.FREE_TRIAL_DAYS / 2) {
+ showDonationDialog(trialDaysLeft, onValid);
+ } else {
+ Util.toast(context, context.getResources().getString(R.string.select_album_not_licensed, trialDaysLeft));
+ onValid.run();
+ }
+ }
+
+ private void showDonationDialog(int trialDaysLeft, final Runnable onValid) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+
+ if (trialDaysLeft == 0) {
+ builder.setTitle(R.string.select_album_donate_dialog_0_trial_days_left);
+ } else {
+ builder.setTitle(context.getResources().getQuantityString(R.plurals.select_album_donate_dialog_n_trial_days_left,
+ trialDaysLeft, trialDaysLeft));
+ }
+
+ builder.setMessage(R.string.select_album_donate_dialog_message);
+
+ builder.setPositiveButton(R.string.select_album_donate_dialog_now,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.DONATION_URL)));
+ }
+ });
+
+ builder.setNegativeButton(R.string.select_album_donate_dialog_later,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dialogInterface.dismiss();
+ if (onValid != null) {
+ onValid.run();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+
+ private View createHeader(List<MusicDirectory.Entry> entries) {
+ View header = entryList.findViewById(R.id.select_album_header);
+ boolean add = false;
+ if(header == null) {
+ header = LayoutInflater.from(context).inflate(R.layout.select_album_header, entryList, false);
+ add = true;
+ }
+
+ View coverArtView = header.findViewById(R.id.select_album_art);
+ getImageLoader().loadImage(coverArtView, entries.get(random.nextInt(entries.size())), false, true);
+
+ TextView titleView = (TextView) header.findViewById(R.id.select_album_title);
+ if(playlistName != null) {
+ titleView.setText(playlistName);
+ } else if(podcastName != null) {
+ titleView.setText(podcastName);
+ titleView.setPadding(0, 6, 4, 8);
+ } else if(name != null) {
+ titleView.setText(name);
+ }
+
+ int songCount = 0;
+
+ Set<String> artists = new HashSet<String>();
+ Integer totalDuration = 0;
+ for (MusicDirectory.Entry entry : entries) {
+ if (!entry.isDirectory()) {
+ songCount++;
+ if (entry.getArtist() != null) {
+ artists.add(entry.getArtist());
+ }
+ Integer duration = entry.getDuration();
+ if(duration != null) {
+ totalDuration += duration;
+ }
+ }
+ }
+
+ TextView artistView = (TextView) header.findViewById(R.id.select_album_artist);
+ if(podcastDescription != null) {
+ artistView.setText(podcastDescription);
+ artistView.setSingleLine(false);
+ artistView.setLines(5);
+ } else if (artists.size() == 1) {
+ artistView.setText(artists.iterator().next());
+ artistView.setVisibility(View.VISIBLE);
+ } else {
+ artistView.setVisibility(View.GONE);
+ }
+
+ 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) {
+ String s = context.getResources().getQuantityString(R.plurals.select_album_n_songs, songCount, songCount);
+ songCountView.setText(s.toUpperCase());
+ songLengthView.setText(Util.formatDuration(totalDuration));
+ } else {
+ songCountView.setVisibility(View.GONE);
+ songLengthView.setVisibility(View.GONE);
+ }
+
+ if(add) {
+ return header;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectGenreFragment.java b/src/github/daneren2005/dsub/fragments/SelectGenreFragment.java new file mode 100644 index 00000000..623aba4e --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SelectGenreFragment.java @@ -0,0 +1,142 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.view.GenreAdapter;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.MenuInflater;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SelectGenreFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectGenreFragment.class.getSimpleName();
+ private ListView genreListView;
+ private View emptyView;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_genres, container, false);
+
+ genreListView = (ListView)rootView.findViewById(R.id.select_genre_list);
+ genreListView.setOnItemClickListener(this);
+ emptyView = rootView.findViewById(R.id.select_genre_empty);
+ refresh();
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_genres, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void setPrimaryFragment(boolean primary) {
+ super.setPrimaryFragment(primary);
+ if(rootView != null) {
+ if(primary) {
+ ((ViewGroup)rootView).getChildAt(0).setVisibility(View.VISIBLE);
+ } else {
+ ((ViewGroup)rootView).getChildAt(0).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private void load(final boolean refresh) {
+ setTitle(R.string.main_albums_genres);
+ genreListView.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<List<Genre>> task = new TabBackgroundTask<List<Genre>>(this) {
+ @Override
+ protected List<Genre> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ List<Genre> genres = new ArrayList<Genre>();
+
+ try {
+ genres = musicService.getGenres(refresh, context, this);
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load genres", x);
+ }
+
+ return genres;
+ }
+
+ @Override
+ protected void done(List<Genre> result) {
+ emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
+
+ if (result != null) {
+ genreListView.setAdapter(new GenreAdapter(context, result));
+ genreListView.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Genre genre = (Genre) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, "genres");
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 20);
+ args.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
+ args.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA, genre.getName());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_genre_layout);
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java new file mode 100644 index 00000000..de74cdb2 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SelectPlaylistFragment.java @@ -0,0 +1,286 @@ +package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.CacheCleaner;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.PlaylistAdapter;
+import java.util.List;
+
+public class SelectPlaylistFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectPlaylistFragment.class.getSimpleName();
+
+ private ListView list;
+ private View emptyTextView;
+ private PlaylistAdapter playlistAdapter;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_playlist, container, false);
+
+ list = (ListView) rootView.findViewById(R.id.select_playlist_list);
+ emptyTextView = rootView.findViewById(R.id.select_playlist_empty);
+ list.setOnItemClickListener(this);
+ registerForContextMenu(list);
+ if(!primaryFragment) {
+ invalidated = true;
+ } else {
+ refresh(false);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(com.actionbarsherlock.view.Menu menu, com.actionbarsherlock.view.MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_playlist, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ MenuInflater inflater = context.getMenuInflater();
+ if (Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_playlist_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_playlist_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ Playlist playlist = (Playlist) list.getItemAtPosition(info.position);
+
+ SubsonicFragment fragment;
+ Bundle args;
+ FragmentTransaction trans;
+ switch (menuItem.getItemId()) {
+ case R.id.playlist_menu_download:
+ downloadPlaylist(playlist.getId(), playlist.getName(), false, true, false, false, true);
+ break;
+ case R.id.playlist_menu_pin:
+ downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true);
+ break;
+ case R.id.playlist_menu_play_now:
+ fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_playlist_layout);
+ break;
+ case R.id.playlist_menu_play_shuffled:
+ fragment = new SelectDirectoryFragment();
+ args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ args.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_playlist_layout);
+ break;
+ case R.id.playlist_menu_delete:
+ deletePlaylist(playlist);
+ break;
+ case R.id.playlist_info:
+ displayPlaylistInfo(playlist);
+ break;
+ case R.id.playlist_update_info:
+ updatePlaylistInfo(playlist);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ Playlist playlist = (Playlist) parent.getItemAtPosition(position);
+
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_playlist_layout);
+ }
+
+ @Override
+ protected void refresh(boolean refresh) {
+ load(refresh);
+ }
+
+ private void load(final boolean refresh) {
+ setTitle(R.string.playlist_label);
+ list.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<List<Playlist>> task = new TabBackgroundTask<List<Playlist>>(this) {
+ @Override
+ protected List<Playlist> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ List<Playlist> playlists = musicService.getPlaylists(refresh, context, this);
+ if(!Util.isOffline(context) && refresh) {
+ new CacheCleaner(context, getDownloadService()).cleanPlaylists(playlists);
+ }
+ return playlists;
+ }
+
+ @Override
+ protected void done(List<Playlist> result) {
+ list.setAdapter(playlistAdapter = new PlaylistAdapter(context, result));
+ emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
+ list.setVisibility(View.VISIBLE);
+ }
+ };
+ task.execute();
+ }
+
+ private void deletePlaylist(final Playlist playlist) {
+ Util.confirmDialog(context, R.string.common_delete, playlist.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePlaylist(playlist.getId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ playlistAdapter.remove(playlist);
+ playlistAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ private void displayPlaylistInfo(final Playlist playlist) {
+ String message = "Owner: " + playlist.getOwner() + "\nComments: " +
+ ((playlist.getComment() == null) ? "" : playlist.getComment()) +
+ "\nSong Count: " + playlist.getSongCount() +
+ ((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic())) +
+ "\nCreation Date: " + playlist.getCreated().replace('T', ' ');
+ Util.info(context, playlist.getName(), message);
+ }
+
+ private void updatePlaylistInfo(final Playlist playlist) {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.update_playlist, null);
+ final EditText nameBox = (EditText)dialogView.findViewById(R.id.get_playlist_name);
+ final EditText commentBox = (EditText)dialogView.findViewById(R.id.get_playlist_comment);
+ final CheckBox publicBox = (CheckBox)dialogView.findViewById(R.id.get_playlist_public);
+
+ nameBox.setText(playlist.getName());
+ commentBox.setText(playlist.getComment());
+ Boolean pub = playlist.getPublic();
+ if(pub == null) {
+ publicBox.setEnabled(false);
+ } else {
+ publicBox.setChecked(pub);
+ }
+
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.playlist_update_info)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.updatePlaylist(playlist.getId(), nameBox.getText().toString(), commentBox.getText().toString(), publicBox.isChecked(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ refresh();
+ Util.toast(context, context.getResources().getString(R.string.playlist_updated_info, playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_updated_info_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ })
+ .setNegativeButton(R.string.common_cancel, null)
+ .show();
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java b/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java new file mode 100644 index 00000000..f0f78569 --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SelectPodcastsFragment.java @@ -0,0 +1,310 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.BackgroundTask;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.TabBackgroundTask;
+import github.daneren2005.dsub.util.Util;
+import github.daneren2005.dsub.view.PodcastChannelAdapter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Scott
+ */
+public class SelectPodcastsFragment extends SubsonicFragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = SelectPodcastsFragment.class.getSimpleName();
+ private ListView podcastListView;
+ private PodcastChannelAdapter podcastAdapter;
+ private View emptyView;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ rootView = inflater.inflate(R.layout.select_podcasts, container, false);
+
+ podcastListView = (ListView)rootView.findViewById(R.id.select_podcasts_list);
+ podcastListView.setOnItemClickListener(this);
+ registerForContextMenu(podcastListView);
+ emptyView = rootView.findViewById(R.id.select_podcasts_empty);
+ if(!primaryFragment) {
+ invalidated = true;
+ } else {
+ refresh(false);
+ }
+
+ return rootView;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.select_podcasts, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ if(super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.menu_check:
+ refreshPodcasts();
+ break;
+ case R.id.menu_add_podcast:
+ addNewPodcast();
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+ if(!Util.isOffline(context)) {
+ android.view.MenuInflater inflater = context.getMenuInflater();
+ inflater.inflate(R.menu.select_podcasts_context, menu);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ if(!primaryFragment) {
+ return false;
+ }
+
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+ PodcastChannel channel = (PodcastChannel) podcastListView.getItemAtPosition(info.position);
+
+ switch (menuItem.getItemId()) {
+ case R.id.podcast_channel_info:
+ displayPodcastInfo(channel);
+ break;
+ case R.id.podcast_channel_delete:
+ deletePodcast(channel);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void refresh(final boolean refresh) {
+ setTitle(R.string.button_bar_podcasts);
+ podcastListView.setVisibility(View.INVISIBLE);
+
+ BackgroundTask<List<PodcastChannel>> task = new TabBackgroundTask<List<PodcastChannel>>(this) {
+ @Override
+ protected List<PodcastChannel> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+
+ try {
+ channels = musicService.getPodcastChannels(refresh, context, this);
+ } catch (Exception x) {
+ Log.e(TAG, "Failed to load podcasts", x);
+ }
+
+ return channels;
+ }
+
+ @Override
+ protected void done(List<PodcastChannel> result) {
+ emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
+
+ if (result != null) {
+ podcastListView.setAdapter(podcastAdapter = new PodcastChannelAdapter(context, result));
+ podcastListView.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ task.execute();
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ PodcastChannel channel = (PodcastChannel) parent.getItemAtPosition(position);
+
+ if("error".equals(channel.getStatus())) {
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_invalid_podcast_channel, channel.getErrorMessage() == null ? "error" : channel.getErrorMessage()));
+ } else if("downloading".equals(channel.getStatus())) {
+ Util.toast(context, R.string.select_podcasts_initializing);
+ } else {
+ SubsonicFragment fragment = new SelectDirectoryFragment();
+ Bundle args = new Bundle();
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_ID, channel.getId());
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_NAME, channel.getName());
+ args.putString(Constants.INTENT_EXTRA_NAME_PODCAST_DESCRIPTION, channel.getDescription());
+ fragment.setArguments(args);
+
+ replaceFragment(fragment, R.id.select_podcasts_layout);
+ }
+ }
+
+ public void refreshPodcasts() {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.refreshPodcasts(context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.select_podcasts_refreshing);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ Util.toast(context, getErrorMessage(error), false);
+ }
+ }.execute();
+ }
+
+ private void addNewPodcast() {
+ View dialogView = context.getLayoutInflater().inflate(R.layout.create_podcast, null);
+ final TextView urlBox = (TextView) dialogView.findViewById(R.id.create_podcast_url);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_add_podcast)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ addNewPodcast(urlBox.getText().toString());
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ })
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void addNewPodcast(final String url) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createPodcastChannel(url, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ refresh();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.select_podcasts_created_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void displayPodcastInfo(final PodcastChannel channel) {
+ String message = ((channel.getName()) == null ? "" : "Title: " + channel.getName()) +
+ "\nURL: " + channel.getUrl() +
+ "\nStatus: " + channel.getStatus() +
+ ((channel.getErrorMessage()) == null ? "" : "\nError Message: " + channel.getErrorMessage()) +
+ ((channel.getDescription()) == null ? "" : "\nDescription: " + channel.getDescription());
+
+ Util.info(context, channel.getName(), message);
+ }
+
+ private void deletePodcast(final PodcastChannel channel) {
+ Util.confirmDialog(context, R.string.common_delete, channel.getName(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new LoadingTask<Void>(context, false) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deletePodcastChannel(channel.getId(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ podcastAdapter.remove(channel);
+ podcastAdapter.notifyDataSetChanged();
+ Util.toast(context, context.getResources().getString(R.string.select_podcasts_deleted, channel.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.select_podcasts_deleted_error, channel.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ }
+}
diff --git a/src/github/daneren2005/dsub/fragments/SubsonicFragment.java b/src/github/daneren2005/dsub/fragments/SubsonicFragment.java new file mode 100644 index 00000000..9e8ec29c --- /dev/null +++ b/src/github/daneren2005/dsub/fragments/SubsonicFragment.java @@ -0,0 +1,968 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+import com.actionbarsherlock.app.SherlockFragment;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.DownloadActivity;
+import github.daneren2005.dsub.activity.HelpActivity;
+import github.daneren2005.dsub.activity.MainActivity;
+import github.daneren2005.dsub.activity.SearchActivity;
+import github.daneren2005.dsub.activity.SettingsActivity;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+import github.daneren2005.dsub.util.LoadingTask;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+
+public class SubsonicFragment extends SherlockFragment {
+ private static final String TAG = SubsonicFragment.class.getSimpleName();
+ private static int internalID = Integer.MAX_VALUE;
+ private static int TAG_INC = 10;
+ private int tag;
+
+ protected SubsonicActivity context;
+ protected CharSequence title = "DSub";
+ protected CharSequence subtitle = null;
+ protected View rootView;
+ protected boolean primaryFragment = false;
+ protected boolean invalidated = false;
+ protected static Random random = new Random();
+
+ public SubsonicFragment() {
+ super();
+ tag = TAG_INC++;
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ context = (SubsonicActivity)activity;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_refresh:
+ refresh(true);
+ return true;
+ case R.id.menu_shuffle:
+ onShuffleRequested();
+ return true;
+ case R.id.menu_search:
+ context.onSearchRequested();
+ return true;
+ case R.id.menu_exit:
+ exit();
+ return true;
+ case R.id.menu_settings:
+ startActivity(new Intent(context, SettingsActivity.class));
+ return true;
+ case R.id.menu_help:
+ startActivity(new Intent(context, HelpActivity.class));
+ return true;
+ }
+
+ return false;
+ }
+
+ public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo, Object selected) {
+ MenuInflater inflater = context.getMenuInflater();
+
+ if(selected instanceof MusicDirectory.Entry) {
+ MusicDirectory.Entry entry = (MusicDirectory.Entry) selected;
+ if(entry instanceof PodcastEpisode && !entry.isVideo()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_podcast_episode_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_podcast_episode_context, menu);
+ }
+ }
+ else if (entry.isDirectory()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_album_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_album_context, menu);
+ }
+ menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ } else if(!entry.isVideo()) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_song_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_song_context, menu);
+ }
+ menu.findItem(entry.isDirectory() ? R.id.album_menu_star : R.id.song_menu_star).setTitle(entry.isStarred() ? R.string.common_unstar : R.string.common_star);
+ } else {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_video_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_video_context, menu);
+ }
+ }
+ } else if(selected instanceof Artist) {
+ if(Util.isOffline(context)) {
+ inflater.inflate(R.menu.select_artist_context_offline, menu);
+ }
+ else {
+ inflater.inflate(R.menu.select_artist_context, menu);
+ }
+ }
+ }
+
+ public boolean onContextItemSelected(MenuItem menuItem, Object selectedItem) {
+ Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
+ MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
+ List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
+ songs.add(entry);
+
+ switch (menuItem.getItemId()) {
+ case R.id.artist_menu_play_now:
+ downloadRecursively(artist.getId(), false, false, true, false, false);
+ break;
+ case R.id.artist_menu_play_shuffled:
+ downloadRecursively(artist.getId(), false, false, true, true, false);
+ break;
+ case R.id.artist_menu_play_last:
+ downloadRecursively(artist.getId(), false, true, false, false, false);
+ break;
+ case R.id.artist_menu_download:
+ downloadRecursively(artist.getId(), false, true, false, false, true);
+ break;
+ case R.id.artist_menu_pin:
+ downloadRecursively(artist.getId(), true, true, false, false, true);
+ break;
+ case R.id.artist_menu_delete:
+ deleteRecursively(artist);
+ break;
+ case R.id.album_menu_play_now:
+ downloadRecursively(entry.getId(), false, false, true, false, false);
+ break;
+ case R.id.album_menu_play_shuffled:
+ downloadRecursively(entry.getId(), false, false, true, true, false);
+ break;
+ case R.id.album_menu_play_last:
+ downloadRecursively(entry.getId(), false, true, false, false, false);
+ break;
+ case R.id.album_menu_download:
+ downloadRecursively(entry.getId(), false, true, false, false, true);
+ break;
+ case R.id.album_menu_pin:
+ downloadRecursively(entry.getId(), true, true, false, false, true);
+ break;
+ case R.id.album_menu_star:
+ toggleStarred(entry);
+ break;
+ case R.id.album_menu_delete:
+ deleteRecursively(entry);
+ break;
+ case R.id.song_menu_play_now:
+ getDownloadService().clear();
+ getDownloadService().download(songs, false, true, true, false);
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ if(context instanceof SearchActivity) {
+ context.finish();
+ }
+ break;
+ case R.id.song_menu_play_next:
+ getDownloadService().download(songs, false, false, true, false);
+ break;
+ case R.id.song_menu_play_last:
+ getDownloadService().download(songs, false, false, false, false);
+ break;
+ case R.id.song_menu_download:
+ getDownloadService().downloadBackground(songs, false);
+ break;
+ case R.id.song_menu_pin:
+ getDownloadService().downloadBackground(songs, true);
+ break;
+ case R.id.song_menu_delete:
+ getDownloadService().delete(songs);
+ break;
+ case R.id.song_menu_add_playlist:
+ addToPlaylist(songs);
+ break;
+ case R.id.song_menu_star:
+ toggleStarred(entry);
+ break;
+ case R.id.song_menu_play_external:
+ playExternalPlayer(entry);
+ break;
+ case R.id.song_menu_info:
+ displaySongInfo(entry);
+ break;
+ case R.id.song_menu_stream_external:
+ streamExternalPlayer(entry);
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ public void replaceFragment(SubsonicFragment fragment, int id) {
+ context.replaceFragment(fragment, id, fragment.getSupportTag());
+ }
+
+ protected int getNewId() {
+ internalID--;
+ return internalID;
+ }
+ public int getRootId() {
+ return rootView.getId();
+ }
+
+ public int getSupportTag() {
+ return tag;
+ }
+
+ public void setPrimaryFragment(boolean primary) {
+ primaryFragment = primary;
+ if(primary) {
+ if(context != null) {
+ context.setTitle(title);
+ context.setSubtitle(subtitle);
+ }
+ if(invalidated) {
+ invalidated = false;
+ refresh(false);
+ }
+ }
+ }
+
+ public void invalidate() {
+ if(primaryFragment) {
+ refresh(false);
+ } else {
+ invalidated = true;
+ }
+ }
+
+ public DownloadService getDownloadService() {
+ return context != null ? context.getDownloadService() : null;
+ }
+
+ protected void refresh() {
+ refresh(true);
+ }
+ protected void refresh(boolean refresh) {
+
+ }
+
+ protected void exit() {
+ if(context.getClass() != MainActivity.class) {
+ Intent intent = new Intent(context, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
+ Util.startActivityWithoutTransition(context, intent);
+ } else {
+ context.stopService(new Intent(context, DownloadServiceImpl.class));
+ context.finish();
+ }
+ }
+
+ public void setProgressVisible(boolean visible) {
+ View view = rootView.findViewById(R.id.tab_progress);
+ if (view != null) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public void updateProgress(String message) {
+ TextView view = (TextView) rootView.findViewById(R.id.tab_progress_message);
+ if (view != null) {
+ view.setText(message);
+ }
+ }
+
+ protected synchronized ImageLoader getImageLoader() {
+ return context.getImageLoader();
+ }
+ public synchronized static ImageLoader getStaticImageLoader(Context context) {
+ return SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ public void setTitle(CharSequence title) {
+ this.title = title;
+ context.setTitle(title);
+ }
+ public void setTitle(int title) {
+ this.title = context.getResources().getString(title);
+ context.setTitle(this.title);
+ }
+ public void setSubtitle(CharSequence title) {
+ this.subtitle = title;
+ context.setSubtitle(title);
+ }
+ public CharSequence getTitle() {
+ return this.title;
+ }
+
+ protected void warnIfNetworkOrStorageUnavailable() {
+ 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);
+ }
+ }
+
+ protected void onShuffleRequested() {
+ if(Util.isOffline(context)) {
+ Intent intent = new Intent(context, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ Util.startActivityWithoutTransition(context, intent);
+ return;
+ }
+
+ View dialogView = context.getLayoutInflater().inflate(R.layout.shuffle_dialog, null);
+ final EditText startYearBox = (EditText)dialogView.findViewById(R.id.start_year);
+ final EditText endYearBox = (EditText)dialogView.findViewById(R.id.end_year);
+ final EditText genreBox = (EditText)dialogView.findViewById(R.id.genre);
+ final Button genreCombo = (Button)dialogView.findViewById(R.id.genre_combo);
+
+ final SharedPreferences prefs = Util.getPreferences(context);
+ final String oldStartYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "");
+ final String oldEndYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "");
+ final String oldGenre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "");
+
+ boolean _useCombo = false;
+ if(Util.checkServerVersion(context, "1.9.0")) {
+ genreBox.setVisibility(View.GONE);
+ genreCombo.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ new LoadingTask<List<Genre>>(context, true) {
+ @Override
+ protected List<Genre> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getGenres(false, context, this);
+ }
+
+ @Override
+ protected void done(final List<Genre> genres) {
+ List<String> names = new ArrayList<String>();
+ String blank = context.getResources().getString(R.string.select_genre_blank);
+ names.add(blank);
+ for(Genre genre: genres) {
+ names.add(genre.getName());
+ }
+ final List<String> finalNames = names;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_pick_genre)
+ .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if(which == 0) {
+ genreCombo.setText("");
+ } else {
+ genreCombo.setText(finalNames.get(which));
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ });
+ _useCombo = true;
+ } else {
+ genreCombo.setVisibility(View.GONE);
+ }
+ final boolean useCombo = _useCombo;
+
+ startYearBox.setText(oldStartYear);
+ endYearBox.setText(oldEndYear);
+ genreBox.setText(oldGenre);
+ genreCombo.setText(oldGenre);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.shuffle_title)
+ .setView(dialogView)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intent = new Intent(context, DownloadActivity.class);
+ intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
+ String genre;
+ if(useCombo) {
+ genre = genreCombo.getText().toString();
+ } else {
+ genre = genreBox.getText().toString();
+ }
+ String startYear = startYearBox.getText().toString();
+ String endYear = endYearBox.getText().toString();
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, startYear);
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, endYear);
+ editor.putString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, genre);
+ editor.commit();
+
+ Util.startActivityWithoutTransition(context, intent);
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ public void toggleStarred(final MusicDirectory.Entry entry) {
+ final boolean starred = !entry.isStarred();
+ entry.setStarred(starred);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.setStarred(entry.getId(), starred, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // UpdateView
+ Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getTitle()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ entry.setStarred(!starred);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, entry.getTitle()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+ public void toggleStarred(final Artist entry) {
+ final boolean starred = !entry.isStarred();
+ entry.setStarred(starred);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.setStarred(entry.getId(), starred, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ // UpdateView
+ Util.toast(context, context.getResources().getString(starred ? R.string.starring_content_starred : R.string.starring_content_unstarred, entry.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ entry.setStarred(!starred);
+
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.starring_content_error, entry.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected void downloadRecursively(final String id, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, "", true, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadPlaylist(final String id, final String name, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ downloadRecursively(id, name, false, save, append, autoplay, shuffle, background);
+ }
+ protected void downloadRecursively(final String id, final String name, final boolean isDirectory, final boolean save, final boolean append, final boolean autoplay, final boolean shuffle, final boolean background) {
+ LoadingTask<List<MusicDirectory.Entry>> task = new LoadingTask<List<MusicDirectory.Entry>>(context) {
+ private static final int MAX_SONGS = 500;
+
+ @Override
+ protected List<MusicDirectory.Entry> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory root;
+ if(isDirectory)
+ root = musicService.getMusicDirectory(id, name, false, context, this);
+ else
+ root = musicService.getPlaylist(id, name, context, this);
+ List<MusicDirectory.Entry> songs = new LinkedList<MusicDirectory.Entry>();
+ getSongsRecursively(root, songs);
+ return songs;
+ }
+
+ private void getSongsRecursively(MusicDirectory parent, List<MusicDirectory.Entry> songs) throws Exception {
+ if (songs.size() > MAX_SONGS) {
+ return;
+ }
+
+ for (MusicDirectory.Entry song : parent.getChildren(false, true)) {
+ if (!song.isVideo()) {
+ songs.add(song);
+ }
+ }
+ for (MusicDirectory.Entry dir : parent.getChildren(true, false)) {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ getSongsRecursively(musicService.getMusicDirectory(dir.getId(), dir.getTitle(), false, context, this), songs);
+ }
+ }
+
+ @Override
+ protected void done(List<MusicDirectory.Entry> songs) {
+ DownloadService downloadService = getDownloadService();
+ if (!songs.isEmpty() && downloadService != null) {
+ if (!append) {
+ downloadService.clear();
+ }
+ warnIfNetworkOrStorageUnavailable();
+ if(!background) {
+ downloadService.download(songs, save, autoplay, false, shuffle);
+ if(!append) {
+ Util.startActivityWithoutTransition(context, DownloadActivity.class);
+ if(context instanceof SearchActivity) {
+ context.finish();
+ }
+ }
+ }
+ else {
+ downloadService.downloadBackground(songs, save);
+ }
+ }
+ }
+ };
+
+ task.execute();
+ }
+
+ protected void addToPlaylist(final List<MusicDirectory.Entry> songs) {
+ if(songs.isEmpty()) {
+ Util.toast(context, "No songs selected");
+ return;
+ }
+
+ new LoadingTask<List<Playlist>>(context, true) {
+ @Override
+ protected List<Playlist> doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ return musicService.getPlaylists(false, context, this);
+ }
+
+ @Override
+ protected void done(final List<Playlist> playlists) {
+ List<String> names = new ArrayList<String>();
+ String createNew = context.getResources().getString(R.string.playlist_create_new);
+ names.add(createNew);
+ for(Playlist playlist: playlists) {
+ names.add(playlist.getName());
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.playlist_add_to)
+ .setItems(names.toArray(new CharSequence[names.size()]), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+
+ if(which > 0) {
+ addToPlaylist(playlists.get(which - 1), songs);
+ } else {
+ createNewPlaylist(songs, false);
+ }
+ }
+ });
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ private void addToPlaylist(final Playlist playlist, final List<MusicDirectory.Entry> songs) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.addToPlaylist(playlist.getId(), songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, context.getResources().getString(R.string.updated_playlist, songs.size(), playlist.getName()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.updated_playlist_error, playlist.getName()) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ protected void createNewPlaylist(final List<MusicDirectory.Entry> songs, boolean getSuggestion) {
+ View layout = context.getLayoutInflater().inflate(R.layout.save_playlist, null);
+ final EditText playlistNameView = (EditText) layout.findViewById(R.id.save_playlist_name);
+ final CheckBox overwriteCheckBox = (CheckBox) layout.findViewById(R.id.save_playlist_overwrite);
+ if(getSuggestion) {
+ String playlistName = (getDownloadService() != null) ? getDownloadService().getSuggestedPlaylistName() : null;
+ if (playlistName != null) {
+ playlistNameView.setText(playlistName);
+ try {
+ if(Util.checkServerVersion(context, "1.8.0") && Integer.parseInt(getDownloadService().getSuggestedPlaylistId()) != -1) {
+ overwriteCheckBox.setChecked(true);
+ overwriteCheckBox.setVisibility(View.VISIBLE);
+ }
+ } catch(Exception e) {
+ Log.d(TAG, "Playlist id isn't a integer, probably MusicCabinet");
+ }
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+ } else {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ playlistNameView.setText(dateFormat.format(new Date()));
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.download_playlist_title)
+ .setMessage(R.string.download_playlist_name)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if(overwriteCheckBox.isChecked()) {
+ overwritePlaylist(songs, String.valueOf(playlistNameView.getText()), getDownloadService().getSuggestedPlaylistId());
+ } else {
+ createNewPlaylist(songs, String.valueOf(playlistNameView.getText()));
+ }
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ })
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ private void createNewPlaylist(final List<MusicDirectory.Entry> songs, final String name) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createPlaylist(null, name, songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ private void overwritePlaylist(final List<MusicDirectory.Entry> songs, final String name, final String id) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ MusicDirectory playlist = musicService.getPlaylist(id, name, context, null);
+ List<MusicDirectory.Entry> toDelete = playlist.getChildren();
+ musicService.overwritePlaylist(id, name, toDelete.size(), songs, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ Util.toast(context, R.string.download_playlist_done);
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.download_playlist_error) + " " + getErrorMessage(error);
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }.execute();
+ }
+
+ public void displaySongInfo(final MusicDirectory.Entry song) {
+ Integer bitrate = null;
+ String format = null;
+ long size = 0;
+ try {
+ DownloadFile downloadFile = new DownloadFile(context, song, false);
+ File file = downloadFile.getCompleteFile();
+ if(file.exists()) {
+ MediaMetadataRetriever metadata = new MediaMetadataRetriever();
+ metadata.setDataSource(file.getAbsolutePath());
+ String tmp = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
+ bitrate = Integer.parseInt((tmp != null) ? tmp : "0") / 1000;
+ format = FileUtil.getExtension(file.getName());
+ size = file.length();
+
+ if(Util.isOffline(context)) {
+ song.setGenre(metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE));
+ String year = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR);
+ song.setYear(Integer.parseInt((year != null) ? year : "0"));
+ }
+ }
+ } catch(Exception e) {
+ Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver");
+ }
+
+ String msg = "";
+ if(song instanceof PodcastEpisode) {
+ msg += "Podcast: " + song.getArtist() + "\nStatus: " + ((PodcastEpisode)song).getStatus();
+ } else if(!song.isVideo()) {
+ msg += "Artist: " + song.getArtist() + "\nAlbum: " + song.getAlbum();
+ }
+ if(song.getTrack() != null && song.getTrack() != 0) {
+ msg += "\nTrack: " + song.getTrack();
+ }
+ if(song.getGenre() != null && !"".equals(song.getGenre())) {
+ msg += "\nGenre: " + song.getGenre();
+ }
+ if(song.getYear() != null && song.getYear() != 0) {
+ msg += "\nYear: " + song.getYear();
+ }
+ if(!Util.isOffline(context) && song.getSuffix() != null) {
+ msg += "\nServer Format: " + song.getSuffix();
+ if(song.getBitRate() != null && song.getBitRate() != 0) {
+ msg += "\nServer Bitrate: " + song.getBitRate() + " kpbs";
+ }
+ }
+ if(format != null && !"".equals(format)) {
+ msg += "\nCached Format: " + format;
+ }
+ if(bitrate != null && bitrate != 0) {
+ msg += "\nCached Bitrate: " + bitrate + " kpbs";
+ }
+ if(size != 0) {
+ msg += "\nSize: " + Util.formatBytes(size);
+ }
+ if(song.getDuration() != null && song.getDuration() != 0) {
+ msg += "\nLength: " + Util.formatDuration(song.getDuration());
+ }
+
+ Util.info(context, song.getTitle(), msg);
+ }
+
+ protected void playVideo(MusicDirectory.Entry entry) {
+ if(entryExists(entry)) {
+ playExternalPlayer(entry);
+ } else {
+ streamExternalPlayer(entry);
+ }
+ }
+
+ protected void playWebView(MusicDirectory.Entry entry) {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(maxBitrate, context, entry.getId())));
+
+ startActivity(intent);
+ }
+ protected void playExternalPlayer(MusicDirectory.Entry entry) {
+ if(!entryExists(entry)) {
+ Util.toast(context, R.string.download_need_download);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.parse(entry.getPath()), "video/*");
+
+ List<ResolveInfo> intents = context.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if(intents != null && intents.size() > 0) {
+ startActivity(intent);
+ }else {
+ Util.toast(context, R.string.download_no_streaming_player);
+ }
+ }
+ }
+ protected void streamExternalPlayer(MusicDirectory.Entry entry) {
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("flash".equals(videoPlayerType)) {
+ playWebView(entry);
+ } else if("hls".equals(videoPlayerType)) {
+ streamExternalPlayer(entry, "hls");
+ } else if("raw".equals(videoPlayerType)) {
+ streamExternalPlayer(entry, "raw");
+ } else {
+ streamExternalPlayer(entry, entry.getTranscodedSuffix());
+ }
+ }
+ protected void streamExternalPlayer(MusicDirectory.Entry entry, String format) {
+ try {
+ int maxBitrate = Util.getMaxVideoBitrate(context);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ if("hls".equals(format)) {
+ intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getHlsUrl(entry.getId(), maxBitrate, context)), "video/*");
+ } else {
+ intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoStreamUrl(format, maxBitrate, context, entry.getId())), "video/*");
+ }
+ intent.putExtra("title", entry.getTitle());
+
+ List<ResolveInfo> intents = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if(intents != null && intents.size() > 0) {
+ startActivity(intent);
+ } else {
+ Util.toast(context, R.string.download_no_streaming_player);
+ }
+ } catch(Exception error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = error.getMessage();
+ } else {
+ msg = context.getResources().getString(R.string.download_no_streaming_player) + " " + error.getMessage();
+ }
+
+ Util.toast(context, msg, false);
+ }
+ }
+
+ protected boolean entryExists(MusicDirectory.Entry entry) {
+ DownloadFile check = new DownloadFile(context, entry, false);
+ return check.isCompleteFileAvailable();
+ }
+
+ public void deleteRecursively(Artist artist) {
+ File dir = FileUtil.getArtistDirectory(context, artist);
+ Util.recursiveDelete(dir);
+ if(Util.isOffline(context)) {
+ refresh();
+ }
+ }
+
+ public void deleteRecursively(MusicDirectory.Entry album) {
+ File dir = FileUtil.getAlbumDirectory(context, album);
+ Util.recursiveDelete(dir);
+ if(Util.isOffline(context)) {
+ refresh();
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubSearchProvider.java b/src/github/daneren2005/dsub/provider/DSubSearchProvider.java new file mode 100644 index 00000000..5ddec0f4 --- /dev/null +++ b/src/github/daneren2005/dsub/provider/DSubSearchProvider.java @@ -0,0 +1,36 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.provider; + +import android.content.SearchRecentSuggestionsProvider; + +/** + * Provides search suggestions based on recent searches. + * + * @author Sindre Mehus + */ +public class DSubSearchProvider extends SearchRecentSuggestionsProvider { + + public static final String AUTHORITY = DSubSearchProvider.class.getName(); + public static final int MODE = DATABASE_MODE_QUERIES; + + public DSubSearchProvider() { + setupSuggestions(AUTHORITY, MODE); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x1.java b/src/github/daneren2005/dsub/provider/DSubWidget4x1.java new file mode 100644 index 00000000..e00bf02d --- /dev/null +++ b/src/github/daneren2005/dsub/provider/DSubWidget4x1.java @@ -0,0 +1,29 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x1 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x1;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x2.java b/src/github/daneren2005/dsub/provider/DSubWidget4x2.java new file mode 100644 index 00000000..4908f632 --- /dev/null +++ b/src/github/daneren2005/dsub/provider/DSubWidget4x2.java @@ -0,0 +1,29 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x2 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x2;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x3.java b/src/github/daneren2005/dsub/provider/DSubWidget4x3.java new file mode 100644 index 00000000..f1908d0d --- /dev/null +++ b/src/github/daneren2005/dsub/provider/DSubWidget4x3.java @@ -0,0 +1,29 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x3 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x3;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidget4x4.java b/src/github/daneren2005/dsub/provider/DSubWidget4x4.java new file mode 100644 index 00000000..7fee2747 --- /dev/null +++ b/src/github/daneren2005/dsub/provider/DSubWidget4x4.java @@ -0,0 +1,29 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.provider;
+
+import android.appwidget.AppWidgetManager;
+import github.daneren2005.dsub.R;
+
+public class DSubWidget4x4 extends DSubWidgetProvider {
+ @Override
+ protected int getLayout() {
+ return R.layout.appwidget4x4;
+ }
+}
diff --git a/src/github/daneren2005/dsub/provider/DSubWidgetProvider.java b/src/github/daneren2005/dsub/provider/DSubWidgetProvider.java new file mode 100644 index 00000000..7215040c --- /dev/null +++ b/src/github/daneren2005/dsub/provider/DSubWidgetProvider.java @@ -0,0 +1,277 @@ +/* + 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 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.provider; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Environment; +import android.util.Log; +import android.view.KeyEvent; +import android.widget.RemoteViews; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.activity.DownloadActivity; +import github.daneren2005.dsub.activity.MainActivity; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.FileUtil; +import java.util.HashMap; + +/** + * Simple widget to show currently playing album art along + * with play/pause and next track buttons. + * <p/> + * Based on source code from the stock Android Music app. + * + * @author Sindre Mehus + */ +public class DSubWidgetProvider extends AppWidgetProvider { + private static final String TAG = DSubWidgetProvider.class.getSimpleName(); + private static DSubWidget4x1 instance4x1; + private static DSubWidget4x2 instance4x2; + private static DSubWidget4x3 instance4x3; + private static DSubWidget4x4 instance4x4; + + public static synchronized void notifyInstances(Context context, DownloadService service, boolean playing) { + if(instance4x1 == null) { + instance4x1 = new DSubWidget4x1(); + } + if(instance4x2 == null) { + instance4x2 = new DSubWidget4x2(); + } + if(instance4x3 == null) { + instance4x3 = new DSubWidget4x3(); + } + if(instance4x4 == null) { + instance4x4 = new DSubWidget4x4(); + } + + instance4x1.notifyChange(context, service, playing); + instance4x2.notifyChange(context, service, playing); + instance4x3.notifyChange(context, service, playing); + instance4x4.notifyChange(context, service, playing); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + defaultAppWidget(context, appWidgetIds); + } + + protected int getLayout() { + return 0; + } + + /** + * Initialize given widgets to default state, where we launch Subsonic on default click + * and hide actions if service not running. + */ + private void defaultAppWidget(Context context, int[] appWidgetIds) { + final Resources res = context.getResources(); + final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout()); + + views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text)); + if(getLayout() == R.layout.appwidget4x2) { + views.setTextViewText(R.id.album, ""); + } + + linkButtons(context, views, false); + pushUpdate(context, appWidgetIds, views); + } + + private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views) { + // Update specific list of appWidgetIds if given, otherwise default to all + final AppWidgetManager manager = AppWidgetManager.getInstance(context); + if (appWidgetIds != null) { + manager.updateAppWidget(appWidgetIds, views); + } else { + manager.updateAppWidget(new ComponentName(context, this.getClass()), views); + } + } + + /** + * Handle a change notification coming over from {@link DownloadService} + */ + public void notifyChange(Context context, DownloadService service, boolean playing) { + if (hasInstances(context)) { + performUpdate(context, service, null, playing); + } + } + + /** + * Check against {@link AppWidgetManager} if there are any instances of this widget. + */ + private boolean hasInstances(Context context) { + AppWidgetManager manager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass())); + return (appWidgetIds.length > 0); + } + + /** + * Update all active widget instances by pushing changes + */ + private void performUpdate(Context context, DownloadService service, int[] appWidgetIds, boolean playing) { + final Resources res = context.getResources(); + final RemoteViews views = new RemoteViews(context.getPackageName(), getLayout()); + + MusicDirectory.Entry currentPlaying = service.getCurrentPlaying() == null ? null : service.getCurrentPlaying().getSong(); + String title = currentPlaying == null ? null : currentPlaying.getTitle(); + CharSequence artist = currentPlaying == null ? null : currentPlaying.getArtist(); + CharSequence album = currentPlaying == null ? null : currentPlaying.getAlbum(); + CharSequence errorState = null; + + // Show error message? + String status = Environment.getExternalStorageState(); + if (status.equals(Environment.MEDIA_SHARED) || + status.equals(Environment.MEDIA_UNMOUNTED)) { + errorState = res.getText(R.string.widget_sdcard_busy); + } else if (status.equals(Environment.MEDIA_REMOVED)) { + errorState = res.getText(R.string.widget_sdcard_missing); + } else if (currentPlaying == null) { + errorState = res.getText(R.string.widget_initial_text); + } + + if (errorState != null) { + // Show error state to user + views.setTextViewText(R.id.title,null); + views.setTextViewText(R.id.artist, errorState); + views.setTextViewText(R.id.album, ""); + if(getLayout() != R.layout.appwidget4x1) { + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_default); + } + } else { + // No error, so show normal titles + views.setTextViewText(R.id.title, title); + views.setTextViewText(R.id.artist, artist); + if(getLayout() != R.layout.appwidget4x1) { + views.setTextViewText(R.id.album, album); + } + } + + // Set correct drawable for pause state + if (playing) { + views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause); + } else { + views.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play); + } + + // Set the cover art + try { + int size; + if(getLayout() != R.layout.appwidget4x1 && getLayout() != R.layout.appwidget4x2) { + size = context.getResources().getDrawable(R.drawable.unknown_album_large).getIntrinsicHeight(); + } else { + size = context.getResources().getDrawable(R.drawable.appwidget_art_default).getIntrinsicHeight(); + } + Bitmap bitmap = currentPlaying == null ? null : FileUtil.getAlbumArtBitmap(context, currentPlaying, size); + + if (bitmap == null) { + // Set default cover art + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown); + } else { + bitmap = getRoundedCornerBitmap(bitmap); + views.setImageViewBitmap(R.id.appwidget_coverart, bitmap); + } + } catch (Exception x) { + Log.e(TAG, "Failed to load cover art", x); + views.setImageViewResource(R.id.appwidget_coverart, R.drawable.appwidget_art_unknown); + } + + // Link actions buttons to intents + linkButtons(context, views, currentPlaying != null); + + pushUpdate(context, appWidgetIds, views); + } + + /** + * Round the corners of a bitmap for the cover art image + */ + private static Bitmap getRoundedCornerBitmap(Bitmap bitmap) { + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final float roundPx = 10; + + // Add extra width to the rect so the right side wont be rounded. + final Rect rect = new Rect(0, 0, bitmap.getWidth() + (int) roundPx, bitmap.getHeight()); + final RectF rectF = new RectF(rect); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawRoundRect(rectF, roundPx, roundPx, paint); + + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + return output; + } + + /** + * Link up various button actions using {@link PendingIntent}. + * + * @param playerActive True if player is active in background, which means + * widget click will launch {@link DownloadActivity}, + * otherwise we launch {@link MainActivity}. + */ + private void linkButtons(Context context, RemoteViews views, boolean playerActive) { + Intent intent = new Intent(context, MainActivity.class); + if(playerActive) { + intent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); + views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); + + // Emulate media button clicks. + intent = new Intent("DSub.PLAY_PAUSE"); + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_play, pendingIntent); + + intent = new Intent("DSub.NEXT"); // Use a unique action name to ensure a different PendingIntent to be created. + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_next, pendingIntent); + + intent = new Intent("DSub.PREVIOUS"); // Use a unique action name to ensure a different PendingIntent to be created. + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); + } +} diff --git a/src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java b/src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java new file mode 100644 index 00000000..c8c3a1f9 --- /dev/null +++ b/src/github/daneren2005/dsub/receiver/A2dpIntentReceiver.java @@ -0,0 +1,48 @@ +package github.daneren2005.dsub.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.DownloadServiceImpl;
+
+public class A2dpIntentReceiver extends BroadcastReceiver {
+ private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse";
+ private String TAG = A2dpIntentReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.i(TAG, "GOT INTENT " + intent);
+
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+
+ if (downloadService != null){
+
+ Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE);
+
+ avrcpIntent.putExtra("duration", (long) downloadService.getPlayerDuration());
+ avrcpIntent.putExtra("position", (long) downloadService.getPlayerPosition());
+ avrcpIntent.putExtra("ListSize", (long) downloadService.getSongs().size());
+
+ switch (downloadService.getPlayerState()){
+ case STARTED:
+ avrcpIntent.putExtra("playing", true);
+ break;
+ case STOPPED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PAUSED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case COMPLETED:
+ avrcpIntent.putExtra("playing", false);
+ break;
+ default:
+ return;
+ }
+
+ context.sendBroadcast(avrcpIntent);
+ }
+ }
+}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java b/src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java new file mode 100644 index 00000000..567cf8f4 --- /dev/null +++ b/src/github/daneren2005/dsub/receiver/BluetoothIntentReceiver.java @@ -0,0 +1,78 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.receiver; + +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.util.Util; + +/** + * Request media button focus when connected to Bluetooth A2DP. + * + * @author Sindre Mehus + */ +public class BluetoothIntentReceiver extends BroadcastReceiver { + private static final String TAG = BluetoothIntentReceiver.class.getSimpleName(); + // Same as constants in android.bluetooth.BluetoothProfile, which is API level 11. + private static final int STATE_DISCONNECTED = 0; + private static final int STATE_CONNECTED = 2; + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "GOT INTENT " + intent); + if (isConnected(intent)) { + Log.i(TAG, "Connected to Bluetooth A2DP, requesting media button focus."); + Util.registerMediaButtonEventReceiver(context); + } else if (isDisconnected(intent)) { + Log.i(TAG, "Disconnected from Bluetooth A2DP, requesting pause."); + context.sendBroadcast(new Intent(DownloadServiceImpl.CMD_PAUSE)); + } + } + private boolean isConnected(Intent intent) { + if ("android.bluetooth.a2dp.action.SINK_STATE_CHANGED".equals(intent.getAction()) && + intent.getIntExtra("android.bluetooth.a2dp.extra.SINK_STATE", -1) == STATE_CONNECTED) { + return true; + } + else if ("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED".equals(intent.getAction()) && + intent.getIntExtra("android.bluetooth.profile.extra.STATE", -1) == STATE_CONNECTED) { + return true; + } + else if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) { + return true; + } + return false; + } + private boolean isDisconnected(Intent intent) { + if ("android.bluetooth.a2dp.action.SINK_STATE_CHANGED".equals(intent.getAction()) && + intent.getIntExtra("android.bluetooth.a2dp.extra.SINK_STATE", -1) == STATE_DISCONNECTED) { + return true; + } + else if ("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED".equals(intent.getAction()) && + intent.getIntExtra("android.bluetooth.profile.extra.STATE", -1) == STATE_DISCONNECTED) { + return true; + } + else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(intent.getAction())) { + return true; + } + return false; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java b/src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java new file mode 100644 index 00000000..9ea04474 --- /dev/null +++ b/src/github/daneren2005/dsub/receiver/MediaButtonIntentReceiver.java @@ -0,0 +1,52 @@ +/* + 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 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; +import github.daneren2005.dsub.service.DownloadServiceImpl; + +/** + * @author Sindre Mehus + */ +public class MediaButtonIntentReceiver extends BroadcastReceiver { + + private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); + + Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); + serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + context.startService(serviceIntent); + if (isOrderedBroadcast()) + { + try { + abortBroadcast(); + } catch (Exception x) { + // Ignored. + } + } + } +} diff --git a/src/github/daneren2005/dsub/service/CachedMusicService.java b/src/github/daneren2005/dsub/service/CachedMusicService.java new file mode 100644 index 00000000..943b5eb2 --- /dev/null +++ b/src/github/daneren2005/dsub/service/CachedMusicService.java @@ -0,0 +1,373 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpResponse; + +import android.content.Context; +import android.graphics.Bitmap; +import android.support.v4.util.LruCache; +import github.daneren2005.dsub.domain.ChatMessage; +import github.daneren2005.dsub.domain.Genre; +import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.JukeboxStatus; +import github.daneren2005.dsub.domain.Lyrics; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicFolder; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.SearchCritera; +import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.domain.Share; +import github.daneren2005.dsub.domain.Version; +import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.TimeLimitedCache; +import github.daneren2005.dsub.util.Util; + +/** + * @author Sindre Mehus + */ +public class CachedMusicService implements MusicService { + + private static final int MUSIC_DIR_CACHE_SIZE = 20; + private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes + + private final MusicService musicService; + private final LruCache<String, TimeLimitedCache<MusicDirectory>> cachedMusicDirectories; + private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS); + private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS); + private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<List<Playlist>>(3600, TimeUnit.SECONDS); + private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<List<MusicFolder>>(10 * 3600, TimeUnit.SECONDS); + private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS); + private final TimeLimitedCache<List<PodcastChannel>> cachedPodcastChannels = new TimeLimitedCache<List<PodcastChannel>>(10 * 3600, TimeUnit.SECONDS); + private String restUrl; + + public CachedMusicService(MusicService musicService) { + this.musicService = musicService; + cachedMusicDirectories = new LruCache<String, TimeLimitedCache<MusicDirectory>>(MUSIC_DIR_CACHE_SIZE); + } + + @Override + public void ping(Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + musicService.ping(context, progressListener); + } + + @Override + public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + Boolean result = cachedLicenseValid.get(); + if (result == null) { + result = musicService.isLicenseValid(context, progressListener); + cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS); + } + return result; + } + + @Override + public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + if (refresh) { + cachedMusicFolders.clear(); + } + List<MusicFolder> result = cachedMusicFolders.get(); + if (result == null) { + result = musicService.getMusicFolders(refresh, context, progressListener); + cachedMusicFolders.set(result); + } + return result; + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + if (refresh) { + cachedIndexes.clear(); + cachedMusicFolders.clear(); + cachedMusicDirectories.evictAll(); + } + Indexes result = cachedIndexes.get(); + if (result == null) { + result = musicService.getIndexes(musicFolderId, refresh, context, progressListener); + cachedIndexes.set(result); + } + return result; + } + + @Override + public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(id); + MusicDirectory dir = cache == null ? null : cache.get(); + if (dir == null) { + dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener); + cache = new TimeLimitedCache<MusicDirectory>(TTL_MUSIC_DIR, TimeUnit.SECONDS); + cache.set(dir); + cachedMusicDirectories.put(id, cache); + } + return dir; + } + + @Override + public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { + return musicService.search(criteria, context, progressListener); + } + + @Override + public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception { + return musicService.getPlaylist(id, name, context, progressListener); + } + + @Override + public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + List<Playlist> result = refresh ? null : cachedPlaylists.get(); + if (result == null) { + result = musicService.getPlaylists(refresh, context, progressListener); + cachedPlaylists.set(result); + } + return result; + } + + @Override + public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception { + cachedPlaylists.clear(); + musicService.createPlaylist(id, name, entries, context, progressListener); + } + + @Override + public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { + musicService.deletePlaylist(id, context, progressListener); + } + + @Override + public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { + musicService.addToPlaylist(id, toAdd, context, progressListener); + } + + @Override + public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception { + musicService.removeFromPlaylist(id, toRemove, context, progressListener); + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { + musicService.overwritePlaylist(id, name, toRemove, toAdd, context, progressListener); + } + + @Override + public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { + musicService.updatePlaylist(id, name, comment, pub, context, progressListener); + } + + @Override + public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { + return musicService.getLyrics(artist, title, context, progressListener); + } + + @Override + public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception { + musicService.scrobble(id, submission, context, progressListener); + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getAlbumList(type, size, offset, context, progressListener); + } + + @Override + public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception { + return musicService.getStarredList(context, progressListener); + } + + @Override + public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { + return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener); + } + + @Override + public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception { + return musicService.getCoverArt(context, entry, size, saveSize, progressListener); + } + + @Override + public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception { + return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task); + } + + @Override + public Version getLocalVersion(Context context) throws Exception { + return musicService.getLocalVersion(context); + } + + @Override + public Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception { + return musicService.getLatestVersion(context, progressListener); + } + + @Override + public String getVideoUrl(int maxBitrate, Context context, String id) { + return musicService.getVideoUrl(maxBitrate, context, id); + } + + @Override + public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception { + return musicService.getVideoStreamUrl(format, maxBitrate, context, id); + } + + @Override + public String getHlsUrl(String id, int bitRate, Context context) throws Exception { + return musicService.getHlsUrl(id, bitRate, context); + } + + @Override + public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception { + return musicService.updateJukeboxPlaylist(ids, context, progressListener); + } + + @Override + public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception { + return musicService.skipJukebox(index, offsetSeconds, context, progressListener); + } + + @Override + public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception { + return musicService.stopJukebox(context, progressListener); + } + + @Override + public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception { + return musicService.startJukebox(context, progressListener); + } + + @Override + public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception { + return musicService.getJukeboxStatus(context, progressListener); + } + + @Override + public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { + return musicService.setJukeboxGain(gain, context, progressListener); + } + + @Override + public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception { + musicService.setStarred(id, starred, context, progressListener); + } + + @Override + public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception { + return musicService.getShares(context, progressListener); + } + + @Override + public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception { + return musicService.getChatMessages(since, context, progressListener); + } + + @Override + public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception { + musicService.addChatMessage(message, context, progressListener); + } + + @Override + public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + List<Genre> result = refresh ? null : cachedGenres.get(); + + if (result == null) { + result = musicService.getGenres(refresh, context, progressListener); + cachedGenres.set(result); + } + + return result; + } + + @Override + public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { + return musicService.getSongsByGenre(genre, count, offset, context, progressListener); + } + + @Override + public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkSettingsChanged(context); + List<PodcastChannel> result = refresh ? null : cachedPodcastChannels.get(); + + if (result == null) { + result = musicService.getPodcastChannels(refresh, context, progressListener); + cachedPodcastChannels.set(result); + } + + return result; + } + + @Override + public MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception { + return musicService.getPodcastEpisodes(id, context, progressListener); + } + + @Override + public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception { + musicService.refreshPodcasts(context, progressListener); + } + + @Override + public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{ + musicService.createPodcastChannel(url, context, progressListener); + } + + @Override + public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception{ + musicService.deletePodcastChannel(id, context, progressListener); + } + + @Override + public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ + musicService.downloadPodcastEpisode(id, context, progressListener); + } + + @Override + public void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ + musicService.deletePodcastEpisode(id, context, progressListener); + } + + @Override + public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ + return musicService.processOfflineSyncs(context, progressListener); + } + + + + private void checkSettingsChanged(Context context) { + String newUrl = Util.getRestUrl(context, null); + if (!Util.equals(newUrl, restUrl)) { + cachedMusicFolders.clear(); + cachedMusicDirectories.evictAll(); + cachedLicenseValid.clear(); + cachedIndexes.clear(); + cachedPlaylists.clear(); + cachedPodcastChannels.clear(); + restUrl = newUrl; + } + } +} diff --git a/src/github/daneren2005/dsub/service/DownloadFile.java b/src/github/daneren2005/dsub/service/DownloadFile.java new file mode 100644 index 00000000..5ab7ad70 --- /dev/null +++ b/src/github/daneren2005/dsub/service/DownloadFile.java @@ -0,0 +1,398 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.util.DisplayMetrics; +import android.util.Log; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.CacheCleaner; +import org.apache.http.Header; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class DownloadFile { + + private static final String TAG = DownloadFile.class.getSimpleName(); + private final Context context; + private final MusicDirectory.Entry song; + private final File partialFile; + private final File completeFile; + private final File saveFile; + + private final MediaStoreService mediaStoreService; + private CancellableTask downloadTask; + private boolean save; + private boolean failed; + private int bitRate; + private boolean isPlaying = false; + private boolean saveWhenDone = false; + private boolean completeWhenDone = false; + private Integer contentLength = null; + + public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) { + this.context = context; + this.song = song; + this.save = save; + saveFile = FileUtil.getSongFile(context, song); + bitRate = Util.getMaxBitrate(context); + partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + + ".partial." + FileUtil.getExtension(saveFile.getName())); + completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) + + ".complete." + FileUtil.getExtension(saveFile.getName())); + mediaStoreService = new MediaStoreService(context); + } + + public MusicDirectory.Entry getSong() { + return song; + } + + /** + * Returns the effective bit rate. + */ + public int getBitRate() { + if(!partialFile.exists()) { + bitRate = Util.getMaxBitrate(context); + } + if (bitRate > 0) { + return bitRate; + } + return song.getBitRate() == null ? 160 : song.getBitRate(); + } + + public Integer getContentLength() { + return contentLength; + } + + public synchronized void download() { + FileUtil.createDirectoryForParent(saveFile); + failed = false; + if(!partialFile.exists()) { + bitRate = Util.getMaxBitrate(context); + } + downloadTask = new DownloadTask(); + downloadTask.start(); + } + + public synchronized void cancelDownload() { + if (downloadTask != null) { + downloadTask.cancel(); + } + } + + public File getCompleteFile() { + if (saveFile.exists()) { + return saveFile; + } + + if (completeFile.exists()) { + return completeFile; + } + + return saveFile; + } + + public File getPartialFile() { + return partialFile; + } + + public boolean isSaved() { + return saveFile.exists(); + } + + public synchronized boolean isCompleteFileAvailable() { + return saveFile.exists() || completeFile.exists(); + } + + public synchronized boolean isWorkDone() { + return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone; + } + + public synchronized boolean isDownloading() { + return downloadTask != null && downloadTask.isRunning(); + } + + public synchronized boolean isDownloadCancelled() { + return downloadTask != null && downloadTask.isCancelled(); + } + + public boolean shouldSave() { + return save; + } + + public boolean isFailed() { + return failed; + } + + public void delete() { + cancelDownload(); + Util.delete(partialFile); + Util.delete(completeFile); + Util.delete(saveFile); + mediaStoreService.deleteFromMediaStore(this); + } + + public void unpin() { + if (saveFile.exists()) { + saveFile.renameTo(completeFile); + } + } + + public boolean cleanup() { + boolean ok = true; + if (completeFile.exists() || saveFile.exists()) { + ok = Util.delete(partialFile); + } + if (saveFile.exists()) { + ok &= Util.delete(completeFile); + } + return ok; + } + + // In support of LRU caching. + public void updateModificationDate() { + updateModificationDate(saveFile); + updateModificationDate(partialFile); + updateModificationDate(completeFile); + } + + private void updateModificationDate(File file) { + if (file.exists()) { + boolean ok = file.setLastModified(System.currentTimeMillis()); + if (!ok) { + Log.w(TAG, "Failed to set last-modified date on " + file); + } + } + } + + public void setPlaying(boolean isPlaying) { + try { + if(saveWhenDone && isPlaying == false) { + Util.renameFile(completeFile, saveFile); + saveWhenDone = false; + } else if(completeWhenDone && isPlaying == false) { + if(save) { + Util.renameFile(partialFile, saveFile); + mediaStoreService.saveInMediaStore(DownloadFile.this); + } else { + Util.renameFile(partialFile, completeFile); + } + completeWhenDone = false; + } + } catch(IOException ex) { + Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile); + } + + this.isPlaying = isPlaying; + } + public boolean getPlaying() { + return isPlaying; + } + + @Override + public String toString() { + return "DownloadFile (" + song + ")"; + } + + private class DownloadTask extends CancellableTask { + + @Override + public void execute() { + + InputStream in = null; + FileOutputStream out = null; + PowerManager.WakeLock wakeLock = null; + WifiManager.WifiLock wifiLock = null; + try { + + if (Util.isScreenLitOnDownload(context)) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString()); + wakeLock.acquire(); + Log.i(TAG, "Acquired wake lock " + wakeLock); + } + + wifiLock = Util.createWifiLock(context, toString()); + wifiLock.acquire(); + + if (saveFile.exists()) { + Log.i(TAG, saveFile + " already exists. Skipping."); + return; + } + if (completeFile.exists()) { + if (save) { + if(isPlaying) { + saveWhenDone = true; + } else { + Util.renameFile(completeFile, saveFile); + } + } else { + Log.i(TAG, completeFile + " already exists. Skipping."); + } + return; + } + + MusicService musicService = MusicServiceFactory.getMusicService(context); + + // Some devices seem to throw error on partial file which doesn't exist + boolean compare; + try { + compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length(); + } catch(Exception e) { + compare = true; + } + if(compare) { + // Attempt partial HTTP GET, appending to the file if it exists. + HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this); + Header contentLengthHeader = response.getFirstHeader("Content-Length"); + if(contentLengthHeader != null) { + String contentLengthString = contentLengthHeader.getValue(); + if(contentLengthString != null) { + Log.i(TAG, "Content Length: " + contentLengthString); + contentLength = Integer.parseInt(contentLengthString); + } + } + in = response.getEntity().getContent(); + boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT; + if (partial) { + Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes"); + } + + out = new FileOutputStream(partialFile, partial); + long n = copy(in, out); + Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile); + out.flush(); + out.close(); + + if (isCancelled()) { + throw new Exception("Download of '" + song + "' was cancelled"); + } + + downloadAndSaveCoverArt(musicService); + } + + if(isPlaying) { + completeWhenDone = true; + } else { + if(save) { + Util.renameFile(partialFile, saveFile); + mediaStoreService.saveInMediaStore(DownloadFile.this); + } else { + Util.renameFile(partialFile, completeFile); + } + } + + } catch (Exception x) { + Util.close(out); + Util.delete(completeFile); + Util.delete(saveFile); + if (!isCancelled()) { + failed = true; + Log.w(TAG, "Failed to download '" + song + "'.", x); + } + + } finally { + Util.close(in); + Util.close(out); + if (wakeLock != null) { + wakeLock.release(); + Log.i(TAG, "Released wake lock " + wakeLock); + } + if (wifiLock != null) { + wifiLock.release(); + } + new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace(); + if(DownloadServiceImpl.getInstance() != null) { + ((DownloadServiceImpl)DownloadServiceImpl.getInstance()).checkDownloads(); + } + } + } + + @Override + public String toString() { + return "DownloadTask (" + song + ")"; + } + + private void downloadAndSaveCoverArt(MusicService musicService) throws Exception { + try { + if (song.getCoverArt() != null) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + int size = Math.min(metrics.widthPixels, metrics.heightPixels); + musicService.getCoverArt(context, song, size, size, null); + } + } catch (Exception x) { + Log.e(TAG, "Failed to get cover art.", x); + } + } + + private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException { + + // Start a thread that will close the input stream if the task is + // cancelled, thus causing the copy() method to return. + new Thread() { + @Override + public void run() { + while (true) { + Util.sleepQuietly(3000L); + if (isCancelled()) { + Util.close(in); + return; + } + if (!isRunning()) { + return; + } + } + } + }.start(); + + byte[] buffer = new byte[1024 * 16]; + long count = 0; + int n; + long lastLog = System.currentTimeMillis(); + + while (!isCancelled() && (n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + count += n; + + long now = System.currentTimeMillis(); + if (now - lastLog > 3000L) { // Only every so often. + Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song); + lastLog = now; + } + } + return count; + } + } +} diff --git a/src/github/daneren2005/dsub/service/DownloadService.java b/src/github/daneren2005/dsub/service/DownloadService.java new file mode 100644 index 00000000..328cc962 --- /dev/null +++ b/src/github/daneren2005/dsub/service/DownloadService.java @@ -0,0 +1,141 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.util.List; + +import github.daneren2005.dsub.audiofx.EqualizerController; +import github.daneren2005.dsub.audiofx.VisualizerController; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.domain.RepeatMode; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public interface DownloadService { + + void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle); + void downloadBackground(List<MusicDirectory.Entry> songs, boolean save); + + void setShufflePlayEnabled(boolean enabled); + + boolean isShufflePlayEnabled(); + + void shuffle(); + + RepeatMode getRepeatMode(); + + void setRepeatMode(RepeatMode repeatMode); + + boolean getKeepScreenOn(); + + void setKeepScreenOn(boolean screenOn); + + boolean getShowVisualization(); + + void setShowVisualization(boolean showVisualization); + + void clear(); + + void clearBackground(); + + void clearIncomplete(); + + int size(); + + void remove(int which); + + void remove(DownloadFile downloadFile); + + List<DownloadFile> getSongs(); + + List<DownloadFile> getDownloads(); + + List<DownloadFile> getBackgroundDownloads(); + + int getCurrentPlayingIndex(); + + DownloadFile getCurrentPlaying(); + + DownloadFile getCurrentDownloading(); + + void play(int index); + + void seekTo(int position); + + void previous(); + + void next(); + + void pause(); + + void stop(); + + void start(); + + void reset(); + + PlayerState getPlayerState(); + + int getPlayerPosition(); + + int getPlayerDuration(); + + void delete(List<MusicDirectory.Entry> songs); + + void unpin(List<MusicDirectory.Entry> songs); + + DownloadFile forSong(MusicDirectory.Entry song); + + long getDownloadListUpdateRevision(); + + void setSuggestedPlaylistName(String name, String id); + + String getSuggestedPlaylistName(); + + String getSuggestedPlaylistId(); + + boolean getEqualizerAvailable(); + + boolean getVisualizerAvailable(); + + EqualizerController getEqualizerController(); + + VisualizerController getVisualizerController(); + + boolean isJukeboxEnabled(); + + void setJukeboxEnabled(boolean b); + + void adjustJukeboxVolume(boolean up); + + void setSleepTimerDuration(int duration); + + void startSleepTimer(); + + void stopSleepTimer(); + + boolean getSleepTimer(); + + void setVolume(float volume); + + void swap(boolean mainList, int from, int to); +} diff --git a/src/github/daneren2005/dsub/service/DownloadServiceImpl.java b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java new file mode 100644 index 00000000..04875f34 --- /dev/null +++ b/src/github/daneren2005/dsub/service/DownloadServiceImpl.java @@ -0,0 +1,1539 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import static github.daneren2005.dsub.domain.PlayerState.COMPLETED; +import static github.daneren2005.dsub.domain.PlayerState.DOWNLOADING; +import static github.daneren2005.dsub.domain.PlayerState.IDLE; +import static github.daneren2005.dsub.domain.PlayerState.PAUSED; +import static github.daneren2005.dsub.domain.PlayerState.PREPARED; +import static github.daneren2005.dsub.domain.PlayerState.PREPARING; +import static github.daneren2005.dsub.domain.PlayerState.STARTED; +import static github.daneren2005.dsub.domain.PlayerState.STOPPED; +import github.daneren2005.dsub.audiofx.EqualizerController; +import github.daneren2005.dsub.audiofx.VisualizerController; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.domain.RepeatMode; +import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; +import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.ShufflePlayBuffer; +import github.daneren2005.dsub.util.SimpleServiceBinder; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.RemoteControlClientHelper; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.audiofx.AudioEffect; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.PowerManager; +import android.util.Log; +import android.support.v4.util.LruCache; +import java.net.URLEncoder; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class DownloadServiceImpl extends Service implements DownloadService { + + private static final String TAG = DownloadServiceImpl.class.getSimpleName(); + + public static final String CMD_PLAY = "github.daneren2005.dsub.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "github.daneren2005.dsub.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "github.daneren2005.dsub.CMD_PAUSE"; + public static final String CMD_STOP = "github.daneren2005.dsub.CMD_STOP"; + public static final String CMD_PREVIOUS = "github.daneren2005.dsub.CMD_PREVIOUS"; + public static final String CMD_NEXT = "github.daneren2005.dsub.CMD_NEXT"; + + + private RemoteControlClientHelper mRemoteControl; + + private final IBinder binder = new SimpleServiceBinder<DownloadService>(this); + private Looper mediaPlayerLooper; + private MediaPlayer mediaPlayer; + private MediaPlayer nextMediaPlayer; + private boolean nextSetup = false; + private boolean isPartial = true; + private final List<DownloadFile> downloadList = new ArrayList<DownloadFile>(); + private final List<DownloadFile> backgroundDownloadList = new ArrayList<DownloadFile>(); + private final Handler handler = new Handler(); + private Handler mediaPlayerHandler; + private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); + private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); + + private final LruCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LruCache<MusicDirectory.Entry, DownloadFile>(100); + private final List<DownloadFile> cleanupCandidates = new ArrayList<DownloadFile>(); + private final Scrobbler scrobbler = new Scrobbler(); + private final JukeboxService jukeboxService = new JukeboxService(this); + private DownloadFile currentPlaying; + private DownloadFile nextPlaying; + private DownloadFile currentDownloading; + private CancellableTask bufferTask; + private CancellableTask nextPlayingTask; + private PlayerState playerState = IDLE; + private PlayerState nextPlayerState = IDLE; + private boolean shufflePlay; + private long revision; + private static DownloadService instance; + private String suggestedPlaylistName; + private String suggestedPlaylistId; + private PowerManager.WakeLock wakeLock; + private boolean keepScreenOn; + private int cachedPosition = 0; + + private static boolean equalizerAvailable; + private static boolean visualizerAvailable; + private EqualizerController equalizerController; + private VisualizerController visualizerController; + private boolean showVisualization; + private boolean jukeboxEnabled; + private PositionCache positionCache; + private StreamProxy proxy; + + private Timer sleepTimer; + private int timerDuration; + private boolean autoPlayStart = false; + + static { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + equalizerAvailable = true; + visualizerAvailable = true; + } + } + + @Override + public void onCreate() { + super.onCreate(); + + new Thread(new Runnable() { + public void run() { + Looper.prepare(); + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK); + + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) { + handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")")); + return false; + } + }); + + try { + Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } catch(Throwable e) { + // Froyo or lower + } + + mediaPlayerLooper = Looper.myLooper(); + mediaPlayerHandler = new Handler(mediaPlayerLooper); + Looper.loop(); + } + }).start(); + + Util.registerMediaButtonEventReceiver(this); + + if (mRemoteControl == null) { + // Use the remote control APIs (if available) to set the playback state + mRemoteControl = RemoteControlClientHelper.createInstance(); + ComponentName mediaButtonReceiverComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName()); + mRemoteControl.register(this, mediaButtonReceiverComponent); + } + + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + SharedPreferences prefs = Util.getPreferences(this); + try { + timerDuration = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_SLEEP_TIMER_DURATION, "5")); + } catch(Throwable e) { + timerDuration = 5; + } + sleepTimer = null; + + keepScreenOn = prefs.getBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, false); + + instance = this; + lifecycleSupport.onCreate(); + + if(prefs.getBoolean(Constants.PREFERENCES_EQUALIZER_ON, false)) { + getEqualizerController(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + lifecycleSupport.onStart(intent); + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + instance = null; + + if(currentPlaying != null) currentPlaying.setPlaying(false); + if(sleepTimer != null){ + sleepTimer.cancel(); + sleepTimer.purge(); + } + lifecycleSupport.onDestroy(); + + try { + Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.getAudioSessionId()); + i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); + sendBroadcast(i); + } catch(Throwable e) { + // Froyo or lower + } + + mediaPlayer.release(); + if(nextMediaPlayer != null) { + nextMediaPlayer.release(); + } + mediaPlayerLooper.quit(); + shufflePlayBuffer.shutdown(); + if (equalizerController != null) { + equalizerController.release(); + } + if (visualizerController != null) { + visualizerController.release(); + } + if (mRemoteControl != null) { + mRemoteControl.unregister(this); + mRemoteControl = null; + } + + if(bufferTask != null) { + bufferTask.cancel(); + } + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + } + Util.hidePlayingNotification(this, this, handler); + } + + public static DownloadService getInstance() { + return instance; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoplay, boolean playNext, boolean shuffle) { + setShufflePlayEnabled(false); + int offset = 1; + + if (songs.isEmpty()) { + return; + } + if (playNext) { + if (autoplay && getCurrentPlayingIndex() >= 0) { + offset = 0; + } + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); + offset++; + } + revision++; + } else { + int size = size(); + int index = getCurrentPlayingIndex(); + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + downloadList.add(downloadFile); + } + if(!autoplay && (size - 1) == index) { + setNextPlaying(); + } + revision++; + } + updateJukeboxPlaylist(); + + if(shuffle) { + shuffle(); + } + + if (autoplay) { + play(0); + } else { + if (currentPlaying == null) { + currentPlaying = downloadList.get(0); + currentPlaying.setPlaying(true); + } + checkDownloads(); + } + lifecycleSupport.serializeDownloadQueue(); + } + public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save) { + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + backgroundDownloadList.add(downloadFile); + } + revision++; + + checkDownloads(); + lifecycleSupport.serializeDownloadQueue(); + } + + private void updateJukeboxPlaylist() { + if (jukeboxEnabled) { + jukeboxService.updatePlaylist(); + } + } + + public void restore(List<MusicDirectory.Entry> songs, int currentPlayingIndex, int currentPlayingPosition) { + SharedPreferences prefs = Util.getPreferences(this); + boolean startShufflePlay = prefs.getBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, false); + download(songs, false, false, false, false); + if(startShufflePlay) { + shufflePlay = true; + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, true); + editor.commit(); + } + if (currentPlayingIndex != -1) { + while(mediaPlayer == null) { + Util.sleepQuietly(50L); + } + + play(currentPlayingIndex, false); + if (currentPlaying != null && currentPlaying.isCompleteFileAvailable()) { + doPlay(currentPlaying, currentPlayingPosition, autoPlayStart); + } + autoPlayStart = false; + } + } + + @Override + public synchronized void setShufflePlayEnabled(boolean enabled) { + shufflePlay = enabled; + if (shufflePlay) { + clear(); + checkDownloads(); + } + SharedPreferences.Editor editor = Util.getPreferences(this).edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_SHUFFLE_MODE, enabled); + editor.commit(); + } + + @Override + public boolean isShufflePlayEnabled() { + return shufflePlay; + } + + @Override + public synchronized void shuffle() { + Collections.shuffle(downloadList); + if (currentPlaying != null) { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, currentPlaying); + } + revision++; + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + setNextPlaying(); + } + + @Override + public RepeatMode getRepeatMode() { + return Util.getRepeatMode(this); + } + + @Override + public void setRepeatMode(RepeatMode repeatMode) { + Util.setRepeatMode(this, repeatMode); + setNextPlaying(); + } + + @Override + public boolean getKeepScreenOn() { + return keepScreenOn; + } + + @Override + public void setKeepScreenOn(boolean keepScreenOn) { + this.keepScreenOn = keepScreenOn; + + SharedPreferences prefs = Util.getPreferences(this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_KEEP_SCREEN_ON, keepScreenOn); + editor.commit(); + } + + @Override + public boolean getShowVisualization() { + return showVisualization; + } + + @Override + public void setShowVisualization(boolean showVisualization) { + this.showVisualization = showVisualization; + } + + @Override + public synchronized DownloadFile forSong(MusicDirectory.Entry song) { + for (DownloadFile downloadFile : downloadList) { + if (downloadFile.getSong().equals(song) && + ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) + || downloadFile.isWorkDone())) { + return downloadFile; + } + } + for (DownloadFile downloadFile : backgroundDownloadList) { + if (downloadFile.getSong().equals(song)) { + return downloadFile; + } + } + + DownloadFile downloadFile = downloadFileCache.get(song); + if (downloadFile == null) { + downloadFile = new DownloadFile(this, song, false); + downloadFileCache.put(song, downloadFile); + } + return downloadFile; + } + + @Override + public synchronized void clear() { + clear(true); + } + + @Override + public synchronized void clearBackground() { + if(currentDownloading != null && backgroundDownloadList.contains(currentDownloading)) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + backgroundDownloadList.clear(); + } + + @Override + public synchronized void clearIncomplete() { + reset(); + Iterator<DownloadFile> iterator = downloadList.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (!downloadFile.isCompleteFileAvailable()) { + iterator.remove(); + } + } + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + } + + @Override + public synchronized int size() { + return downloadList.size(); + } + + public synchronized void clear(boolean serialize) { + reset(); + downloadList.clear(); + revision++; + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + setCurrentPlaying(null, false); + + if (serialize) { + lifecycleSupport.serializeDownloadQueue(); + } + updateJukeboxPlaylist(); + setNextPlaying(); + } + + @Override + public synchronized void remove(int which) { + downloadList.remove(which); + } + + @Override + public synchronized void remove(DownloadFile downloadFile) { + if (downloadFile == currentDownloading) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + if (downloadFile == currentPlaying) { + reset(); + setCurrentPlaying(null, false); + } + downloadList.remove(downloadFile); + backgroundDownloadList.remove(downloadFile); + revision++; + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + if(downloadFile == nextPlaying) { + setNextPlaying(); + } + } + + @Override + public synchronized void delete(List<MusicDirectory.Entry> songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).delete(); + } + } + + @Override + public synchronized void unpin(List<MusicDirectory.Entry> songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).unpin(); + } + } + + synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) { + try { + setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification); + } catch (IndexOutOfBoundsException x) { + // Ignored + } + } + + synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) { + if(this.currentPlaying != null) { + this.currentPlaying.setPlaying(false); + } + this.currentPlaying = currentPlaying; + + if (currentPlaying != null) { + Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); + mRemoteControl.updateMetadata(this, currentPlaying.getSong()); + } else { + Util.broadcastNewTrackInfo(this, null); + Util.hidePlayingNotification(this, this, handler); + } + } + + synchronized void setNextPlaying() { + SharedPreferences prefs = Util.getPreferences(DownloadServiceImpl.this); + boolean gaplessPlayback = prefs.getBoolean(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK, true); + if(!gaplessPlayback) { + nextPlaying = null; + nextPlayerState = IDLE; + return; + } + + int index = getCurrentPlayingIndex(); + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + index = index + 1; + break; + case ALL: + index = (index + 1) % size(); + break; + case SINGLE: + break; + default: + break; + } + } + + nextSetup = false; + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + if(index < size() && index != -1) { + nextPlaying = downloadList.get(index); + nextPlayingTask = new CheckCompletionTask(nextPlaying); + nextPlayingTask.start(); + } else { + nextPlaying = null; + setNextPlayerState(IDLE); + } + } + + @Override + public synchronized int getCurrentPlayingIndex() { + return downloadList.indexOf(currentPlaying); + } + + @Override + public DownloadFile getCurrentPlaying() { + return currentPlaying; + } + + @Override + public DownloadFile getCurrentDownloading() { + return currentDownloading; + } + + @Override + public List<DownloadFile> getSongs() { + return downloadList; + } + + @Override + public synchronized List<DownloadFile> getDownloads() { + List<DownloadFile> temp = new ArrayList<DownloadFile>(); + temp.addAll(downloadList); + temp.addAll(backgroundDownloadList); + return temp; + } + + @Override + public List<DownloadFile> getBackgroundDownloads() { + return backgroundDownloadList; + } + + /** Plays either the current song (resume) or the first/next one in queue. */ + public synchronized void play() + { + int current = getCurrentPlayingIndex(); + if (current == -1) { + play(0); + } else { + play(current); + } + } + + @Override + public synchronized void play(int index) { + play(index, true); + } + + private synchronized void play(int index, boolean start) { + if (index < 0 || index >= size()) { + reset(); + setCurrentPlaying(null, false); + lifecycleSupport.serializeDownloadQueue(); + } else { + if(nextPlayingTask != null) { + nextPlayingTask.cancel(); + nextPlayingTask = null; + } + setCurrentPlaying(index, start); + if (start) { + if (jukeboxEnabled) { + jukeboxService.skip(getCurrentPlayingIndex(), 0); + setPlayerState(STARTED); + } else { + bufferAndPlay(); + } + } + checkDownloads(); + setNextPlaying(); + } + } + private synchronized void playNext() { + if(nextPlaying != null && nextPlayerState == PlayerState.PREPARED) { + if(!nextSetup) { + playNext(true); + } else { + nextSetup = false; + playNext(false); + } + } else { + onSongCompleted(); + } + } + private synchronized void playNext(boolean start) { + // Swap the media players since nextMediaPlayer is ready to play + if(start) { + nextMediaPlayer.start(); + } else { + Log.i(TAG, "nextMediaPlayer already playing"); + } + MediaPlayer tmp = mediaPlayer; + mediaPlayer = nextMediaPlayer; + nextMediaPlayer = tmp; + setCurrentPlaying(nextPlaying, true); + setPlayerState(PlayerState.STARTED); + setupHandlers(currentPlaying, false); + setNextPlaying(); + + // Proxy should not be being used here since the next player was already setup to play + if(proxy != null) { + proxy.stop(); + proxy = null; + } + } + + /** Plays or resumes the playback, depending on the current player state. */ + public synchronized void togglePlayPause() { + if (playerState == PAUSED || playerState == COMPLETED || playerState == STOPPED) { + start(); + } else if (playerState == STOPPED || playerState == IDLE) { + autoPlayStart = true; + play(); + } else if (playerState == STARTED) { + pause(); + } + } + + @Override + public synchronized void seekTo(int position) { + try { + if (jukeboxEnabled) { + jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); + } else { + mediaPlayer.seekTo(position); + cachedPosition = position; + } + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void previous() { + int index = getCurrentPlayingIndex(); + if (index == -1) { + return; + } + + // Restart song if played more than five seconds. + if (getPlayerPosition() > 5000 || index == 0) { + play(index); + } else { + play(index - 1); + } + } + + @Override + public synchronized void next() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + play(index + 1); + } + } + + private void onSongCompleted() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + play(index + 1); + break; + case ALL: + play((index + 1) % size()); + break; + case SINGLE: + play(index); + break; + default: + break; + } + } + } + + @Override + public synchronized void pause() { + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(PAUSED); + } + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void stop() { + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(STOPPED); + } else if(playerState == PAUSED) { + setPlayerState(STOPPED); + } + } catch(Exception x) { + handleError(x); + } + } + + @Override + public synchronized void start() { + try { + if (jukeboxEnabled) { + jukeboxService.start(); + } else { + mediaPlayer.start(); + } + setPlayerState(STARTED); + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void reset() { + if (bufferTask != null) { + bufferTask.cancel(); + } + try { + setPlayerState(IDLE); + mediaPlayer.setOnErrorListener(null); + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized int getPlayerPosition() { + try { + if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { + return 0; + } + if (jukeboxEnabled) { + return jukeboxService.getPositionSeconds() * 1000; + } else { + return cachedPosition; + } + } catch (Exception x) { + handleError(x); + return 0; + } + } + + @Override + public synchronized int getPlayerDuration() { + if (currentPlaying != null) { + Integer duration = currentPlaying.getSong().getDuration(); + if (duration != null) { + return duration * 1000; + } + } + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { + try { + return mediaPlayer.getDuration(); + } catch (Exception x) { + handleError(x); + } + } + return 0; + } + + @Override + public PlayerState getPlayerState() { + return playerState; + } + + public synchronized void setPlayerState(final PlayerState playerState) { + Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); + + if (playerState == PAUSED) { + lifecycleSupport.serializeDownloadQueue(); + } + + boolean show = playerState == PlayerState.STARTED; + boolean pause = playerState == PlayerState.PAUSED; + boolean hide = playerState == PlayerState.STOPPED; + Util.broadcastPlaybackStatusChange(this, playerState); + + this.playerState = playerState; + + if(playerState == STARTED) { + Util.requestAudioFocus(this); + } + + if (show) { + Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); + } else if (pause) { + SharedPreferences prefs = Util.getPreferences(this); + if(prefs.getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false)) { + Util.showPlayingNotification(this, this, handler, currentPlaying.getSong()); + } else { + Util.hidePlayingNotification(this, this, handler); + } + } else if(hide) { + Util.hidePlayingNotification(this, this, handler); + } + mRemoteControl.setPlaybackState(playerState.getRemoteControlClientPlayState()); + + if (playerState == STARTED) { + scrobbler.scrobble(this, currentPlaying, false); + } else if (playerState == COMPLETED) { + scrobbler.scrobble(this, currentPlaying, true); + } + + if(playerState == STARTED && positionCache == null) { + positionCache = new PositionCache(); + Thread thread = new Thread(positionCache); + thread.start(); + } else if(playerState != STARTED && positionCache != null) { + positionCache.stop(); + positionCache = null; + } + } + + private class PositionCache implements Runnable { + boolean isRunning = true; + + public void stop() { + isRunning = false; + } + + @Override + public void run() { + // Stop checking position before the song reaches completion + while(isRunning) { + try { + if(mediaPlayer != null && playerState == STARTED) { + cachedPosition = mediaPlayer.getCurrentPosition(); + } + Thread.sleep(200L); + } + catch(Exception e) { + Log.w(TAG, "Crashed getting current position", e); + isRunning = false; + positionCache = null; + } + } + } + } + + private void setPlayerStateCompleted() { + Log.i(TAG, this.playerState.name() + " -> " + PlayerState.COMPLETED + " (" + currentPlaying + ")"); + this.playerState = PlayerState.COMPLETED; + if(positionCache != null) { + positionCache.stop(); + positionCache = null; + } + scrobbler.scrobble(this, currentPlaying, true); + } + + private synchronized void setNextPlayerState(PlayerState playerState) { + Log.i(TAG, "Next: " + this.nextPlayerState.name() + " -> " + playerState.name() + " (" + nextPlaying + ")"); + this.nextPlayerState = playerState; + } + + @Override + public void setSuggestedPlaylistName(String name, String id) { + this.suggestedPlaylistName = name; + this.suggestedPlaylistId = id; + } + + @Override + public String getSuggestedPlaylistName() { + return suggestedPlaylistName; + } + + @Override + public String getSuggestedPlaylistId() { + return suggestedPlaylistId; + } + + @Override + public boolean getEqualizerAvailable() { + return equalizerAvailable; + } + + @Override + public boolean getVisualizerAvailable() { + return visualizerAvailable; + } + + @Override + public EqualizerController getEqualizerController() { + if (equalizerAvailable && equalizerController == null) { + equalizerController = new EqualizerController(this, mediaPlayer); + if (!equalizerController.isAvailable()) { + equalizerController = null; + } else { + equalizerController.loadSettings(); + } + } + return equalizerController; + } + + @Override + public VisualizerController getVisualizerController() { + if (visualizerAvailable && visualizerController == null) { + visualizerController = new VisualizerController(this, mediaPlayer); + if (!visualizerController.isAvailable()) { + visualizerController = null; + } + } + return visualizerController; + } + + @Override + public boolean isJukeboxEnabled() { + return jukeboxEnabled; + } + + @Override + public void setJukeboxEnabled(boolean jukeboxEnabled) { + this.jukeboxEnabled = jukeboxEnabled; + jukeboxService.setEnabled(jukeboxEnabled); + if (jukeboxEnabled) { + reset(); + + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + } + } + + @Override + public void adjustJukeboxVolume(boolean up) { + jukeboxService.adjustVolume(up); + } + + private synchronized void bufferAndPlay() { + if(playerState != PREPARED) { + reset(); + + bufferTask = new BufferTask(currentPlaying, 0); + bufferTask.start(); + } else { + doPlay(currentPlaying, 0, true); + } + } + + private synchronized void doPlay(final DownloadFile downloadFile, final int position, final boolean start) { + try { + downloadFile.setPlaying(true); + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + isPartial = file.equals(downloadFile.getPartialFile()); + downloadFile.updateModificationDate(); + + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + setPlayerState(IDLE); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + String dataSource = file.getPath(); + if(isPartial) { + if (proxy == null) { + proxy = new StreamProxy(this); + proxy.start(); + } + dataSource = String.format("http://127.0.0.1:%d/%s", proxy.getPort(), URLEncoder.encode(dataSource, Constants.UTF_8)); + Log.i(TAG, "Data Source: " + dataSource); + } else if(proxy != null) { + proxy.stop(); + proxy = null; + } + mediaPlayer.setDataSource(dataSource); + setPlayerState(PREPARING); + + mediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { + public void onBufferingUpdate(MediaPlayer mp, int percent) { + Log.i(TAG, "Buffered " + percent + "%"); + if(percent == 100) { + mediaPlayer.setOnBufferingUpdateListener(null); + } + } + }); + + mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mediaPlayer) { + try { + setPlayerState(PREPARED); + + synchronized (DownloadServiceImpl.this) { + if (position != 0) { + Log.i(TAG, "Restarting player from position " + position); + mediaPlayer.seekTo(position); + } + cachedPosition = position; + + if (start) { + mediaPlayer.start(); + setPlayerState(STARTED); + } else { + setPlayerState(PAUSED); + } + } + + lifecycleSupport.serializeDownloadQueue(); + } catch (Exception x) { + handleError(x); + } + } + }); + + setupHandlers(downloadFile, isPartial); + + mediaPlayer.prepareAsync(); + } catch (Exception x) { + handleError(x); + } + } + + private synchronized void setupNext(final DownloadFile downloadFile) { + try { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + if(nextMediaPlayer != null) { + nextMediaPlayer.setOnCompletionListener(null); + nextMediaPlayer.release(); + nextMediaPlayer = null; + } + nextMediaPlayer = new MediaPlayer(); + nextMediaPlayer.setWakeMode(DownloadServiceImpl.this, PowerManager.PARTIAL_WAKE_LOCK); + try { + nextMediaPlayer.setAudioSessionId(mediaPlayer.getAudioSessionId()); + } catch(Throwable e) { + nextMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + } + nextMediaPlayer.setDataSource(file.getPath()); + setNextPlayerState(PREPARING); + + nextMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + public void onPrepared(MediaPlayer mp) { + try { + setNextPlayerState(PREPARED); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)) { + mediaPlayer.setNextMediaPlayer(nextMediaPlayer); + nextSetup = true; + } + } catch (Exception x) { + handleErrorNext(x); + } + } + }); + + nextMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + public boolean onError(MediaPlayer mediaPlayer, int what, int extra) { + Log.w(TAG, "Error on playing next " + "(" + what + ", " + extra + "): " + downloadFile); + return true; + } + }); + + nextMediaPlayer.prepareAsync(); + } catch (Exception x) { + handleErrorNext(x); + } + } + + private void setupHandlers(final DownloadFile downloadFile, final boolean isPartial) { + 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) { + Log.w(TAG, "Error on playing file " + "(" + what + ", " + extra + "): " + downloadFile); + int pos = cachedPosition; + reset(); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) { + playNext(); + } else { + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } + return true; + } + }); + + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock.acquire(60000); + + setPlayerStateCompleted(); + + int pos = cachedPosition; + Log.i(TAG, "Ending position " + pos + " of " + duration); + if (!isPartial || (downloadFile.isWorkDone() && (Math.abs(duration - pos) < 10000))) { + playNext(); + return; + } + + // If file is not completely downloaded, restart the playback from the current position. + synchronized (DownloadServiceImpl.this) { + if(downloadFile.isWorkDone()) { + // Complete was called early even though file is fully buffered + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + downloadFile.setPlaying(false); + doPlay(downloadFile, pos, true); + downloadFile.setPlaying(true); + } else { + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + bufferTask = new BufferTask(downloadFile, pos); + bufferTask.start(); + } + } + } + }); + } + + @Override + public void setSleepTimerDuration(int duration){ + timerDuration = duration; + } + + @Override + public void startSleepTimer(){ + if(sleepTimer != null){ + sleepTimer.cancel(); + sleepTimer.purge(); + } + + sleepTimer = new Timer(); + + sleepTimer.schedule(new TimerTask() { + @Override + public void run() { + pause(); + sleepTimer.cancel(); + sleepTimer.purge(); + sleepTimer = null; + } + + }, timerDuration * 60 * 1000); + } + + @Override + public void stopSleepTimer() { + if(sleepTimer != null){ + sleepTimer.cancel(); + sleepTimer.purge(); + } + sleepTimer = null; + } + + @Override + public boolean getSleepTimer() { + return sleepTimer != null; + } + + @Override + public void setVolume(float volume) { + if(mediaPlayer != null) { + mediaPlayer.setVolume(volume, volume); + } + } + + @Override + public synchronized void swap(boolean mainList, int from, int to) { + List<DownloadFile> list = mainList ? downloadList : backgroundDownloadList; + int max = list.size(); + if(to >= max) { + to = max - 1; + } + else if(to < 0) { + to = 0; + } + + int currentPlayingIndex = getCurrentPlayingIndex(); + DownloadFile movedSong = list.remove(from); + list.add(to, movedSong); + if(jukeboxEnabled && mainList) { + updateJukeboxPlaylist(); + } else if(mainList && (movedSong == nextPlaying || (currentPlayingIndex + 1) == to)) { + // Moving next playing or moving a song to be next playing + setNextPlaying(); + } + } + + private void handleError(Exception x) { + Log.w(TAG, "Media player error: " + x, x); + mediaPlayer.reset(); + setPlayerState(IDLE); + } + private void handleErrorNext(Exception x) { + Log.w(TAG, "Next Media player error: " + x, x); + nextMediaPlayer.reset(); + setNextPlayerState(IDLE); + } + + protected synchronized void checkDownloads() { + if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { + return; + } + + if (shufflePlay) { + checkShufflePlay(); + } + + if (jukeboxEnabled || !Util.isNetworkConnected(this)) { + return; + } + + if (downloadList.isEmpty() && backgroundDownloadList.isEmpty()) { + return; + } + + // Need to download current playing? + if (currentPlaying != null && currentPlaying != currentDownloading && !currentPlaying.isWorkDone()) { + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + + currentDownloading = currentPlaying; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + } + + // Find a suitable target for download. + else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed() && (!downloadList.isEmpty() || !backgroundDownloadList.isEmpty())) { + currentDownloading = null; + int n = size(); + + int preloaded = 0; + + if(n != 0) { + int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + if(start == -1) { + start = 0; + } + int i = start; + do { + DownloadFile downloadFile = downloadList.get(i); + if (!downloadFile.isWorkDone()) { + if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + if(i == (start + 1)) { + setNextPlayerState(DOWNLOADING); + } + break; + } + } else if (currentPlaying != downloadFile) { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + if((preloaded + 1 == n || preloaded >= Util.getPreloadCount(this) || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty()) { + for(int i = 0; i < backgroundDownloadList.size(); i++) { + DownloadFile downloadFile = backgroundDownloadList.get(i); + if(downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved())) { + // Don't need to keep list like active song list + backgroundDownloadList.remove(i); + revision++; + i--; + } else { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + break; + } + } + } + } + + // Delete obsolete .partial and .complete files. + cleanup(); + } + + private synchronized void checkShufflePlay() { + + // Get users desired random playlist size + SharedPreferences prefs = Util.getPreferences(this); + int listSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20")); + boolean wasEmpty = downloadList.isEmpty(); + + long revisionBefore = revision; + + // First, ensure that list is at least 20 songs long. + int size = size(); + if (size < listSize) { + for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) { + DownloadFile downloadFile = new DownloadFile(this, song, false); + downloadList.add(downloadFile); + revision++; + } + } + + int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + // Only shift playlist if playing song #5 or later. + if (currIndex > 4) { + int songsToShift = currIndex - 2; + for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) { + downloadList.add(new DownloadFile(this, song, false)); + downloadList.get(0).cancelDownload(); + downloadList.remove(0); + revision++; + } + } + + if (revisionBefore != revision) { + updateJukeboxPlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) { + play(0); + } + } + + public long getDownloadListUpdateRevision() { + return revision; + } + + private synchronized void cleanup() { + Iterator<DownloadFile> iterator = cleanupCandidates.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (downloadFile != currentPlaying && downloadFile != currentDownloading) { + if (downloadFile.cleanup()) { + iterator.remove(); + } + } + } + } + + private class BufferTask extends CancellableTask { + private final DownloadFile downloadFile; + private final int position; + private final long expectedFileSize; + private final File partialFile; + + public BufferTask(DownloadFile downloadFile, int position) { + this.downloadFile = downloadFile; + this.position = position; + partialFile = downloadFile.getPartialFile(); + + SharedPreferences prefs = Util.getPreferences(DownloadServiceImpl.this); + long bufferLength = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_BUFFER_LENGTH, "5")); + if(bufferLength == 0) { + // Set to seconds in a day, basically infinity + bufferLength = 86400L; + } + + // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. + int bitRate = downloadFile.getBitRate(); + long byteCount = Math.max(100000, bitRate * 1024L / 8L * bufferLength); + + // Find out how large the file should grow before resuming playback. + Log.i(TAG, "Buffering from position " + position + " and bitrate " + bitRate); + expectedFileSize = (position * bitRate / 8) + byteCount; + } + + @Override + public void execute() { + setPlayerState(DOWNLOADING); + + while (!bufferComplete()) { + Util.sleepQuietly(1000L); + if (isCancelled()) { + return; + } + } + doPlay(downloadFile, position, true); + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + long size = partialFile.length(); + + Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")"); + return completeFileAvailable || size >= expectedFileSize; + } + + @Override + public String toString() { + return "BufferTask (" + downloadFile + ")"; + } + } + + private class CheckCompletionTask extends CancellableTask { + private final DownloadFile downloadFile; + private final File partialFile; + + public CheckCompletionTask(DownloadFile downloadFile) { + setNextPlayerState(PlayerState.IDLE); + this.downloadFile = downloadFile; + if(downloadFile != null) { + partialFile = downloadFile.getPartialFile(); + } else { + partialFile = null; + } + } + + @Override + public void execute() { + if(downloadFile == null) { + return; + } + + // Do an initial sleep so this prepare can't compete with main prepare + Util.sleepQuietly(5000L); + while (!bufferComplete()) { + Util.sleepQuietly(5000L); + if (isCancelled()) { + return; + } + } + + // Start the setup of the next media player + mediaPlayerHandler.post(new Runnable() { + public void run() { + setupNext(downloadFile); + } + }); + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isWorkDone(); + Log.i(TAG, "Buffering next " + partialFile + " (" + partialFile.length() + ")"); + return completeFileAvailable && (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED); + } + + @Override + public String toString() { + return "CheckCompletionTask (" + downloadFile + ")"; + } + } +} diff --git a/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java b/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java new file mode 100644 index 00000000..ae378865 --- /dev/null +++ b/src/github/daneren2005/dsub/service/DownloadServiceLifecycleSupport.java @@ -0,0 +1,352 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.RemoteControlClient; +import android.os.AsyncTask; +import android.os.Build; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.KeyEvent; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.util.CacheCleaner; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.Util; + +/** + * @author Sindre Mehus + */ +public class DownloadServiceLifecycleSupport { + + private static final String TAG = DownloadServiceLifecycleSupport.class.getSimpleName(); + private static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser"; + + private final DownloadServiceImpl downloadService; + private ScheduledExecutorService executorService; + private BroadcastReceiver headsetEventReceiver; + private BroadcastReceiver ejectEventReceiver; + private PhoneStateListener phoneStateListener; + private boolean externalStorageAvailable= true; + private ReentrantLock lock = new ReentrantLock(); + private final AtomicBoolean setup = new AtomicBoolean(false); + private long lastPressTime = 0; + + /** + * This receiver manages the intent that could come from other applications. + */ + private BroadcastReceiver intentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.i(TAG, "intentReceiver.onReceive: " + action); + if (DownloadServiceImpl.CMD_PLAY.equals(action)) { + downloadService.play(); + } else if (DownloadServiceImpl.CMD_NEXT.equals(action)) { + downloadService.next(); + } else if (DownloadServiceImpl.CMD_PREVIOUS.equals(action)) { + downloadService.previous(); + } else if (DownloadServiceImpl.CMD_TOGGLEPAUSE.equals(action)) { + downloadService.togglePlayPause(); + } else if (DownloadServiceImpl.CMD_PAUSE.equals(action)) { + downloadService.pause(); + } else if (DownloadServiceImpl.CMD_STOP.equals(action)) { + downloadService.pause(); + downloadService.seekTo(0); + } + } + }; + + + public DownloadServiceLifecycleSupport(DownloadServiceImpl downloadService) { + this.downloadService = downloadService; + } + + public void onCreate() { + Runnable downloadChecker = new Runnable() { + @Override + public void run() { + try { + downloadService.checkDownloads(); + } catch (Throwable x) { + Log.e(TAG, "checkDownloads() failed.", x); + } + } + }; + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS); + + // Pause when headset is unplugged. + headsetEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Headset event for: " + intent.getExtras().get("name")); + if (intent.getExtras().getInt("state") == 0) { + if(!downloadService.isJukeboxEnabled()) { + downloadService.pause(); + } + } + } + }; + downloadService.registerReceiver(headsetEventReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + + // Stop when SD card is ejected. + ejectEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction()); + if (!externalStorageAvailable) { + Log.i(TAG, "External media is ejecting. Stopping playback."); + downloadService.reset(); + } else { + Log.i(TAG, "External media is available."); + } + } + }; + IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT); + ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); + ejectFilter.addDataScheme("file"); + downloadService.registerReceiver(ejectEventReceiver, ejectFilter); + + // React to media buttons. + Util.registerMediaButtonEventReceiver(downloadService); + + // Pause temporarily on incoming phone calls. + phoneStateListener = new MyPhoneStateListener(); + TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + + // Register the handler for outside intents. + IntentFilter commandFilter = new IntentFilter(); + commandFilter.addAction(DownloadServiceImpl.CMD_PLAY); + commandFilter.addAction(DownloadServiceImpl.CMD_TOGGLEPAUSE); + commandFilter.addAction(DownloadServiceImpl.CMD_PAUSE); + commandFilter.addAction(DownloadServiceImpl.CMD_STOP); + commandFilter.addAction(DownloadServiceImpl.CMD_PREVIOUS); + commandFilter.addAction(DownloadServiceImpl.CMD_NEXT); + downloadService.registerReceiver(intentReceiver, commandFilter); + + deserializeDownloadQueue(); + + new CacheCleaner(downloadService, downloadService).clean(); + } + + public void onStart(Intent intent) { + if (intent != null && intent.getExtras() != null) { + KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + if (event != null) { + handleKeyEvent(event); + } + } + } + + public void onDestroy() { + executorService.shutdown(); + serializeDownloadQueueNow(); + downloadService.clear(false); + downloadService.unregisterReceiver(ejectEventReceiver); + downloadService.unregisterReceiver(headsetEventReceiver); + downloadService.unregisterReceiver(intentReceiver); + + TelephonyManager telephonyManager = (TelephonyManager) downloadService.getSystemService(Context.TELEPHONY_SERVICE); + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + } + + public boolean isExternalStorageAvailable() { + return externalStorageAvailable; + } + + public void serializeDownloadQueue() { + if(!setup.get()) { + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + new SerializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + new SerializeTask().execute(); + } + } + + public void serializeDownloadQueueNow() { + List<DownloadFile> songs = new ArrayList<DownloadFile>(downloadService.getSongs()); + State state = new State(); + for (DownloadFile downloadFile : songs) { + state.songs.add(downloadFile.getSong()); + } + state.currentPlayingIndex = downloadService.getCurrentPlayingIndex(); + state.currentPlayingPosition = downloadService.getPlayerPosition(); + + Log.i(TAG, "Serialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + FileUtil.serialize(downloadService, state, FILENAME_DOWNLOADS_SER); + } + + private void deserializeDownloadQueue() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + new DeserializeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + new DeserializeTask().execute(); + } + } + private void deserializeDownloadQueueNow() { + State state = FileUtil.deserialize(downloadService, FILENAME_DOWNLOADS_SER); + if (state == null) { + return; + } + Log.i(TAG, "Deserialized currentPlayingIndex: " + state.currentPlayingIndex + ", currentPlayingPosition: " + state.currentPlayingPosition); + downloadService.restore(state.songs, state.currentPlayingIndex, state.currentPlayingPosition); + + // Work-around: Serialize again, as the restore() method creates a serialization without current playing info. + serializeDownloadQueue(); + } + + private void handleKeyEvent(KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() > 0) { + switch (event.getKeyCode()) { + case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.seekTo(downloadService.getPlayerPosition() - 10000); + break; + case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_NEXT: + downloadService.seekTo(downloadService.getPlayerPosition() + 10000); + break; + } + } else if(event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE: + downloadService.togglePlayPause(); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + if(lastPressTime < (System.currentTimeMillis() - 500)) { + lastPressTime = System.currentTimeMillis(); + downloadService.togglePlayPause(); + } else { + downloadService.next(); + } + break; + case RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + downloadService.previous(); + break; + case RemoteControlClient.FLAG_KEY_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_NEXT: + if (downloadService.getCurrentPlayingIndex() < downloadService.size() - 1) { + downloadService.next(); + } + break; + case RemoteControlClient.FLAG_KEY_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_STOP: + downloadService.stop(); + break; + case RemoteControlClient.FLAG_KEY_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PLAY: + if(downloadService.getPlayerState() != PlayerState.STARTED) { + downloadService.start(); + } + break; + case RemoteControlClient.FLAG_KEY_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + downloadService.pause(); + default: + break; + } + } + } + + /** + * Logic taken from packages/apps/Music. Will pause when an incoming + * call rings or if a call (incoming or outgoing) is connected. + */ + private class MyPhoneStateListener extends PhoneStateListener { + private boolean resumeAfterCall; + + @Override + public void onCallStateChanged(int state, String incomingNumber) { + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + case TelephonyManager.CALL_STATE_OFFHOOK: + if (downloadService.getPlayerState() == PlayerState.STARTED && !downloadService.isJukeboxEnabled()) { + resumeAfterCall = true; + downloadService.pause(); + } + break; + case TelephonyManager.CALL_STATE_IDLE: + if (resumeAfterCall) { + resumeAfterCall = false; + downloadService.start(); + } + break; + default: + break; + } + } + } + + private static class State implements Serializable { + private static final long serialVersionUID = -6346438781062572270L; + + private List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(); + private int currentPlayingIndex; + private int currentPlayingPosition; + } + + private class SerializeTask extends AsyncTask<Void, Void, Void> { + @Override + protected Void doInBackground(Void... params) { + if(lock.tryLock()) { + try { + serializeDownloadQueueNow(); + } finally { + lock.unlock(); + } + } + return null; + } + } + private class DeserializeTask extends AsyncTask<Void, Void, Void> { + @Override + protected Void doInBackground(Void... params) { + try { + lock.lock(); + deserializeDownloadQueueNow(); + setup.set(true); + } finally { + lock.unlock(); + } + return null; + } + } +} diff --git a/src/github/daneren2005/dsub/service/JukeboxService.java b/src/github/daneren2005/dsub/service/JukeboxService.java new file mode 100644 index 00000000..96b82336 --- /dev/null +++ b/src/github/daneren2005/dsub/service/JukeboxService.java @@ -0,0 +1,358 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.JukeboxStatus; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.service.parser.SubsonicRESTException; +import github.daneren2005.dsub.util.Util; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Provides an asynchronous interface to the remote jukebox on the Subsonic server. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class JukeboxService { + + private static final String TAG = JukeboxService.class.getSimpleName(); + private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L; + + private final Handler handler = new Handler(); + private final TaskQueue tasks = new TaskQueue(); + private final DownloadServiceImpl downloadService; + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture<?> statusUpdateFuture; + private final AtomicLong timeOfLastUpdate = new AtomicLong(); + private JukeboxStatus jukeboxStatus; + private float gain = 0.5f; + private VolumeToast volumeToast; + + // TODO: Report warning if queue fills up. + // TODO: Create shutdown method? + // TODO: Disable repeat. + // TODO: Persist RC state? + // TODO: Minimize status updates. + + public JukeboxService(DownloadServiceImpl downloadService) { + this.downloadService = downloadService; + new Thread() { + @Override + public void run() { + processTasks(); + } + }.start(); + } + + private synchronized void startStatusUpdate() { + stopStatusUpdate(); + Runnable updateTask = new Runnable() { + @Override + public void run() { + tasks.remove(GetStatus.class); + tasks.add(new GetStatus()); + } + }; + statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS, + STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS); + } + + private synchronized void stopStatusUpdate() { + if (statusUpdateFuture != null) { + statusUpdateFuture.cancel(false); + statusUpdateFuture = null; + } + } + + private void processTasks() { + while (true) { + JukeboxTask task = null; + try { + task = tasks.take(); + JukeboxStatus status = task.execute(); + onStatusUpdate(status); + } catch (Throwable x) { + onError(task, x); + } + } + } + + private void onStatusUpdate(JukeboxStatus jukeboxStatus) { + timeOfLastUpdate.set(System.currentTimeMillis()); + this.jukeboxStatus = jukeboxStatus; + + // Track change? + Integer index = jukeboxStatus.getCurrentPlayingIndex(); + if (index != null && index != -1 && index != downloadService.getCurrentPlayingIndex()) { + downloadService.setPlayerState(PlayerState.COMPLETED); + downloadService.setCurrentPlaying(index, true); + downloadService.setPlayerState(PlayerState.STARTED); + } + } + + private void onError(JukeboxTask task, Throwable x) { + if (x instanceof ServerTooOldException && !(task instanceof Stop)) { + disableJukeboxOnError(x, R.string.download_jukebox_server_too_old); + } else if (x instanceof OfflineException && !(task instanceof Stop)) { + disableJukeboxOnError(x, R.string.download_jukebox_offline); + } else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop)) { + disableJukeboxOnError(x, R.string.download_jukebox_not_authorized); + } else { + Log.e(TAG, "Failed to process jukebox task: " + x, x); + } + } + + private void disableJukeboxOnError(Throwable x, final int resourceId) { + Log.w(TAG, x.toString()); + handler.post(new Runnable() { + @Override + public void run() { + Util.toast(downloadService, resourceId, false); + } + }); + downloadService.setJukeboxEnabled(false); + } + + public void updatePlaylist() { + tasks.remove(Skip.class); + tasks.remove(Stop.class); + tasks.remove(Start.class); + + List<String> ids = new ArrayList<String>(); + for (DownloadFile file : downloadService.getDownloads()) { + ids.add(file.getSong().getId()); + } + tasks.add(new SetPlaylist(ids)); + } + + public void skip(final int index, final int offsetSeconds) { + tasks.remove(Skip.class); + tasks.remove(Stop.class); + tasks.remove(Start.class); + + startStatusUpdate(); + if (jukeboxStatus != null) { + jukeboxStatus.setPositionSeconds(offsetSeconds); + } + tasks.add(new Skip(index, offsetSeconds)); + downloadService.setPlayerState(PlayerState.STARTED); + } + + public void stop() { + tasks.remove(Stop.class); + tasks.remove(Start.class); + + stopStatusUpdate(); + tasks.add(new Stop()); + } + + public void start() { + tasks.remove(Stop.class); + tasks.remove(Start.class); + + startStatusUpdate(); + tasks.add(new Start()); + } + + public synchronized void adjustVolume(boolean up) { + float delta = up ? 0.1f : -0.1f; + gain += delta; + gain = Math.max(gain, 0.0f); + gain = Math.min(gain, 1.0f); + + tasks.remove(SetGain.class); + tasks.add(new SetGain(gain)); + + if (volumeToast == null) { + volumeToast = new VolumeToast(downloadService); + } + volumeToast.setVolume(gain); + } + + private MusicService getMusicService() { + return MusicServiceFactory.getMusicService(downloadService); + } + + public int getPositionSeconds() { + if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0) { + return 0; + } + + if (jukeboxStatus.isPlaying()) { + int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L); + return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate; + } + + return jukeboxStatus.getPositionSeconds(); + } + + public void setEnabled(boolean enabled) { + tasks.clear(); + if (enabled) { + updatePlaylist(); + } + stop(); + downloadService.setPlayerState(PlayerState.IDLE); + } + + private static class TaskQueue { + + private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<JukeboxTask>(); + + void add(JukeboxTask jukeboxTask) { + queue.add(jukeboxTask); + } + + JukeboxTask take() throws InterruptedException { + return queue.take(); + } + + void remove(Class<? extends JukeboxTask> clazz) { + try { + Iterator<JukeboxTask> iterator = queue.iterator(); + while (iterator.hasNext()) { + JukeboxTask task = iterator.next(); + if (clazz.equals(task.getClass())) { + iterator.remove(); + } + } + } catch (Throwable x) { + Log.w(TAG, "Failed to clean-up task queue.", x); + } + } + + void clear() { + queue.clear(); + } + } + + private abstract class JukeboxTask { + + abstract JukeboxStatus execute() throws Exception; + + @Override + public String toString() { + return getClass().getSimpleName(); + } + } + + private class GetStatus extends JukeboxTask { + @Override + JukeboxStatus execute() throws Exception { + return getMusicService().getJukeboxStatus(downloadService, null); + } + } + + private class SetPlaylist extends JukeboxTask { + + private final List<String> ids; + + SetPlaylist(List<String> ids) { + this.ids = ids; + } + + @Override + JukeboxStatus execute() throws Exception { + return getMusicService().updateJukeboxPlaylist(ids, downloadService, null); + } + } + + private class Skip extends JukeboxTask { + private final int index; + private final int offsetSeconds; + + Skip(int index, int offsetSeconds) { + this.index = index; + this.offsetSeconds = offsetSeconds; + } + + @Override + JukeboxStatus execute() throws Exception { + return getMusicService().skipJukebox(index, offsetSeconds, downloadService, null); + } + } + + private class Stop extends JukeboxTask { + @Override + JukeboxStatus execute() throws Exception { + return getMusicService().stopJukebox(downloadService, null); + } + } + + private class Start extends JukeboxTask { + @Override + JukeboxStatus execute() throws Exception { + return getMusicService().startJukebox(downloadService, null); + } + } + + private class SetGain extends JukeboxTask { + + private final float gain; + + private SetGain(float gain) { + this.gain = gain; + } + + @Override + JukeboxStatus execute() throws Exception { + return getMusicService().setJukeboxGain(gain, downloadService, null); + } + } + + private static class VolumeToast extends Toast { + + private final ProgressBar progressBar; + + public VolumeToast(Context context) { + super(context); + setDuration(Toast.LENGTH_SHORT); + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.jukebox_volume, null); + progressBar = (ProgressBar) view.findViewById(R.id.jukebox_volume_progress_bar); + + setView(view); + setGravity(Gravity.TOP, 0, 0); + } + + public void setVolume(float volume) { + progressBar.setProgress(Math.round(100 * volume)); + show(); + } + } +} diff --git a/src/github/daneren2005/dsub/service/MediaStoreService.java b/src/github/daneren2005/dsub/service/MediaStoreService.java new file mode 100644 index 00000000..4de77bf2 --- /dev/null +++ b/src/github/daneren2005/dsub/service/MediaStoreService.java @@ -0,0 +1,109 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.io.File; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.util.Log; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.FileUtil; + +/** + * @author Sindre Mehus + */ +public class MediaStoreService { + + private static final String TAG = MediaStoreService.class.getSimpleName(); + private static final Uri ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart"); + + private final Context context; + + public MediaStoreService(Context context) { + this.context = context; + } + + public void saveInMediaStore(DownloadFile downloadFile) { + MusicDirectory.Entry song = downloadFile.getSong(); + File songFile = downloadFile.getCompleteFile(); + + // Delete existing row in case the song has been downloaded before. + deleteFromMediaStore(downloadFile); + + ContentResolver contentResolver = context.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.TITLE, song.getTitle()); + values.put(MediaStore.Audio.AudioColumns.ARTIST, song.getArtist()); + values.put(MediaStore.Audio.AudioColumns.ALBUM, song.getAlbum()); + values.put(MediaStore.Audio.AudioColumns.TRACK, song.getTrack()); + values.put(MediaStore.Audio.AudioColumns.YEAR, song.getYear()); + values.put(MediaStore.MediaColumns.DATA, songFile.getAbsolutePath()); + values.put(MediaStore.MediaColumns.MIME_TYPE, song.getContentType()); + values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, 1); + + Uri uri = contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values); + + // Look up album, and add cover art if found. + Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Audio.AudioColumns.ALBUM_ID}, null, null, null); + if (cursor.moveToFirst()) { + int albumId = cursor.getInt(0); + insertAlbumArt(albumId, downloadFile); + } + cursor.close(); + } + + public void deleteFromMediaStore(DownloadFile downloadFile) { + ContentResolver contentResolver = context.getContentResolver(); + MusicDirectory.Entry song = downloadFile.getSong(); + File file = downloadFile.getCompleteFile(); + + int n = contentResolver.delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + MediaStore.Audio.AudioColumns.TITLE_KEY + "=? AND " + + MediaStore.MediaColumns.DATA + "=?", + new String[]{MediaStore.Audio.keyFor(song.getTitle()), file.getAbsolutePath()}); + if (n > 0) { + Log.i(TAG, "Deleting media store row for " + song); + } + } + + private void insertAlbumArt(int albumId, DownloadFile downloadFile) { + ContentResolver contentResolver = context.getContentResolver(); + + Cursor cursor = contentResolver.query(Uri.withAppendedPath(ALBUM_ART_URI, String.valueOf(albumId)), null, null, null, null); + if (!cursor.moveToFirst()) { + + // No album art found, add it. + File albumArtFile = FileUtil.getAlbumArtFile(context, downloadFile.getSong()); + if (albumArtFile.exists()) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId); + values.put(MediaStore.MediaColumns.DATA, albumArtFile.getPath()); + contentResolver.insert(ALBUM_ART_URI, values); + Log.i(TAG, "Added album art: " + albumArtFile); + } + } + cursor.close(); + } + +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/MusicService.java b/src/github/daneren2005/dsub/service/MusicService.java new file mode 100644 index 00000000..7aa878ab --- /dev/null +++ b/src/github/daneren2005/dsub/service/MusicService.java @@ -0,0 +1,140 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.util.List; + +import org.apache.http.HttpResponse; + +import android.content.Context; +import android.graphics.Bitmap; +import github.daneren2005.dsub.domain.ChatMessage; +import github.daneren2005.dsub.domain.Genre; +import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.JukeboxStatus; +import github.daneren2005.dsub.domain.Lyrics; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicFolder; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.PodcastEpisode; +import github.daneren2005.dsub.domain.SearchCritera; +import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.domain.Share; +import github.daneren2005.dsub.domain.Version; +import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.ProgressListener; + +/** + * @author Sindre Mehus + */ +public interface MusicService { + + void ping(Context context, ProgressListener progressListener) throws Exception; + + boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception; + + List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception; + + List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception; + + void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception; + + void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception; + + void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception; + + void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception; + + void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception; + + Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception; + + void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception; + + Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception; + + HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception; + + Version getLocalVersion(Context context) throws Exception; + + Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception; + + String getVideoUrl(int maxBitrate, Context context, String id); + + String getVideoStreamUrl(String format, int Bitrate, Context context, String id) throws Exception; + + String getHlsUrl(String id, int bitRate, Context context) throws Exception; + + JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception; + + JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception; + + JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception; + + JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception; + + JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception; + + JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception; + + void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception; + + List<Share> getShares(Context context, ProgressListener progressListener) throws Exception; + + List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception; + + void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception; + + List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception; + + List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception; + + MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception; + + void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception; + + void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception; + + void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception; + + void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception; + + void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception; + + int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception; +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/MusicServiceFactory.java b/src/github/daneren2005/dsub/service/MusicServiceFactory.java new file mode 100644 index 00000000..e04522ff --- /dev/null +++ b/src/github/daneren2005/dsub/service/MusicServiceFactory.java @@ -0,0 +1,36 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import android.content.Context; +import github.daneren2005.dsub.util.Util; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class MusicServiceFactory { + + private static final MusicService REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService()); + private static final MusicService OFFLINE_MUSIC_SERVICE = new OfflineMusicService(); + + public static MusicService getMusicService(Context context) { + return Util.isOffline(context) ? OFFLINE_MUSIC_SERVICE : REST_MUSIC_SERVICE; + } +} diff --git a/src/github/daneren2005/dsub/service/OfflineException.java b/src/github/daneren2005/dsub/service/OfflineException.java new file mode 100644 index 00000000..e3a8d460 --- /dev/null +++ b/src/github/daneren2005/dsub/service/OfflineException.java @@ -0,0 +1,32 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +/** + * Thrown by service methods that are not available in offline mode. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class OfflineException extends Exception { + + public OfflineException(String message) { + super(message); + } +} diff --git a/src/github/daneren2005/dsub/service/OfflineMusicService.java b/src/github/daneren2005/dsub/service/OfflineMusicService.java new file mode 100644 index 00000000..22fdcd9b --- /dev/null +++ b/src/github/daneren2005/dsub/service/OfflineMusicService.java @@ -0,0 +1,676 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.io.File; +import java.io.Reader; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.util.Log; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.Genre; +import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.domain.JukeboxStatus; +import github.daneren2005.dsub.domain.Lyrics; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.MusicFolder; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.PodcastEpisode; +import github.daneren2005.dsub.domain.SearchCritera; +import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.Util; +import java.io.*; +import java.util.Comparator; +import java.util.SortedSet; + +/** + * @author Sindre Mehus + */ +public class OfflineMusicService extends RESTMusicService { + private static final String TAG = OfflineMusicService.class.getSimpleName(); + + @Override + public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { + return true; + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List<Artist> artists = new ArrayList<Artist>(); + File root = FileUtil.getMusicDirectory(context); + for (File file : FileUtil.listFiles(root)) { + if (file.isDirectory()) { + Artist artist = new Artist(); + artist.setId(file.getPath()); + artist.setIndex(file.getName().substring(0, 1)); + artist.setName(file.getName()); + artists.add(artist); + } + } + + SharedPreferences prefs = Util.getPreferences(context); + String ignoredArticlesString = prefs.getString(Constants.CACHE_KEY_IGNORE, "The El La Los Las Le Les"); + final String[] ignoredArticles = ignoredArticlesString.split(" "); + + Collections.sort(artists, new Comparator<Artist>() { + public int compare(Artist lhsArtist, Artist rhsArtist) { + String lhs = lhsArtist.getName().toLowerCase(); + String rhs = rhsArtist.getName().toLowerCase(); + + char lhs1 = lhs.charAt(0); + char rhs1 = rhs.charAt(0); + + if(Character.isDigit(lhs1) && !Character.isDigit(rhs1)) { + return 1; + } else if(Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { + return -1; + } + + for(String article: ignoredArticles) { + int index = lhs.indexOf(article.toLowerCase() + " "); + if(index == 0) { + lhs = lhs.substring(article.length() + 1); + } + index = rhs.indexOf(article.toLowerCase() + " "); + if(index == 0) { + rhs = rhs.substring(article.length() + 1); + } + } + + return lhs.compareTo(rhs); + } + }); + + return new Indexes(0L, Collections.<Artist>emptyList(), artists); + } + + @Override + public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + File dir = new File(id); + MusicDirectory result = new MusicDirectory(); + result.setName(dir.getName()); + + Set<String> names = new HashSet<String>(); + + for (File file : FileUtil.listMediaFiles(dir)) { + String name = getName(file); + if (name != null & !names.contains(name)) { + names.add(name); + result.addChild(createEntry(context, file, name)); + } + } + result.sortChildren(); + return result; + } + + private String getName(File file) { + String name = file.getName(); + if (file.isDirectory()) { + return name; + } + + if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) { + return null; + } + + name = name.replace(".complete", ""); + return FileUtil.getBaseName(name); + } + + private MusicDirectory.Entry createEntry(Context context, File file, String name) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + entry.setDirectory(file.isDirectory()); + entry.setId(file.getPath()); + entry.setParent(file.getParent()); + entry.setSize(file.length()); + String root = FileUtil.getMusicDirectory(context).getPath(); + if(!file.getParentFile().getParentFile().getPath().equals(root)) { + entry.setGrandParent(file.getParentFile().getParent()); + } + entry.setPath(file.getPath().replaceFirst("^" + root + "/" , "")); + String title = name; + if (file.isFile()) { + File artistFolder = file.getParentFile().getParentFile(); + File albumFolder = file.getParentFile(); + if(artistFolder.getPath().equals(root)) { + entry.setArtist(albumFolder.getName()); + } else { + entry.setArtist(artistFolder.getName()); + } + entry.setAlbum(albumFolder.getName()); + + int index = name.indexOf('-'); + if(index != -1) { + try { + entry.setTrack(Integer.parseInt(name.substring(0, index))); + title = title.substring(index + 1); + } catch(Exception e) { + // Failed parseInt, just means track filled out + } + } + + try { + MediaMetadataRetriever metadata = new MediaMetadataRetriever(); + metadata.setDataSource(file.getAbsolutePath()); + String discNumber = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER); + if(discNumber == null) { + discNumber = "1/1"; + } + int slashIndex = discNumber.indexOf("/"); + if(slashIndex > 0) { + discNumber = discNumber.substring(0, slashIndex); + } + entry.setDiscNumber(Integer.parseInt(discNumber)); + String bitrate = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE); + entry.setBitRate(Integer.parseInt((bitrate != null) ? bitrate : "0") / 1000); + String length = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + entry.setDuration(Integer.parseInt(length) / 1000); + String artist = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); + if(artist != null) { + entry.setArtist(artist); + } + String album = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); + if(album != null) { + entry.setAlbum(album); + } + metadata.release(); + } catch(Exception e) { + Log.i(TAG, "Device doesn't properly support MediaMetadataRetreiver"); + } + } + + entry.setTitle(title); + entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", ""))); + + File albumArt = FileUtil.getAlbumArtFile(context, entry); + if (albumArt.exists()) { + entry.setCoverArt(albumArt.getPath()); + } + if(FileUtil.isVideoFile(file)) { + entry.setVideo(true); + } + return entry; + } + + @Override + public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception { + try { + return FileUtil.getAlbumArtBitmap(context, entry, size); + } catch(Exception e) { + return null; + } + } + + @Override + public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Music folders not available in offline mode"); + } + + @Override + public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { + List<Artist> artists = new ArrayList<Artist>(); + List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>(); + List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(); + File root = FileUtil.getMusicDirectory(context); + int closeness = 0; + for (File artistFile : FileUtil.listFiles(root)) { + String artistName = artistFile.getName(); + if (artistFile.isDirectory()) { + if((closeness = matchCriteria(criteria, artistName)) > 0) { + Artist artist = new Artist(); + artist.setId(artistFile.getPath()); + artist.setIndex(artistFile.getName().substring(0, 1)); + artist.setName(artistName); + artist.setCloseness(closeness); + artists.add(artist); + } + + recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs); + } + } + + Collections.sort(artists, new Comparator<Artist>() { + public int compare(Artist lhs, Artist rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + Collections.sort(albums, new Comparator<MusicDirectory.Entry>() { + public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + Collections.sort(songs, new Comparator<MusicDirectory.Entry>() { + public int compare(MusicDirectory.Entry lhs, MusicDirectory.Entry rhs) { + if(lhs.getCloseness() == rhs.getCloseness()) { + return 0; + } + else if(lhs.getCloseness() > rhs.getCloseness()) { + return -1; + } + else { + return 1; + } + } + }); + + return new SearchResult(artists, albums, songs); + } + + private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs) { + int closeness; + for(File albumFile : FileUtil.listMediaFiles(file)) { + if(albumFile.isDirectory()) { + String albumName = getName(albumFile); + if((closeness = matchCriteria(criteria, albumName)) > 0) { + MusicDirectory.Entry album = createEntry(context, albumFile, albumName); + album.setArtist(artistName); + album.setCloseness(closeness); + albums.add(album); + } + + for(File songFile : FileUtil.listMediaFiles(albumFile)) { + String songName = getName(songFile); + if(songFile.isDirectory()) { + recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs); + } + else if((closeness = matchCriteria(criteria, songName)) > 0){ + MusicDirectory.Entry song = createEntry(context, albumFile, songName); + song.setArtist(artistName); + song.setAlbum(albumName); + song.setCloseness(closeness); + songs.add(song); + } + } + } + else { + String songName = getName(albumFile); + if((closeness = matchCriteria(criteria, songName)) > 0) { + MusicDirectory.Entry song = createEntry(context, albumFile, songName); + song.setArtist(artistName); + song.setAlbum(songName); + song.setCloseness(closeness); + songs.add(song); + } + } + } + } + private int matchCriteria(SearchCritera criteria, String name) { + String query = criteria.getQuery().toLowerCase(); + String[] queryParts = query.split(" "); + String[] nameParts = name.toLowerCase().split(" "); + + int closeness = 0; + for(String queryPart : queryParts) { + for(String namePart : nameParts) { + if(namePart.equals(queryPart)) { + closeness++; + } + } + } + + return closeness; + } + + @Override + public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List<Playlist> playlists = new ArrayList<Playlist>(); + File root = FileUtil.getPlaylistDirectory(); + String lastServer = null; + boolean removeServer = true; + for (File folder : FileUtil.listFiles(root)) { + if(folder.isDirectory()) { + String server = folder.getName(); + SortedSet<File> fileList = FileUtil.listFiles(folder); + for(File file: fileList) { + if(FileUtil.isPlaylistFile(file)) { + String id = file.getName(); + String filename = server + ": " + FileUtil.getBaseName(id); + Playlist playlist = new Playlist(server, filename); + playlists.add(playlist); + } + } + + if(!server.equals(lastServer) && fileList.size() > 0) { + if(lastServer != null) { + removeServer = false; + } + lastServer = server; + } + } else { + // Delete legacy playlist files + try { + folder.delete(); + } catch(Exception e) { + Log.w(TAG, "Failed to delete old playlist file: " + folder.getName()); + } + } + } + + if(removeServer) { + for(Playlist playlist: playlists) { + playlist.setName(playlist.getName().substring(playlist.getId().length() + 2)); + } + } + return playlists; + } + + @Override + public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception { + DownloadService downloadService = DownloadServiceImpl.getInstance(); + if (downloadService == null) { + return new MusicDirectory(); + } + + Reader reader = null; + BufferedReader buffer = null; + try { + int firstIndex = name.indexOf(id); + if(firstIndex != -1) { + name = name.substring(id.length() + 2); + } + + File playlistFile = FileUtil.getPlaylistFile(id, name); + reader = new FileReader(playlistFile); + buffer = new BufferedReader(reader); + + MusicDirectory playlist = new MusicDirectory(); + String line = buffer.readLine(); + if(!"#EXTM3U".equals(line)) return playlist; + + while( (line = buffer.readLine()) != null ){ + File entryFile = new File(line); + String entryName = getName(entryFile); + if(entryFile.exists() && entryName != null){ + playlist.addChild(createEntry(context, entryFile, entryName)); + } + } + + return playlist; + } finally { + Util.close(buffer); + Util.close(reader); + } + } + + @Override + public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Playlists not available in offline mode"); + } + + @Override + public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Playlists not available in offline mode"); + } + + @Override + public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Updating playlist not available in offline mode"); + } + + @Override + public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Removing from playlist not available in offline mode"); + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Overwriting playlist not available in offline mode"); + } + + @Override + public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Updating playlist not available in offline mode"); + } + + @Override + public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Lyrics not available in offline mode"); + } + + @Override + public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception { + if(!submission) { + return; + } + + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + + SharedPreferences offline = Util.getOfflineSync(context); + int scrobbles = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); + scrobbles++; + SharedPreferences.Editor offlineEditor = offline.edit(); + + if(id.indexOf(cacheLocn) != -1) { + String scrobbleSearchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + offlineEditor.putString(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles, scrobbleSearchCriteria); + offlineEditor.remove(Constants.OFFLINE_SCROBBLE_ID + scrobbles); + } else { + offlineEditor.putString(Constants.OFFLINE_SCROBBLE_ID + scrobbles, id); + offlineEditor.remove(Constants.OFFLINE_SCROBBLE_SEARCH + scrobbles); + } + + offlineEditor.putLong(Constants.OFFLINE_SCROBBLE_TIME + scrobbles, System.currentTimeMillis()); + offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, scrobbles); + offlineEditor.commit(); + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Album lists not available in offline mode"); + } + + @Override + public String getVideoUrl(int maxBitrate, Context context, String id) { + return null; + } + + @Override + public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception { + return null; + } + + @Override + public String getHlsUrl(String id, int bitRate, Context context) throws Exception { + return null; + } + + @Override + public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + + SharedPreferences offline = Util.getOfflineSync(context); + int stars = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0); + stars++; + SharedPreferences.Editor offlineEditor = offline.edit(); + + if(id.indexOf(cacheLocn) != -1) { + String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + offlineEditor.putString(Constants.OFFLINE_STAR_SEARCH + stars, searchCriteria); + offlineEditor.remove(Constants.OFFLINE_STAR_ID + stars); + } else { + offlineEditor.putString(Constants.OFFLINE_STAR_ID + stars, id); + offlineEditor.remove(Constants.OFFLINE_STAR_SEARCH + stars); + } + + offlineEditor.putBoolean(Constants.OFFLINE_STAR_SETTING + stars, starred); + offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, stars); + offlineEditor.commit(); + } + + @Override + public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Getting Genres not available in offline mode"); + } + + @Override + public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Getting Songs By Genre not available in offline mode"); + } + + @Override + public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { + File root = FileUtil.getMusicDirectory(context); + List<File> children = new LinkedList<File>(); + listFilesRecursively(root, children); + MusicDirectory result = new MusicDirectory(); + + if (children.isEmpty()) { + return result; + } + Random random = new Random(); + for (int i = 0; i < size; i++) { + File file = children.get(random.nextInt(children.size())); + result.addChild(createEntry(context, file, getName(file))); + } + + return result; + } + + @Override + public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List<PodcastChannel> channels = new ArrayList<PodcastChannel>(); + + File dir = FileUtil.getPodcastDirectory(context); + String line; + for(File file: dir.listFiles()) { + BufferedReader br = new BufferedReader(new FileReader(file)); + while ((line = br.readLine()) != null && !"".equals(line)) { + PodcastChannel channel = new PodcastChannel(); + channel.setId(line); + channel.setName(line); + channel.setStatus("completed"); + + if(FileUtil.getPodcastDirectory(context, channel).exists()) { + channels.add(channel); + } + } + br.close(); + } + + return channels; + } + + @Override + public MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception { + return getMusicDirectory(FileUtil.getPodcastDirectory(context, id).getPath(), null, false, context, progressListener); + } + + @Override + public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Getting Podcasts not available in offline mode"); + } + + @Override + public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{ + throw new OfflineException("Getting Podcasts not available in offline mode"); + } + + @Override + public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception{ + throw new OfflineException("Getting Podcasts not available in offline mode"); + } + + @Override + public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ + throw new OfflineException("Getting Podcasts not available in offline mode"); + } + + @Override + public void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ + throw new OfflineException("Getting Podcasts not available in offline mode"); + } + + @Override + public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ + throw new OfflineException("Offline scrobble cached can not be processes while in offline mode"); + } + + private void listFilesRecursively(File parent, List<File> children) { + for (File file : FileUtil.listMediaFiles(parent)) { + if (file.isFile()) { + children.add(file); + } else { + listFilesRecursively(file, children); + } + } + } +} diff --git a/src/github/daneren2005/dsub/service/RESTMusicService.java b/src/github/daneren2005/dsub/service/RESTMusicService.java new file mode 100644 index 00000000..e81c2b95 --- /dev/null +++ b/src/github/daneren2005/dsub/service/RESTMusicService.java @@ -0,0 +1,1296 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.FileReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnPerRouteBean; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.scheme.SocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HttpContext; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.*; +import github.daneren2005.dsub.domain.MusicDirectory.Entry; +import github.daneren2005.dsub.service.parser.AlbumListParser; +import github.daneren2005.dsub.service.parser.ChatMessageParser; +import github.daneren2005.dsub.service.parser.ErrorParser; +import github.daneren2005.dsub.service.parser.GenreParser; +import github.daneren2005.dsub.service.parser.IndexesParser; +import github.daneren2005.dsub.service.parser.JukeboxStatusParser; +import github.daneren2005.dsub.service.parser.LicenseParser; +import github.daneren2005.dsub.service.parser.LyricsParser; +import github.daneren2005.dsub.service.parser.MusicDirectoryParser; +import github.daneren2005.dsub.service.parser.MusicFoldersParser; +import github.daneren2005.dsub.service.parser.PlaylistParser; +import github.daneren2005.dsub.service.parser.PlaylistsParser; +import github.daneren2005.dsub.service.parser.PodcastChannelParser; +import github.daneren2005.dsub.service.parser.PodcastEntryParser; +import github.daneren2005.dsub.service.parser.RandomSongsParser; +import github.daneren2005.dsub.service.parser.SearchResult2Parser; +import github.daneren2005.dsub.service.parser.SearchResultParser; +import github.daneren2005.dsub.service.parser.ShareParser; +import github.daneren2005.dsub.service.parser.StarredListParser; +import github.daneren2005.dsub.service.parser.VersionParser; +import github.daneren2005.dsub.service.ssl.SSLSocketFactory; +import github.daneren2005.dsub.service.ssl.TrustSelfSignedStrategy; +import github.daneren2005.dsub.util.CancellableTask; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.Util; +import java.io.*; + +/** + * @author Sindre Mehus + */ +public class RESTMusicService implements MusicService { + + private static final String TAG = RESTMusicService.class.getSimpleName(); + + private static final int SOCKET_CONNECT_TIMEOUT = 10 * 1000; + private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000; + private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000; + private static final int SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS = 60 * 1000; + private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000; + + // Allow 20 seconds extra timeout per MB offset. + private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0; + + /** + * URL from which to fetch latest versions. + */ + private static final String VERSION_URL = "http://subsonic.org/backend/version.view"; + + private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5; + private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L; + + private final DefaultHttpClient httpClient; + private long redirectionLastChecked; + private int redirectionNetworkType = -1; + private String redirectFrom; + private String redirectTo; + private final ThreadSafeClientConnManager connManager; + + public RESTMusicService() { + + // Create and initialize default HTTP parameters + HttpParams params = new BasicHttpParams(); + ConnManagerParams.setMaxTotalConnections(params, 20); + ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(20)); + HttpConnectionParams.setConnectionTimeout(params, SOCKET_CONNECT_TIMEOUT); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_DEFAULT); + + // Turn off stale checking. Our connections break all the time anyway, + // and it's not worth it to pay the penalty of checking every time. + HttpConnectionParams.setStaleCheckingEnabled(params, false); + + // Create and initialize scheme registry + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + schemeRegistry.register(new Scheme("https", createSSLSocketFactory(), 443)); + + // Create an HttpClient with the ThreadSafeClientConnManager. + // This connection manager must be used if more than one thread will + // be using the HttpClient. + connManager = new ThreadSafeClientConnManager(params, schemeRegistry); + httpClient = new DefaultHttpClient(connManager, params); + } + + private SocketFactory createSSLSocketFactory() { + try { + return new SSLSocketFactory(new TrustSelfSignedStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + } catch (Throwable x) { + Log.e(TAG, "Failed to create custom SSL socket factory, using default.", x); + return org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory(); + } + } + + @Override + public void ping(Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "ping", null); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { + + Reader reader = getReader(context, progressListener, "getLicense", null); + try { + ServerInfo serverInfo = new LicenseParser(context).parse(reader); + return serverInfo.isLicenseValid(); + } finally { + Util.close(reader); + } + } + + public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + + List<MusicFolder> cachedMusicFolders = readCachedMusicFolders(context); + if (cachedMusicFolders != null && !refresh) { + return cachedMusicFolders; + } + + Reader reader = getReader(context, progressListener, "getMusicFolders", null); + try { + List<MusicFolder> musicFolders = new MusicFoldersParser(context).parse(reader, progressListener); + writeCachedMusicFolders(context, musicFolders); + return musicFolders; + } finally { + Util.close(reader); + } + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Indexes cachedIndexes = readCachedIndexes(context, musicFolderId); + if (cachedIndexes != null && !refresh) { + return cachedIndexes; + } + + long lastModified = (cachedIndexes == null || refresh) ? 0L : cachedIndexes.getLastModified(); + + List<String> parameterNames = new ArrayList<String>(); + List<Object> parameterValues = new ArrayList<Object>(); + + if(lastModified != 0L) { + parameterNames.add("ifModifiedSince"); + parameterValues.add(lastModified); + } + + if (musicFolderId != null) { + parameterNames.add("musicFolderId"); + parameterValues.add(musicFolderId); + } + + Reader reader = getReader(context, progressListener, "getIndexes", null, parameterNames, parameterValues); + try { + Indexes indexes = new IndexesParser(context).parse(reader, progressListener); + if (indexes != null) { + writeCachedIndexes(context, indexes, musicFolderId); + return indexes; + } + if(cachedIndexes != null) { + return cachedIndexes; + } else { + return new Indexes(0, new ArrayList<Artist>(), new ArrayList<Artist>()); + } + } finally { + Util.close(reader); + } + } + + private Indexes readCachedIndexes(Context context, String musicFolderId) { + String filename = getCachedIndexesFilename(context, musicFolderId); + return FileUtil.deserialize(context, filename); + } + + private void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) { + String filename = getCachedIndexesFilename(context, musicFolderId); + FileUtil.serialize(context, indexes, filename); + } + + private String getCachedIndexesFilename(Context context, String musicFolderId) { + String s = Util.getRestUrl(context, null) + musicFolderId; + return "indexes-" + Math.abs(s.hashCode()) + ".ser"; + } + + private ArrayList<MusicFolder> readCachedMusicFolders(Context context) { + String filename = getCachedMusicFoldersFilename(context); + return FileUtil.deserialize(context, filename); + } + + private void writeCachedMusicFolders(Context context, List<MusicFolder> musicFolders) { + String filename = getCachedMusicFoldersFilename(context); + FileUtil.serialize(context, new ArrayList<MusicFolder>(musicFolders), filename); + } + + private String getCachedMusicFoldersFilename(Context context) { + String s = Util.getRestUrl(context, null); + return "musicFolders-" + Math.abs(s.hashCode()) + ".ser"; + } + + @Override + public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(id.indexOf(cacheLocn) != -1) { + String search = Util.parseOfflineIDSearch(context, id, cacheLocn); + SearchCritera critera = new SearchCritera(search, 1, 1, 0); + SearchResult result = searchNew(critera, context, progressListener); + if(result.getArtists().size() == 1) { + id = result.getArtists().get(0).getId(); + } else if(result.getAlbums().size() == 1) { + id = result.getAlbums().get(0).getId(); + } + } + + Reader reader = getReader(context, progressListener, "getMusicDirectory", null, "id", id); + try { + return new MusicDirectoryParser(context).parse(name, reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public SearchResult search(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + try { + return searchNew(critera, context, progressListener); + } catch (ServerTooOldException x) { + // Ensure backward compatibility with REST 1.3. + return searchOld(critera, context, progressListener); + } + } + + /** + * Search using the "search" REST method. + */ + private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + List<String> parameterNames = Arrays.asList("any", "songCount"); + List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount()); + Reader reader = getReader(context, progressListener, "search", null, parameterNames, parameterValues); + try { + return new SearchResultParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + /** + * Search using the "search2" REST method, available in 1.4.0 and later. + */ + private SearchResult searchNew(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.4", null); + + List<String> parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount"); + List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getArtistCount(), + critera.getAlbumCount(), critera.getSongCount()); + Reader reader = getReader(context, progressListener, "search2", null, parameterNames, parameterValues); + try { + return new SearchResult2Parser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST); + + Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id); + try { + MusicDirectory playlist = new PlaylistParser(context).parse(reader, progressListener); + + File playlistFile = FileUtil.getPlaylistFile(Util.getServerName(context), name); + FileWriter fw = new FileWriter(playlistFile); + BufferedWriter bw = new BufferedWriter(fw); + try { + fw.write("#EXTM3U\n"); + for (MusicDirectory.Entry e : playlist.getChildren()) { + String filePath = FileUtil.getSongFile(context, e).getAbsolutePath(); + if(! new File(filePath).exists()){ + String ext = FileUtil.getExtension(filePath); + String base = FileUtil.getBaseName(filePath); + filePath = base + ".complete." + ext; + } + fw.write(filePath + "\n"); + } + } catch(Exception e) { + Log.w(TAG, "Failed to save playlist: " + name); + } finally { + bw.close(); + fw.close(); + } + + return playlist; + } finally { + Util.close(reader); + } + } + + @Override + public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getPlaylists", null); + try { + return new PlaylistsParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception { + List<String> parameterNames = new LinkedList<String>(); + List<Object> parameterValues = new LinkedList<Object>(); + + if (id != null) { + parameterNames.add("playlistId"); + parameterValues.add(id); + } + if (name != null) { + parameterNames.add("name"); + parameterValues.add(name); + } + for (MusicDirectory.Entry entry : entries) { + parameterNames.add("songId"); + parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener)); + } + + Reader reader = getReader(context, progressListener, "createPlaylist", null, parameterNames, parameterValues); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "deletePlaylist", null, "id", id); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.8", "Updating playlists is not supported."); + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + names.add("playlistId"); + values.add(id); + for(MusicDirectory.Entry song: toAdd) { + names.add("songIdToAdd"); + values.add(getOfflineSongId(song.getId(), context, progressListener)); + } + Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.8", "Updating playlists is not supported."); + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + names.add("playlistId"); + values.add(id); + for(Integer song: toRemove) { + names.add("songIndexToRemove"); + values.add(song); + } + Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.8", "Updating playlists is not supported."); + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + names.add("playlistId"); + values.add(id); + names.add("name"); + values.add(name); + for(MusicDirectory.Entry song: toAdd) { + names.add("songIdToAdd"); + values.add(song.getId()); + } + for(int i = 0; i < toRemove; i++) { + names.add("songIndexToRemove"); + values.add(i); + } + Reader reader = getReader(context, progressListener, "updatePlaylist", null, names, values); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.8", "Updating playlists is not supported."); + Reader reader = getReader(context, progressListener, "updatePlaylist", null, Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub)); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getLyrics", null, Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title)); + try { + return new LyricsParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception { + id = getOfflineSongId(id, context, progressListener); + scrobble(id, submission, 0, context, progressListener); + } + + public void scrobble(String id, boolean submission, long time, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.5", "Scrobbling not supported."); + Reader reader; + if(time > 0){ + checkServerVersion(context, "1.8", "Scrobbling with a time not supported."); + reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time)); + } + else + reader = getReader(context, progressListener, "scrobble", null, Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission)); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getAlbumList", + null, Arrays.asList("type", "size", "offset"), Arrays.<Object>asList(type, size, offset)); + try { + return new AlbumListParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getStarred", null); + try { + return new StarredListParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List<String> names = new ArrayList<String>(); + List<Object> values = new ArrayList<Object>(); + + names.add("size"); + values.add(size); + + if (musicFolderId != null && !"".equals(musicFolderId)) { + names.add("musicFolderId"); + values.add(musicFolderId); + } + if(genre != null && !"".equals(genre)) { + names.add("genre"); + values.add(genre); + } + if(startYear != null && !"".equals(startYear)) { + names.add("fromYear"); + values.add(startYear); + } + if(endYear != null && !"".equals(endYear)) { + names.add("toYear"); + values.add(endYear); + } + + Reader reader = getReader(context, progressListener, "getRandomSongs", params, names, values); + try { + return new RandomSongsParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public Version getLocalVersion(Context context) throws Exception { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo("github.daneren2005.dsub", 0); + return new Version(packageInfo.versionName); + } + + @Override + public Version getLatestVersion(Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReaderForURL(context, VERSION_URL, null, null, null, progressListener); + try { + return new VersionParser().parse(reader); + } finally { + Util.close(reader); + } + } + + private void checkServerVersion(Context context, String version, String text) throws ServerTooOldException { + Version serverVersion = Util.getServerRestVersion(context); + Version requiredVersion = new Version(version); + boolean ok = serverVersion == null || serverVersion.compareTo(requiredVersion) >= 0; + + if (!ok) { + throw new ServerTooOldException(text, serverVersion, requiredVersion); + } + } + + @Override + public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, int saveSize, ProgressListener progressListener) throws Exception { + + // Synchronize on the entry so that we don't download concurrently for the same song. + synchronized (entry) { + + // Use cached file, if existing. + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size); + if (bitmap != null) { + return bitmap; + } + + String url = Util.getRestUrl(context, "getCoverArt"); + + InputStream in = null; + try { + List<String> parameterNames = Arrays.asList("id", "size"); + List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt(), saveSize); + HttpEntity entity = getEntityForURL(context, url, null, parameterNames, parameterValues, progressListener); + in = entity.getContent(); + + // If content type is XML, an error occured. Get it. + String contentType = Util.getContentType(entity); + if (contentType != null && contentType.startsWith("text/xml")) { + new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8)); + return null; // Never reached. + } + + byte[] bytes = Util.toByteArray(in); + + File albumDir = FileUtil.getAlbumDirectory(context, entry); + if (albumDir.exists()) { + OutputStream out = null; + try { + out = new FileOutputStream(FileUtil.getAlbumArtFile(albumDir)); + out.write(bytes); + } finally { + Util.close(out); + } + } + + bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + if(size != saveSize) { + bitmap = Bitmap.createScaledBitmap(bitmap, size, size, true); + } + return bitmap; + + } finally { + Util.close(in); + } + } + } + + @Override + public HttpResponse getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception { + + String url = Util.getRestUrl(context, "stream"); + + // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is + // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server. + // In that case, the server uses a long time before sending any data, causing the client to time out. + HttpParams params = new BasicHttpParams(); + int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE); + HttpConnectionParams.setSoTimeout(params, timeout); + + // Add "Range" header if offset is given. + List<Header> headers = new ArrayList<Header>(); + if (offset > 0) { + headers.add(new BasicHeader("Range", "bytes=" + offset + "-")); + } + List<String> parameterNames = Arrays.asList("id", "maxBitRate"); + List<Object> parameterValues = Arrays.<Object>asList(song.getId(), maxBitrate); + HttpResponse response = getResponseForURL(context, url, params, parameterNames, parameterValues, headers, null, task); + + // If content type is XML, an error occurred. Get it. + String contentType = Util.getContentType(response.getEntity()); + if (contentType != null && contentType.startsWith("text/xml")) { + InputStream in = response.getEntity().getContent(); + try { + new ErrorParser(context).parse(new InputStreamReader(in, Constants.UTF_8)); + } finally { + Util.close(in); + } + } + + return response; + } + + @Override + public String getVideoUrl(int maxBitrate, Context context, String id) { + StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "videoPlayer")); + builder.append("&id=").append(id); + builder.append("&maxBitRate=").append(maxBitrate); + builder.append("&autoplay=true"); + + String url = rewriteUrlWithRedirect(context, builder.toString()); + Log.i(TAG, "Using video URL: " + url); + return url; + } + + @Override + public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception { + StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "stream")); + builder.append("&id=").append(id); + if(!"raw".equals(format)) { + checkServerVersion(context, "1.9", "Video streaming not supported."); + builder.append("&maxBitRate=").append(maxBitrate); + } + builder.append("&format=").append(format); + + String url = rewriteUrlWithRedirect(context, builder.toString()); + Log.i(TAG, "Using video URL: " + url); + return url; + } + + @Override + public String getHlsUrl(String id, int bitRate, Context context) throws Exception { + checkServerVersion(context, "1.9", "HLS video streaming not supported."); + + StringBuilder builder = new StringBuilder(Util.getRestUrl(context, "hls")); + builder.append("&id=").append(id); + if(bitRate > 0) { + builder.append("&bitRate=").append(bitRate); + } + + String url = rewriteUrlWithRedirect(context, builder.toString()); + Log.i(TAG, "Using hls URL: " + url); + return url; + } + + @Override + public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception { + int n = ids.size(); + List<String> parameterNames = new ArrayList<String>(n + 1); + parameterNames.add("action"); + for (int i = 0; i < n; i++) { + parameterNames.add("id"); + } + List<Object> parameterValues = new ArrayList<Object>(); + parameterValues.add("set"); + parameterValues.addAll(ids); + + return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); + } + + @Override + public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception { + List<String> parameterNames = Arrays.asList("action", "index", "offset"); + List<Object> parameterValues = Arrays.<Object>asList("skip", index, offsetSeconds); + return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); + } + + @Override + public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception { + return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("stop")); + } + + @Override + public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception { + return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("start")); + } + + @Override + public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception { + return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("status")); + } + + @Override + public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { + List<String> parameterNames = Arrays.asList("action", "gain"); + List<Object> parameterValues = Arrays.<Object>asList("setGain", gain); + return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); + + } + + private JukeboxStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception { + checkServerVersion(context, "1.7", "Jukebox not supported."); + Reader reader = getReader(context, progressListener, "jukeboxControl", null, parameterNames, parameterValues); + try { + return new JukeboxStatusParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void setStarred(String id, boolean starred, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.8", "Starring is not supported."); + id = getOfflineSongId(id, context, progressListener); + + Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", null, "id", id); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.6", "Shares not supported."); + + Reader reader = getReader(context, progressListener, "getShares", null); + try { + return new ShareParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.2", "Chat not supported."); + + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List<String> parameterNames = new ArrayList<String>(); + List<Object> parameterValues = new ArrayList<Object>(); + + parameterNames.add("since"); + parameterValues.add(since); + + Reader reader = getReader(context, progressListener, "getChatMessages", params, parameterNames, parameterValues); + + try { + return new ChatMessageParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.2", "Chat not supported."); + + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List<String> parameterNames = new ArrayList<String>(); + List<Object> parameterValues = new ArrayList<Object>(); + + parameterNames.add("message"); + parameterValues.add(message); + + Reader reader = getReader(context, progressListener, "addChatMessage", params, parameterNames, parameterValues); + + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.9", "Genres not supported."); + + Reader reader = getReader(context, progressListener, "getGenres", null); + try { + return new GenreParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.9", "Genres not supported."); + + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_RANDOM_SONGS); + + List<String> parameterNames = new ArrayList<String>(); + List<Object> parameterValues = new ArrayList<Object>(); + + parameterNames.add("genre"); + parameterValues.add(genre); + parameterNames.add("count"); + parameterValues.add(count); + parameterNames.add("offset"); + parameterValues.add(offset); + + Reader reader = getReader(context, progressListener, "getSongsByGenre", params, parameterNames, parameterValues); + + try { + return new RandomSongsParser(context).parse(reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.9", "Podcasts not supported."); + + Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false")); + try { + List<PodcastChannel> channels = new PodcastChannelParser(context).parse(reader, progressListener); + + String content = ""; + for(PodcastChannel channel: channels) { + content += channel.getName() + "\n"; + } + + File file = FileUtil.getPodcastFile(context, Util.getServerName(context)); + BufferedWriter bw = new BufferedWriter(new FileWriter(file)); + bw.write(content); + bw.close(); + + return channels; + } finally { + Util.close(reader); + } + } + + @Override + public MusicDirectory getPodcastEpisodes(String id, Context context, ProgressListener progressListener) throws Exception { + Reader reader = getReader(context, progressListener, "getPodcasts", null, Arrays.asList("id"), Arrays.<Object>asList(id)); + try { + return new PodcastEntryParser(context).parse(id, reader, progressListener); + } finally { + Util.close(reader); + } + } + + @Override + public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.9", "Refresh podcasts not supported."); + + Reader reader = getReader(context, progressListener, "refreshPodcasts", null); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{ + checkServerVersion(context, "1.9", "Creating podcasts not supported."); + + Reader reader = getReader(context, progressListener, "createPodcastChannel", null, "url", url); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception { + checkServerVersion(context, "1.9", "Deleting podcasts not supported."); + + Reader reader = getReader(context, progressListener, "deletePodcastChannel", null, "id", id); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ + checkServerVersion(context, "1.9", "Downloading podcasts not supported."); + + Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", null, "id", id); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public void deletePodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ + checkServerVersion(context, "1.9", "Deleting podcasts not supported."); + + Reader reader = getReader(context, progressListener, "deletePodcastEpisode", null, "id", id); + try { + new ErrorParser(context).parse(reader); + } finally { + Util.close(reader); + } + } + + @Override + public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ + return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener); + } + + public int processOfflineScrobbles(final Context context, final ProgressListener progressListener) throws Exception { + SharedPreferences offline = Util.getOfflineSync(context); + SharedPreferences.Editor offlineEditor = offline.edit(); + int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); + int retry = 0; + for(int i = 1; i <= count; i++) { + String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); + long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); + if(id != null) { + scrobble(id, true, time, context, progressListener); + } else { + String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); + try{ + SearchCritera critera = new SearchCritera(search, 0, 0, 1); + SearchResult result = searchNew(critera, context, progressListener); + if(result.getSongs().size() == 1){ + Log.i(TAG, "Query '" + search + "' returned song " + result.getSongs().get(0).getTitle() + " by " + result.getSongs().get(0).getArtist() + " with id " + result.getSongs().get(0).getId()); + Log.i(TAG, "Scrobbling " + result.getSongs().get(0).getId() + " with time " + time); + scrobble(result.getSongs().get(0).getId(), true, time, context, progressListener); + } + else{ + throw new Exception("Song not found on server"); + } + } + catch(Exception e){ + Log.e(TAG, e.toString()); + retry++; + } + } + } + + offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); + offlineEditor.commit(); + + return count - retry; + } + + public int processOfflineStars(final Context context, final ProgressListener progressListener) throws Exception { + SharedPreferences offline = Util.getOfflineSync(context); + SharedPreferences.Editor offlineEditor = offline.edit(); + int count = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0); + int retry = 0; + for(int i = 1; i <= count; i++) { + String id = offline.getString(Constants.OFFLINE_STAR_ID + i, null); + boolean starred = offline.getBoolean(Constants.OFFLINE_STAR_SETTING + i, false); + if(id != null) { + setStarred(id, starred, context, progressListener); + } else { + String search = offline.getString(Constants.OFFLINE_STAR_SEARCH + i, ""); + try{ + SearchCritera critera = new SearchCritera(search, 0, 1, 1); + SearchResult result = searchNew(critera, context, progressListener); + if(result.getSongs().size() == 1){ + Log.i(TAG, "Query '" + search + "' returned song " + result.getSongs().get(0).getTitle() + " by " + result.getSongs().get(0).getArtist() + " with id " + result.getSongs().get(0).getId()); + setStarred(result.getSongs().get(0).getId(), starred, context, progressListener); + } else if(result.getAlbums().size() == 1){ + Log.i(TAG, "Query '" + search + "' returned song " + result.getAlbums().get(0).getTitle() + " by " + result.getAlbums().get(0).getArtist() + " with id " + result.getAlbums().get(0).getId()); + setStarred(result.getAlbums().get(0).getId(), starred, context, progressListener); + } + else{ + throw new Exception("Song not found on server"); + } + } + catch(Exception e){ + Log.e(TAG, e.toString()); + retry++; + } + } + } + + offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, 0); + offlineEditor.commit(); + + return count - retry; + } + + private String getOfflineSongId(String id, Context context, ProgressListener progressListener) throws Exception { + SharedPreferences prefs = Util.getPreferences(context); + String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); + if(id.indexOf(cacheLocn) != -1) { + String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); + SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1); + SearchResult result = searchNew(critera, context, progressListener); + if(result.getSongs().size() == 1){ + id = result.getSongs().get(0).getId(); + } + } + + return id; + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception { + return getReader(context, progressListener, method, requestParams, Collections.<String>emptyList(), Collections.emptyList()); + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, String parameterName, Object parameterValue) throws Exception { + return getReader(context, progressListener, method, requestParams, Arrays.asList(parameterName), Arrays.<Object>asList(parameterValue)); + } + + private Reader getReader(Context context, ProgressListener progressListener, String method, + HttpParams requestParams, List<String> parameterNames, List<Object> parameterValues) throws Exception { + + if (progressListener != null) { + progressListener.updateProgress(R.string.service_connecting); + } + + String url = Util.getRestUrl(context, method); + return getReaderForURL(context, url, requestParams, parameterNames, parameterValues, progressListener); + } + + private Reader getReaderForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, + List<Object> parameterValues, ProgressListener progressListener) throws Exception { + HttpEntity entity = getEntityForURL(context, url, requestParams, parameterNames, parameterValues, progressListener); + if (entity == null) { + throw new RuntimeException("No entity received for URL " + url); + } + + InputStream in = entity.getContent(); + return new InputStreamReader(in, Constants.UTF_8); + } + + private HttpEntity getEntityForURL(Context context, String url, HttpParams requestParams, List<String> parameterNames, + List<Object> parameterValues, ProgressListener progressListener) throws Exception { + return getResponseForURL(context, url, requestParams, parameterNames, parameterValues, null, progressListener, null).getEntity(); + } + + private HttpResponse getResponseForURL(Context context, String url, HttpParams requestParams, + List<String> parameterNames, List<Object> parameterValues, + List<Header> headers, ProgressListener progressListener, CancellableTask task) throws Exception { + Log.d(TAG, "Connections in pool: " + connManager.getConnectionsInPool()); + + // If not too many parameters, extract them to the URL rather than relying on the HTTP POST request being + // received intact. Remember, HTTP POST requests are converted to GET requests during HTTP redirects, thus + // loosing its entity. + if (parameterNames != null && parameterNames.size() < 10) { + StringBuilder builder = new StringBuilder(url); + for (int i = 0; i < parameterNames.size(); i++) { + builder.append("&").append(parameterNames.get(i)).append("="); + builder.append(URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8")); + } + url = builder.toString(); + parameterNames = null; + parameterValues = null; + } + + String rewrittenUrl = rewriteUrlWithRedirect(context, url); + return executeWithRetry(context, rewrittenUrl, url, requestParams, parameterNames, parameterValues, headers, progressListener, task); + } + + private HttpResponse executeWithRetry(Context context, String url, String originalUrl, HttpParams requestParams, + List<String> parameterNames, List<Object> parameterValues, + List<Header> headers, ProgressListener progressListener, CancellableTask task) throws IOException { + // Strip out sensitive information from log + Log.i(TAG, "Using URL " + url.substring(0, url.indexOf("?u=") + 1) + url.substring(url.indexOf("&v=") + 1)); + + SharedPreferences prefs = Util.getPreferences(context); + int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000")); + HttpParams newParams = httpClient.getParams(); + HttpConnectionParams.setSoTimeout(newParams, networkTimeout); + httpClient.setParams(newParams); + + final AtomicReference<Boolean> cancelled = new AtomicReference<Boolean>(false); + int attempts = 0; + while (true) { + attempts++; + HttpContext httpContext = new BasicHttpContext(); + final HttpPost request = new HttpPost(url); + + if (task != null) { + // Attempt to abort the HTTP request if the task is cancelled. + task.setOnCancelListener(new CancellableTask.OnCancelListener() { + @Override + public void onCancel() { + new Thread(new Runnable() { + public void run() { + try { + cancelled.set(true); + request.abort(); + } catch(Exception e) { + Log.e(TAG, "Failed to stop http task"); + } + } + }).start(); + } + }); + } + + if (parameterNames != null) { + List<NameValuePair> params = new ArrayList<NameValuePair>(); + for (int i = 0; i < parameterNames.size(); i++) { + params.add(new BasicNameValuePair(parameterNames.get(i), String.valueOf(parameterValues.get(i)))); + } + request.setEntity(new UrlEncodedFormEntity(params, Constants.UTF_8)); + } + + if (requestParams != null) { + request.setParams(requestParams); + Log.d(TAG, "Socket read timeout: " + HttpConnectionParams.getSoTimeout(requestParams) + " ms."); + } + + if (headers != null) { + for (Header header : headers) { + request.addHeader(header); + } + } + + // Set credentials to get through apache proxies that require authentication. + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), + new UsernamePasswordCredentials(username, password)); + + try { + HttpResponse response = httpClient.execute(request, httpContext); + detectRedirect(originalUrl, context, httpContext); + return response; + } catch (IOException x) { + request.abort(); + if (attempts >= HTTP_REQUEST_MAX_ATTEMPTS || cancelled.get()) { + throw x; + } + if (progressListener != null) { + String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1); + progressListener.updateProgress(msg); + } + Log.w(TAG, "Got IOException (" + attempts + "), will retry", x); + increaseTimeouts(requestParams); + Util.sleepQuietly(2000L); + } + } + } + + private void increaseTimeouts(HttpParams requestParams) { + if (requestParams != null) { + int connectTimeout = HttpConnectionParams.getConnectionTimeout(requestParams); + if (connectTimeout != 0) { + HttpConnectionParams.setConnectionTimeout(requestParams, (int) (connectTimeout * 1.3F)); + } + int readTimeout = HttpConnectionParams.getSoTimeout(requestParams); + if (readTimeout != 0) { + HttpConnectionParams.setSoTimeout(requestParams, (int) (readTimeout * 1.5F)); + } + } + } + + private void detectRedirect(String originalUrl, Context context, HttpContext httpContext) { + HttpUriRequest request = (HttpUriRequest) httpContext.getAttribute(ExecutionContext.HTTP_REQUEST); + HttpHost host = (HttpHost) httpContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST); + + // Sometimes the request doesn't contain the "http://host" part + String redirectedUrl; + if (request.getURI().getScheme() == null) { + redirectedUrl = host.toURI() + request.getURI(); + } else { + redirectedUrl = request.getURI().toString(); + } + + redirectFrom = originalUrl.substring(0, originalUrl.indexOf("/rest/")); + redirectTo = redirectedUrl.substring(0, redirectedUrl.indexOf("/rest/")); + + if(redirectFrom.compareTo(redirectTo) != 0) { + Log.i(TAG, redirectFrom + " redirects to " + redirectTo); + } + redirectionLastChecked = System.currentTimeMillis(); + redirectionNetworkType = getCurrentNetworkType(context); + } + + private String rewriteUrlWithRedirect(Context context, String url) { + + // Only cache for a certain time. + if (System.currentTimeMillis() - redirectionLastChecked > REDIRECTION_CHECK_INTERVAL_MILLIS) { + return url; + } + + // Ignore cache if network type has changed. + if (redirectionNetworkType != getCurrentNetworkType(context)) { + return url; + } + + if (redirectFrom == null || redirectTo == null) { + return url; + } + + return url.replace(redirectFrom, redirectTo); + } + + private int getCurrentNetworkType(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + return networkInfo == null ? -1 : networkInfo.getType(); + } +} diff --git a/src/github/daneren2005/dsub/service/Scrobbler.java b/src/github/daneren2005/dsub/service/Scrobbler.java new file mode 100644 index 00000000..222c78c8 --- /dev/null +++ b/src/github/daneren2005/dsub/service/Scrobbler.java @@ -0,0 +1,52 @@ +package github.daneren2005.dsub.service; + +import android.content.Context; +import android.util.Log; +import github.daneren2005.dsub.util.Util; + +/** + * Scrobbles played songs to Last.fm. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class Scrobbler { + + private static final String TAG = Scrobbler.class.getSimpleName(); + + private String lastSubmission; + private String lastNowPlaying; + + public void scrobble(final Context context, final DownloadFile song, final boolean submission) { + if (song == null || !Util.isScrobblingEnabled(context)) { + return; + } + final String id = song.getSong().getId(); + + // Avoid duplicate registrations. + if (submission && id.equals(lastSubmission)) { + return; + } + if (!submission && id.equals(lastNowPlaying)) { + return; + } + if (submission) { + lastSubmission = id; + } else { + lastNowPlaying = id; + } + + new Thread("Scrobble " + song) { + @Override + public void run() { + MusicService service = MusicServiceFactory.getMusicService(context); + try { + service.scrobble(id, submission, context, null); + Log.i(TAG, "Scrobbled '" + (submission ? "submission" : "now playing") + "' for " + song); + } catch (Exception x) { + Log.i(TAG, "Failed to scrobble'" + (submission ? "submission" : "now playing") + "' for " + song, x); + } + } + }.start(); + } +} diff --git a/src/github/daneren2005/dsub/service/ServerTooOldException.java b/src/github/daneren2005/dsub/service/ServerTooOldException.java new file mode 100644 index 00000000..e4a951de --- /dev/null +++ b/src/github/daneren2005/dsub/service/ServerTooOldException.java @@ -0,0 +1,60 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service; + +import github.daneren2005.dsub.domain.Version; + +/** + * Thrown if the REST API version implemented by the server is too old. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class ServerTooOldException extends Exception { + + private final String text; + private final Version serverVersion; + private final Version requiredVersion; + + public ServerTooOldException(String text, Version serverVersion, Version requiredVersion) { + this.text = text; + this.serverVersion = serverVersion; + this.requiredVersion = requiredVersion; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (text != null) { + builder.append(text).append(" "); + } + builder.append("Server API version too old. "); + builder.append("Requires server version ") + .append(requiredVersion.getVersion()) + .append(", but it is version ") + .append(serverVersion.getVersion()) + .append("."); + return builder.toString(); + } + + @Override + public String getMessage() { + return this.toString(); + } +} diff --git a/src/github/daneren2005/dsub/service/StreamProxy.java b/src/github/daneren2005/dsub/service/StreamProxy.java new file mode 100644 index 00000000..24c1b201 --- /dev/null +++ b/src/github/daneren2005/dsub/service/StreamProxy.java @@ -0,0 +1,248 @@ +package github.daneren2005.dsub.service;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URLDecoder;
+import java.net.UnknownHostException;
+import java.util.StringTokenizer;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.message.BasicHttpRequest;
+
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.Constants;
+
+public class StreamProxy implements Runnable {
+ private static final String TAG = StreamProxy.class.getSimpleName();
+
+ private Thread thread;
+ private boolean isRunning;
+ private ServerSocket socket;
+ private int port;
+ private DownloadService downloadService;
+
+ public StreamProxy(DownloadService downloadService) {
+
+ // Create listening socket
+ try {
+ socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
+ socket.setSoTimeout(5000);
+ port = socket.getLocalPort();
+ this.downloadService = downloadService;
+ } catch (UnknownHostException e) { // impossible
+ } catch (IOException e) {
+ Log.e(TAG, "IOException initializing server", e);
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void start() {
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void stop() {
+ isRunning = false;
+ thread.interrupt();
+ }
+
+ @Override
+ public void run() {
+ isRunning = true;
+ while (isRunning) {
+ try {
+ Socket client = socket.accept();
+ if (client == null) {
+ continue;
+ }
+ Log.i(TAG, "client connected");
+
+ StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
+ if (task.processRequest()) {
+ new Thread(task).start();
+ }
+
+ } catch (SocketTimeoutException e) {
+ // Do nothing
+ } catch (IOException e) {
+ Log.e(TAG, "Error connecting to client", e);
+ }
+ }
+ Log.i(TAG, "Proxy interrupted. Shutting down.");
+ }
+
+ private class StreamToMediaPlayerTask implements Runnable {
+
+ String localPath;
+ Socket client;
+ int cbSkip;
+
+ public StreamToMediaPlayerTask(Socket client) {
+ this.client = client;
+ }
+
+ private HttpRequest readRequest() {
+ HttpRequest request = null;
+ InputStream is;
+ String firstLine;
+ try {
+ is = client.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
+ firstLine = reader.readLine();
+ } catch (IOException e) {
+ Log.e(TAG, "Error parsing request", e);
+ return request;
+ }
+
+ if (firstLine == null) {
+ Log.i(TAG, "Proxy client closed connection without a request.");
+ return request;
+ }
+
+ StringTokenizer st = new StringTokenizer(firstLine);
+ String method = st.nextToken();
+ String uri = st.nextToken();
+ String realUri = uri.substring(1);
+ Log.i(TAG, realUri);
+ request = new BasicHttpRequest(method, realUri);
+ return request;
+ }
+
+ public boolean processRequest() {
+ HttpRequest request = readRequest();
+ if (request == null) {
+ return false;
+ }
+
+ // Read HTTP headers
+ Log.i(TAG, "Processing request");
+
+ try {
+ localPath = URLDecoder.decode(request.getRequestLine().getUri(), Constants.UTF_8);
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported encoding", e);
+ return false;
+ }
+
+ Log.i(TAG, "Processing request for file " + localPath);
+ File file = new File(localPath);
+ if (!file.exists()) {
+ Log.e(TAG, "File " + localPath + " does not exist");
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void run() {
+ Log.i(TAG, "Streaming song in background");
+ DownloadFile downloadFile = downloadService.getCurrentPlaying();
+ MusicDirectory.Entry song = downloadFile.getSong();
+
+ // Create HTTP header
+ String headers = "HTTP/1.0 200 OK\r\n";
+ headers += "Content-Type: " + "application/octet-stream" + "\r\n";
+
+ Integer contentLength = downloadFile.getContentLength();
+ long fileSize;
+ if(contentLength == null) {
+ fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8;
+ } else {
+ fileSize = contentLength;
+ headers += "Content-Length: " + fileSize + "\r\n";
+ }
+ Log.i(TAG, "Streaming fileSize: " + fileSize);
+
+ headers += "Connection: close\r\n";
+ headers += "\r\n";
+
+ long cbToSend = fileSize - cbSkip;
+ OutputStream output = null;
+ byte[] buff = new byte[64 * 1024];
+ try {
+ output = new BufferedOutputStream(client.getOutputStream(), 32*1024);
+ output.write(headers.getBytes());
+
+ if(!downloadFile.isWorkDone()) {
+ // Loop as long as there's stuff to send
+ while (isRunning && !client.isClosed()) {
+
+ // See if there's more to send
+ File file = new File(localPath);
+ int cbSentThisBatch = 0;
+ if (file.exists()) {
+ FileInputStream input = new FileInputStream(file);
+ input.skip(cbSkip);
+ int cbToSendThisBatch = input.available();
+ while (cbToSendThisBatch > 0) {
+ int cbToRead = Math.min(cbToSendThisBatch, buff.length);
+ int cbRead = input.read(buff, 0, cbToRead);
+ if (cbRead == -1) {
+ break;
+ }
+ cbToSendThisBatch -= cbRead;
+ cbToSend -= cbRead;
+ output.write(buff, 0, cbRead);
+ output.flush();
+ cbSkip += cbRead;
+ cbSentThisBatch += cbRead;
+ }
+ input.close();
+ }
+
+ // Done regardless of whether or not it thinks it is
+ if(downloadFile.isWorkDone() && cbSkip >= file.length()) {
+ break;
+ }
+
+ // If we did nothing this batch, block for a second
+ if (cbSentThisBatch == 0) {
+ Log.d(TAG, "Blocking until more data appears (" + cbToSend + ")");
+ Thread.sleep(1000);
+ }
+ }
+ } else {
+ Log.w(TAG, "Requesting data for completely downloaded file");
+ }
+ }
+ catch (SocketException socketException) {
+ Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
+ }
+ catch (Exception e) {
+ Log.e(TAG, "Exception thrown from streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+
+ // Cleanup
+ try {
+ if (output != null) {
+ output.close();
+ }
+ client.close();
+ }
+ catch (IOException e) {
+ Log.e(TAG, "IOException while cleaning up streaming task:");
+ Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/AbstractParser.java b/src/github/daneren2005/dsub/service/parser/AbstractParser.java new file mode 100644 index 00000000..1a457754 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/AbstractParser.java @@ -0,0 +1,138 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import java.io.Reader; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.util.Xml; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Version; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.Util; + +/** + * @author Sindre Mehus + */ +public abstract class AbstractParser { + + protected final Context context; + private XmlPullParser parser; + private boolean rootElementFound; + + public AbstractParser(Context context) { + this.context = context; + } + + protected Context getContext() { + return context; + } + + protected void handleError() throws Exception { + int code = getInteger("code"); + String message; + switch (code) { + case 20: + message = context.getResources().getString(R.string.parser_upgrade_client); + break; + case 30: + message = context.getResources().getString(R.string.parser_upgrade_server); + break; + case 40: + message = context.getResources().getString(R.string.parser_not_authenticated); + break; + case 50: + message = context.getResources().getString(R.string.parser_not_authorized); + break; + default: + message = get("message"); + break; + } + throw new SubsonicRESTException(code, message); + } + + protected void updateProgress(ProgressListener progressListener, int messageId) { + if (progressListener != null) { + progressListener.updateProgress(messageId); + } + } + + protected void updateProgress(ProgressListener progressListener, String message) { + if (progressListener != null) { + progressListener.updateProgress(message); + } + } + + protected String getText() { + return parser.getText(); + } + + protected String get(String name) { + return parser.getAttributeValue(null, name); + } + + protected boolean getBoolean(String name) { + return "true".equals(get(name)); + } + + protected Integer getInteger(String name) { + String s = get(name); + return s == null ? null : Integer.valueOf(s); + } + + protected Long getLong(String name) { + String s = get(name); + return s == null ? null : Long.valueOf(s); + } + + protected Float getFloat(String name) { + String s = get(name); + return s == null ? null : Float.valueOf(s); + } + + protected void init(Reader reader) throws Exception { + parser = Xml.newPullParser(); + parser.setInput(reader); + rootElementFound = false; + } + + protected int nextParseEvent() throws Exception { + return parser.next(); + } + + protected String getElementName() { + String name = parser.getName(); + if ("subsonic-response".equals(name)) { + rootElementFound = true; + String version = get("version"); + if (version != null) { + Util.setServerRestVersion(context, new Version(version)); + } + } + return name; + } + + protected void validate() throws Exception { + if (!rootElementFound) { + throw new Exception(context.getResources().getString(R.string.background_task_parse_error)); + } + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/AlbumListParser.java b/src/github/daneren2005/dsub/service/parser/AlbumListParser.java new file mode 100644 index 00000000..64145d67 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/AlbumListParser.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class AlbumListParser extends MusicDirectoryEntryParser { + + public AlbumListParser(Context context) { + super(context); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("album".equals(name)) { + dir.addChild(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return dir; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/ChatMessageParser.java b/src/github/daneren2005/dsub/service/parser/ChatMessageParser.java new file mode 100644 index 00000000..1425a734 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/ChatMessageParser.java @@ -0,0 +1,67 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ChatMessageParser extends AbstractParser {
+
+ public ChatMessageParser(Context context) {
+ super(context);
+ }
+
+ public List<ChatMessage> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+ List<ChatMessage> result = new ArrayList<ChatMessage>();
+ int eventType;
+
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("chatMessage".equals(name)) {
+ ChatMessage chatMessage = new ChatMessage();
+ chatMessage.setUsername(get("username"));
+ chatMessage.setTime(getLong("time"));
+ chatMessage.setMessage(get("message"));
+ result.add(chatMessage);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return result;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/ErrorParser.java b/src/github/daneren2005/dsub/service/parser/ErrorParser.java new file mode 100644 index 00000000..3463687d --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/ErrorParser.java @@ -0,0 +1,49 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class ErrorParser extends AbstractParser { + + public ErrorParser(Context context) { + super(context); + } + + public void parse(Reader reader) throws Exception { + + init(reader); + + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG && "error".equals(getElementName())) { + handleError(); + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/GenreParser.java b/src/github/daneren2005/dsub/service/parser/GenreParser.java new file mode 100644 index 00000000..1062d3af --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/GenreParser.java @@ -0,0 +1,122 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.util.ProgressListener;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class GenreParser extends AbstractParser {
+ private static final String TAG = GenreParser.class.getSimpleName();
+
+ public GenreParser(Context context) {
+ super(context);
+ }
+
+ public List<Genre> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+
+ List<Genre> result = new ArrayList<Genre>();
+ StringReader sr = null;
+
+ try {
+ BufferedReader br = new BufferedReader(reader);
+ String xml = null;
+ String line = null;
+
+ while ((line = br.readLine()) != null) {
+ if (xml == null) {
+ xml = line;
+ } else {
+ xml += line;
+ }
+ }
+ br.close();
+
+ // Replace double escaped ampersand (&apos;)
+ xml = xml.replaceAll("(?:&)(amp;|lt;|gt;|#37;|apos;)", "&$1");
+
+ // Replace unescaped ampersand
+ xml = xml.replaceAll("&(?!amp;|lt;|gt;|#37;|apos;)", "&");
+
+ // Replace unescaped percent symbol
+ // No replacements for <> at this time
+ xml = xml.replaceAll("%", "%");
+
+ xml = xml.replaceAll("'", "'");
+
+ sr = new StringReader(xml);
+ } catch (IOException ioe) {
+ Log.e(TAG, "Error parsing Genre XML", ioe);
+ }
+
+ if (sr == null) {
+ Log.w(TAG, "Unable to parse Genre XML, returning empty list");
+ return result;
+ }
+
+ init(sr);
+
+ Genre genre = null;
+
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("genre".equals(name)) {
+ genre = new Genre();
+ } else if ("error".equals(name)) {
+ handleError();
+ } else {
+ genre = null;
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ if (genre != null) {
+ String value = getText();
+ if (genre != null) {
+ genre.setName(value);
+ genre.setIndex(value.substring(0, 1));
+ result.add(genre);
+ genre = null;
+ }
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return result;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/IndexesParser.java b/src/github/daneren2005/dsub/service/parser/IndexesParser.java new file mode 100644 index 00000000..6196411d --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/IndexesParser.java @@ -0,0 +1,120 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import java.io.Reader; +import java.util.List; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.content.SharedPreferences; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.Indexes; +import github.daneren2005.dsub.util.ProgressListener; +import android.util.Log; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.Util; + +/** + * @author Sindre Mehus + */ +public class IndexesParser extends AbstractParser { + private static final String TAG = IndexesParser.class.getSimpleName(); + + private Context context; + + public IndexesParser(Context context) { + super(context); + this.context = context; + } + + public Indexes parse(Reader reader, ProgressListener progressListener) throws Exception { + + long t0 = System.currentTimeMillis(); + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + List<Artist> artists = new ArrayList<Artist>(); + List<Artist> shortcuts = new ArrayList<Artist>(); + Long lastModified = null; + int eventType; + String index = "#"; + String ignoredArticles = null; + boolean changed = false; + + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("indexes".equals(name)) { + changed = true; + lastModified = getLong("lastModified"); + ignoredArticles = get("ignoredArticles"); + } else if ("index".equals(name)) { + index = get("name"); + + } else if ("artist".equals(name)) { + Artist artist = new Artist(); + artist.setId(get("id")); + artist.setName(get("name")); + artist.setIndex(index); + artist.setStarred(get("starred") != null); + artists.add(artist); + + if (artists.size() % 10 == 0) { + String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size()); + updateProgress(progressListener, msg); + } + } else if ("shortcut".equals(name)) { + Artist shortcut = new Artist(); + shortcut.setId(get("id")); + shortcut.setName(get("name")); + shortcut.setIndex("*"); + shortcut.setStarred(get("starred") != null); + shortcuts.add(shortcut); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + if(ignoredArticles != null) { + SharedPreferences.Editor prefs = Util.getPreferences(context).edit(); + prefs.putString(Constants.CACHE_KEY_IGNORE, ignoredArticles); + prefs.commit(); + } + + if (!changed) { + return null; + } + + long t1 = System.currentTimeMillis(); + Log.d(TAG, "Got " + artists.size() + " artist(s) in " + (t1 - t0) + "ms."); + + String msg = getContext().getResources().getString(R.string.parser_artist_count, artists.size()); + updateProgress(progressListener, msg); + + return new Indexes(lastModified == null ? 0L : lastModified, shortcuts, artists); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java b/src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java new file mode 100644 index 00000000..8526e635 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/JukeboxStatusParser.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import java.io.Reader; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import github.daneren2005.dsub.domain.JukeboxStatus; + +/** + * @author Sindre Mehus + */ +public class JukeboxStatusParser extends AbstractParser { + + public JukeboxStatusParser(Context context) { + super(context); + } + + public JukeboxStatus parse(Reader reader) throws Exception { + + init(reader); + + JukeboxStatus jukeboxStatus = new JukeboxStatus(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("jukeboxPlaylist".equals(name) || "jukeboxStatus".equals(name)) { + jukeboxStatus.setPositionSeconds(getInteger("position")); + jukeboxStatus.setCurrentIndex(getInteger("currentIndex")); + jukeboxStatus.setPlaying(getBoolean("playing")); + jukeboxStatus.setGain(getFloat("gain")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return jukeboxStatus; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/LicenseParser.java b/src/github/daneren2005/dsub/service/parser/LicenseParser.java new file mode 100644 index 00000000..e7b200fd --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/LicenseParser.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +import github.daneren2005.dsub.domain.ServerInfo; +import github.daneren2005.dsub.domain.Version; + +/** + * @author Sindre Mehus + */ +public class LicenseParser extends AbstractParser { + + public LicenseParser(Context context) { + super(context); + } + + public ServerInfo parse(Reader reader) throws Exception { + + init(reader); + + ServerInfo serverInfo = new ServerInfo(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("subsonic-response".equals(name)) { + serverInfo.setRestVersion(new Version(get("version"))); + } else if ("license".equals(name)) { + serverInfo.setLicenseValid(getBoolean("valid")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + + return serverInfo; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/LyricsParser.java b/src/github/daneren2005/dsub/service/parser/LyricsParser.java new file mode 100644 index 00000000..98b0f6a0 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/LyricsParser.java @@ -0,0 +1,65 @@ +/* + 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 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Lyrics; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class LyricsParser extends AbstractParser { + + public LyricsParser(Context context) { + super(context); + } + + public Lyrics parse(Reader reader, ProgressListener progressListener) throws Exception { + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + Lyrics lyrics = null; + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("lyrics".equals(name)) { + lyrics = new Lyrics(); + lyrics.setArtist(get("artist")); + lyrics.setTitle(get("title")); + } else if ("error".equals(name)) { + handleError(); + } + } else if (eventType == XmlPullParser.TEXT) { + if (lyrics != null && lyrics.getText() == null) { + lyrics.setText(getText()); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + return lyrics; + } +} diff --git a/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java b/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java new file mode 100644 index 00000000..b0434aca --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/MusicDirectoryEntryParser.java @@ -0,0 +1,74 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.domain.MusicDirectory; + +/** + * @author Sindre Mehus + */ +public class MusicDirectoryEntryParser extends AbstractParser { + public MusicDirectoryEntryParser(Context context) { + super(context); + } + + protected MusicDirectory.Entry parseEntry(String artist) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + entry.setId(get("id")); + entry.setParent(get("parent")); + entry.setTitle(get("title")); + entry.setDirectory(getBoolean("isDir")); + entry.setCoverArt(get("coverArt")); + entry.setArtist(get("artist")); + entry.setStarred(get("starred") != null); + + if (!entry.isDirectory()) { + entry.setAlbum(get("album")); + entry.setTrack(getInteger("track")); + entry.setYear(getInteger("year")); + entry.setGenre(get("genre")); + entry.setContentType(get("contentType")); + entry.setSuffix(get("suffix")); + entry.setTranscodedContentType(get("transcodedContentType")); + entry.setTranscodedSuffix(get("transcodedSuffix")); + entry.setSize(getLong("size")); + entry.setDuration(getInteger("duration")); + entry.setBitRate(getInteger("bitRate")); + entry.setPath(get("path")); + entry.setVideo(getBoolean("isVideo")); + entry.setDiscNumber(getInteger("discNumber")); + } else if(!"".equals(artist)) { + entry.setPath(artist + "/" + entry.getTitle()); + } + return entry; + } + + protected MusicDirectory.Entry parseArtist() { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + + entry.setId(get("id")); + entry.setTitle(get("name")); + entry.setPath(entry.getTitle()); + entry.setStarred(true); + entry.setDirectory(true); + + return entry; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java b/src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java new file mode 100644 index 00000000..17b09805 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/MusicDirectoryParser.java @@ -0,0 +1,83 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import android.util.Log; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.Version; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.util.Util; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class MusicDirectoryParser extends MusicDirectoryEntryParser { + + private static final String TAG = MusicDirectoryParser.class.getSimpleName(); + private Context context; + + public MusicDirectoryParser(Context context) { + super(context); + this.context = context; + } + + public MusicDirectory parse(String artist, Reader reader, ProgressListener progressListener) throws Exception { + long t0 = System.currentTimeMillis(); + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("child".equals(name)) { + MusicDirectory.Entry entry = parseEntry(artist); + entry.setGrandParent(dir.getParent()); + dir.addChild(entry); + } else if ("directory".equals(name)) { + dir.setName(get("name")); + dir.setId(get("id")); + dir.setParent(get("parent")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + // Only apply sorting on server version 4.7 and greater, where disc is supported + if(Util.checkServerVersion(context, "1.8.0")) { + dir.sortChildren(); + } + + long t1 = System.currentTimeMillis(); + Log.d(TAG, "Got music directory in " + (t1 - t0) + "ms."); + + return dir; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java b/src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java new file mode 100644 index 00000000..5dfebf27 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/MusicFoldersParser.java @@ -0,0 +1,69 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicFolder; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.util.ProgressListener; + +/** + * @author Sindre Mehus + */ +public class MusicFoldersParser extends AbstractParser { + + public MusicFoldersParser(Context context) { + super(context); + } + + public List<MusicFolder> parse(Reader reader, ProgressListener progressListener) throws Exception { + + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + List<MusicFolder> result = new ArrayList<MusicFolder>(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String tag = getElementName(); + if ("musicFolder".equals(tag)) { + String id = get("id"); + String name = get("name"); + result.add(new MusicFolder(id, name)); + } else if ("error".equals(tag)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return result; + } + +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/PlaylistParser.java b/src/github/daneren2005/dsub/service/parser/PlaylistParser.java new file mode 100644 index 00000000..8c6cfc6f --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/PlaylistParser.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class PlaylistParser extends MusicDirectoryEntryParser { + + public PlaylistParser(Context context) { + super(context); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("entry".equals(name)) { + dir.addChild(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return dir; + } + +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/PlaylistsParser.java b/src/github/daneren2005/dsub/service/parser/PlaylistsParser.java new file mode 100644 index 00000000..a5bf2497 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/PlaylistsParser.java @@ -0,0 +1,73 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.util.ProgressListener; +import github.daneren2005.dsub.view.PlaylistAdapter; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Sindre Mehus + */ +public class PlaylistsParser extends AbstractParser { + + public PlaylistsParser(Context context) { + super(context); + } + + public List<Playlist> parse(Reader reader, ProgressListener progressListener) throws Exception { + + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + List<Playlist> result = new ArrayList<Playlist>(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String tag = getElementName(); + if ("playlist".equals(tag)) { + String id = get("id"); + String name = get("name"); + String owner = get("owner"); + String comment = get("comment"); + String songCount = get("songCount"); + String created = get("created"); + String pub = get("public"); + result.add(new Playlist(id, name, owner, comment, songCount, created, pub)); + } else if ("error".equals(tag)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return PlaylistAdapter.PlaylistComparator.sort(result); + } + +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java b/src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java new file mode 100644 index 00000000..b091aefa --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/PodcastChannelParser.java @@ -0,0 +1,67 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.ProgressListener;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastChannelParser extends AbstractParser {
+ public PodcastChannelParser(Context context) {
+ super(context);
+ }
+
+ public List<PodcastChannel> parse(Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<PodcastChannel> channels = new ArrayList<PodcastChannel>();
+ int eventType;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("channel".equals(name)) {
+ PodcastChannel channel = new PodcastChannel();
+ channel.setId(get("id"));
+ channel.setUrl(get("url"));
+ channel.setName(get("title"));
+ channel.setDescription(get("description"));
+ channel.setStatus(get("status"));
+ channel.setErrorMessage(get("errorMessage"));
+ channels.add(channel);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return channels;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java b/src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java new file mode 100644 index 00000000..585b3057 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/PodcastEntryParser.java @@ -0,0 +1,105 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ProgressListener;
+import java.io.Reader;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * @author Scott
+ */
+public class PodcastEntryParser extends AbstractParser {
+ private static int bogusId = -1;
+
+ public PodcastEntryParser(Context context) {
+ super(context);
+ }
+
+ public MusicDirectory parse(String channel, Reader reader, ProgressListener progressListener) throws Exception {
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ MusicDirectory episodes = new MusicDirectory();
+ int eventType;
+ boolean valid = false;
+ do {
+ eventType = nextParseEvent();
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+ if ("channel".equals(name)) {
+ String id = get("id");
+ if(id.equals(channel)) {
+ episodes.setId(id);
+ episodes.setName(get("title"));
+ valid = true;
+ } else {
+ valid = false;
+ }
+ }
+ else if ("episode".equals(name) && valid) {
+ PodcastEpisode episode = new PodcastEpisode();
+ episode.setEpisodeId(get("id"));
+ episode.setId(get("streamId"));
+ episode.setTitle(get("title"));
+ episode.setArtist(episodes.getName());
+ episode.setAlbum(get("description"));
+ episode.setDate(get("publishDate"));
+ if(episode.getDate() == null) {
+ episode.setDate(get("created"));
+ }
+ if(episode.getDate() != null && episode.getDate().indexOf("T") != -1) {
+ episode.setDate(episode.getDate().replace("T", " "));
+ }
+ episode.setStatus(get("status"));
+ episode.setCoverArt(get("coverArt"));
+ episode.setSize(getLong("size"));
+ episode.setContentType(get("contentType"));
+ episode.setSuffix(get("suffix"));
+ episode.setDuration(getInteger("duration"));
+ episode.setBitRate(getInteger("bitRate"));
+ episode.setVideo(getBoolean("isVideo"));
+ episode.setPath(get("path"));
+ if(episode.getPath() == null) {
+ episode.setPath(FileUtil.getPodcastPath(context, episode));
+ } else if(episode.getPath().indexOf("Podcasts/") == 0) {
+ episode.setPath(episode.getPath().substring("Podcasts/".length()));
+ }
+
+ if("error".equals(episode.getStatus()) || "skipped".equals(episode.getStatus())) {
+ episode.setId(String.valueOf(bogusId));
+ bogusId--;
+ }
+ episodes.addChild(episode);
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ return episodes;
+ }
+}
diff --git a/src/github/daneren2005/dsub/service/parser/RandomSongsParser.java b/src/github/daneren2005/dsub/service/parser/RandomSongsParser.java new file mode 100644 index 00000000..3e62d3dc --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/RandomSongsParser.java @@ -0,0 +1,62 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Sindre Mehus + */ +public class RandomSongsParser extends MusicDirectoryEntryParser { + + public RandomSongsParser(Context context) { + super(context); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("song".equals(name)) { + dir.addChild(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return dir; + } + +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java b/src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java new file mode 100644 index 00000000..a0be07ac --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/SearchResult2Parser.java @@ -0,0 +1,75 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.List; +import java.util.ArrayList; + +/** + * @author Sindre Mehus + */ +public class SearchResult2Parser extends MusicDirectoryEntryParser { + + public SearchResult2Parser(Context context) { + super(context); + } + + public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception { + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + List<Artist> artists = new ArrayList<Artist>(); + List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>(); + List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("artist".equals(name)) { + Artist artist = new Artist(); + artist.setId(get("id")); + artist.setName(get("name")); + artists.add(artist); + } else if ("album".equals(name)) { + albums.add(parseEntry("")); + } else if ("song".equals(name)) { + songs.add(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return new SearchResult(artists, albums, songs); + } + +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/SearchResultParser.java b/src/github/daneren2005/dsub/service/parser/SearchResultParser.java new file mode 100644 index 00000000..c8ef4031 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/SearchResultParser.java @@ -0,0 +1,67 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.SearchResult; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +/** + * @author Sindre Mehus + */ +public class SearchResultParser extends MusicDirectoryEntryParser { + + public SearchResultParser(Context context) { + super(context); + } + + public SearchResult parse(Reader reader, ProgressListener progressListener) throws Exception { + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("match".equals(name)) { + songs.add(parseEntry("")); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return new SearchResult(Collections.<Artist>emptyList(), Collections.<MusicDirectory.Entry>emptyList(), songs); + } + +} diff --git a/src/github/daneren2005/dsub/service/parser/ShareParser.java b/src/github/daneren2005/dsub/service/parser/ShareParser.java new file mode 100644 index 00000000..c317e799 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/ShareParser.java @@ -0,0 +1,77 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.service.parser;
+
+import android.content.Context;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Share;
+import github.daneren2005.dsub.util.ProgressListener;
+import org.xmlpull.v1.XmlPullParser;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Joshua Bahnsen
+ */
+public class ShareParser extends MusicDirectoryEntryParser {
+
+ public ShareParser(Context context) {
+ super(context);
+ }
+
+ public List<Share> parse(Reader reader, ProgressListener progressListener) throws Exception {
+
+ updateProgress(progressListener, R.string.parser_reading);
+ init(reader);
+
+ List<Share> dir = new ArrayList<Share>();
+ Share share = null;
+ int eventType;
+
+ do {
+ eventType = nextParseEvent();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String name = getElementName();
+
+ if ("share".equals(name)) {
+ share = new Share();
+ share.setCreated(get("created"));
+ share.setDescription(get("description"));
+ share.setExpires(get("expires"));
+ share.setId(get("id"));
+ share.setLastVisited(get("lastVisited"));
+ share.setUrl(get("url"));
+ share.setUsername(get("username"));
+ share.setVisitCount(getLong("visitCount"));
+ } else if ("entry".equals(name)) {
+ share.addEntry(parseEntry(null));
+ } else if ("error".equals(name)) {
+ handleError();
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ validate();
+ updateProgress(progressListener, R.string.parser_reading_done);
+
+ return dir;
+ }
+}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/StarredListParser.java b/src/github/daneren2005/dsub/service/parser/StarredListParser.java new file mode 100644 index 00000000..fc4cd175 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/StarredListParser.java @@ -0,0 +1,64 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import android.content.Context; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ProgressListener; +import org.xmlpull.v1.XmlPullParser; + +import java.io.Reader; + +/** + * @author Kurt Hardin + */ +public class StarredListParser extends MusicDirectoryEntryParser { + + public StarredListParser(Context context) { + super(context); + } + + public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception { + + updateProgress(progressListener, R.string.parser_reading); + init(reader); + + MusicDirectory dir = new MusicDirectory(); + int eventType; + do { + eventType = nextParseEvent(); + if (eventType == XmlPullParser.START_TAG) { + String name = getElementName(); + if ("album".equals(name) || "song".equals(name)) { + dir.addChild(parseEntry("")); + } else if("artist".equals(name)) { + dir.addChild(parseArtist()); + } else if ("error".equals(name)) { + handleError(); + } + } + } while (eventType != XmlPullParser.END_DOCUMENT); + + validate(); + updateProgress(progressListener, R.string.parser_reading_done); + + return dir; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java b/src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java new file mode 100644 index 00000000..096597a1 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/SubsonicRESTException.java @@ -0,0 +1,19 @@ +package github.daneren2005.dsub.service.parser; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class SubsonicRESTException extends Exception { + + private final int code; + + public SubsonicRESTException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/src/github/daneren2005/dsub/service/parser/VersionParser.java b/src/github/daneren2005/dsub/service/parser/VersionParser.java new file mode 100644 index 00000000..1b646206 --- /dev/null +++ b/src/github/daneren2005/dsub/service/parser/VersionParser.java @@ -0,0 +1,47 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.service.parser; + +import github.daneren2005.dsub.domain.Version; + +import java.io.BufferedReader; +import java.io.Reader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Sindre Mehus + */ +public class VersionParser { + + public Version parse(Reader reader) throws Exception { + + BufferedReader bufferedReader = new BufferedReader(reader); + Pattern pattern = Pattern.compile("SUBSONIC_ANDROID_VERSION_BEGIN(.*)SUBSONIC_ANDROID_VERSION_END"); + String line = bufferedReader.readLine(); + while (line != null) { + Matcher finalMatcher = pattern.matcher(line); + if (finalMatcher.find()) { + return new Version(finalMatcher.group(1)); + } + line = bufferedReader.readLine(); + } + return null; + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java b/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java new file mode 100644 index 00000000..2ffed048 --- /dev/null +++ b/src/github/daneren2005/dsub/service/ssl/SSLSocketFactory.java @@ -0,0 +1,497 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package github.daneren2005.dsub.service.ssl; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.scheme.HostNameResolver; +import org.apache.http.conn.scheme.LayeredSocketFactory; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; + +/** + * Layered socket factory for TLS/SSL connections. + * <p> + * SSLSocketFactory can be used to validate the identity of the HTTPS server against a list of + * trusted certificates and to authenticate to the HTTPS server using a private key. + * <p> + * SSLSocketFactory will enable server authentication when supplied with + * a {@link KeyStore trust-store} file containing one or several trusted certificates. The client + * secure socket will reject the connection during the SSL session handshake if the target HTTPS + * server attempts to authenticate itself with a non-trusted certificate. + * <p> + * Use JDK keytool utility to import a trusted certificate and generate a trust-store file: + * <pre> + * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore + * </pre> + * <p> + * In special cases the standard trust verification process can be bypassed by using a custom + * {@link TrustStrategy}. This interface is primarily intended for allowing self-signed + * certificates to be accepted as trusted without having to add them to the trust-store file. + * <p> + * The following parameters can be used to customize the behavior of this + * class: + * <ul> + * <li>{@link org.apache.http.params.CoreConnectionPNames#CONNECTION_TIMEOUT}</li> + * <li>{@link org.apache.http.params.CoreConnectionPNames#SO_TIMEOUT}</li> + * </ul> + * <p> + * SSLSocketFactory will enable client authentication when supplied with + * a {@link KeyStore key-store} file containing a private key/public certificate + * pair. The client secure socket will use the private key to authenticate + * itself to the target HTTPS server during the SSL session handshake if + * requested to do so by the server. + * The target HTTPS server will in its turn verify the certificate presented + * by the client in order to establish client's authenticity + * <p> + * Use the following sequence of actions to generate a key-store file + * </p> + * <ul> + * <li> + * <p> + * Use JDK keytool utility to generate a new key + * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre> + * For simplicity use the same password for the key as that of the key-store + * </p> + * </li> + * <li> + * <p> + * Issue a certificate signing request (CSR) + * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre> + * </p> + * </li> + * <li> + * <p> + * Send the certificate request to the trusted Certificate Authority for signature. + * One may choose to act as her own CA and sign the certificate request using a PKI + * tool, such as OpenSSL. + * </p> + * </li> + * <li> + * <p> + * Import the trusted CA root certificate + * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre> + * </p> + * </li> + * <li> + * <p> + * Import the PKCS#7 file containg the complete certificate chain + * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre> + * </p> + * </li> + * <li> + * <p> + * Verify the content the resultant keystore file + * <pre>keytool -list -v -keystore my.keystore</pre> + * </p> + * </li> + * </ul> + * + * @since 4.0 + */ +public class SSLSocketFactory implements LayeredSocketFactory { + + public static final String TLS = "TLS"; + public static final String SSL = "SSL"; + public static final String SSLV2 = "SSLv2"; + + public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER + = new AllowAllHostnameVerifier(); + + public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER + = new BrowserCompatHostnameVerifier(); + + public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER + = new StrictHostnameVerifier(); + + /** + * The default factory using the default JVM settings for secure connections. + */ + private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory(); + + /** + * Gets the default factory, which uses the default JVM settings for secure + * connections. + * + * @return the default factory + */ + public static SSLSocketFactory getSocketFactory() { + return DEFAULT_FACTORY; + } + + private final javax.net.ssl.SSLSocketFactory socketfactory; + private final HostNameResolver nameResolver; + // TODO: make final + private volatile X509HostnameVerifier hostnameVerifier; + + private static SSLContext createSSLContext( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final TrustStrategy trustStrategy) + throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { + if (algorithm == null) { + algorithm = TLS; + } + KeyManagerFactory kmfactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + kmfactory.init(keystore, keystorePassword != null ? keystorePassword.toCharArray(): null); + KeyManager[] keymanagers = kmfactory.getKeyManagers(); + TrustManagerFactory tmfactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmfactory.init(keystore); + TrustManager[] trustmanagers = tmfactory.getTrustManagers(); + if (trustmanagers != null && trustStrategy != null) { + for (int i = 0; i < trustmanagers.length; i++) { + TrustManager tm = trustmanagers[i]; + if (tm instanceof X509TrustManager) { + trustmanagers[i] = new TrustManagerDecorator( + (X509TrustManager) tm, trustStrategy); + } + } + } + + SSLContext sslcontext = SSLContext.getInstance(algorithm); + sslcontext.init(keymanagers, trustmanagers, random); + return sslcontext; + } + + /** + * @deprecated Use {@link #SSLSocketFactory(String, KeyStore, String, KeyStore, SecureRandom, X509HostnameVerifier)} + */ + @Deprecated + public SSLSocketFactory( + final String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final HostNameResolver nameResolver) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(createSSLContext( + algorithm, keystore, keystorePassword, truststore, random, null), + nameResolver); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final X509HostnameVerifier hostnameVerifier) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(createSSLContext( + algorithm, keystore, keystorePassword, truststore, random, null), + hostnameVerifier); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + String algorithm, + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore, + final SecureRandom random, + final TrustStrategy trustStrategy, + final X509HostnameVerifier hostnameVerifier) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(createSSLContext( + algorithm, keystore, keystorePassword, truststore, random, trustStrategy), + hostnameVerifier); + } + + public SSLSocketFactory( + final KeyStore keystore, + final String keystorePassword, + final KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, keystore, keystorePassword, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + public SSLSocketFactory( + final KeyStore keystore, + final String keystorePassword) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException{ + this(TLS, keystore, keystorePassword, null, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + public SSLSocketFactory( + final KeyStore truststore) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, null, null, truststore, null, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + final TrustStrategy trustStrategy, + final X509HostnameVerifier hostnameVerifier) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, null, null, null, null, trustStrategy, hostnameVerifier); + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + final TrustStrategy trustStrategy) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + this(TLS, null, null, null, null, trustStrategy, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + public SSLSocketFactory(final SSLContext sslContext) { + this(sslContext, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); + } + + /** + * @deprecated Use {@link #SSLSocketFactory(SSLContext)} + */ + @Deprecated + public SSLSocketFactory( + final SSLContext sslContext, final HostNameResolver nameResolver) { + super(); + this.socketfactory = sslContext.getSocketFactory(); + this.hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER; + this.nameResolver = nameResolver; + } + + /** + * @since 4.1 + */ + public SSLSocketFactory( + final SSLContext sslContext, final X509HostnameVerifier hostnameVerifier) { + super(); + this.socketfactory = sslContext.getSocketFactory(); + this.hostnameVerifier = hostnameVerifier; + this.nameResolver = null; + } + + private SSLSocketFactory() { + super(); + this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory(); + this.hostnameVerifier = null; + this.nameResolver = null; + } + + /** + * @param params Optional parameters. Parameters passed to this method will have no effect. + * This method will create a unconnected instance of {@link Socket} class + * using {@link javax.net.ssl.SSLSocketFactory#createSocket()} method. + * @since 4.1 + */ + @SuppressWarnings("cast") + public Socket createSocket(final HttpParams params) throws IOException { + // the cast makes sure that the factory is working as expected + return (SSLSocket) this.socketfactory.createSocket(); + } + + @SuppressWarnings("cast") + public Socket createSocket() throws IOException { + // the cast makes sure that the factory is working as expected + return (SSLSocket) this.socketfactory.createSocket(); + } + + /** + * @since 4.1 + */ + public Socket connectSocket( + final Socket sock, + final InetSocketAddress remoteAddress, + final InetSocketAddress localAddress, + final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException { + if (remoteAddress == null) { + throw new IllegalArgumentException("Remote address may not be null"); + } + if (params == null) { + throw new IllegalArgumentException("HTTP parameters may not be null"); + } + SSLSocket sslsock = (SSLSocket) (sock != null ? sock : createSocket()); + if (localAddress != null) { +// sslsock.setReuseAddress(HttpConnectionParams.getSoReuseaddr(params)); + sslsock.bind(localAddress); + } + + int connTimeout = HttpConnectionParams.getConnectionTimeout(params); + int soTimeout = HttpConnectionParams.getSoTimeout(params); + + try { + sslsock.connect(remoteAddress, connTimeout); + } catch (SocketTimeoutException ex) { + throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/" + + remoteAddress.getAddress() + " timed out"); + } + sslsock.setSoTimeout(soTimeout); + if (this.hostnameVerifier != null) { + try { + this.hostnameVerifier.verify(remoteAddress.getHostName(), sslsock); + // verifyHostName() didn't blowup - good! + } catch (IOException iox) { + // close the socket before re-throwing the exception + try { sslsock.close(); } catch (Exception x) { /*ignore*/ } + throw iox; + } + } + return sslsock; + } + + + /** + * Checks whether a socket connection is secure. + * This factory creates TLS/SSL socket connections + * which, by default, are considered secure. + * <br/> + * Derived classes may override this method to perform + * runtime checks, for example based on the cypher suite. + * + * @param sock the connected socket + * + * @return <code>true</code> + * + * @throws IllegalArgumentException if the argument is invalid + */ + public boolean isSecure(final Socket sock) throws IllegalArgumentException { + if (sock == null) { + throw new IllegalArgumentException("Socket may not be null"); + } + // This instanceof check is in line with createSocket() above. + if (!(sock instanceof SSLSocket)) { + throw new IllegalArgumentException("Socket not created by this factory"); + } + // This check is performed last since it calls the argument object. + if (sock.isClosed()) { + throw new IllegalArgumentException("Socket is closed"); + } + return true; + } + + /** + * @since 4.1 + */ + public Socket createLayeredSocket( + final Socket socket, + final String host, + final int port, + final boolean autoClose) throws IOException, UnknownHostException { + SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket( + socket, + host, + port, + autoClose + ); + if (this.hostnameVerifier != null) { + this.hostnameVerifier.verify(host, sslSocket); + } + // verifyHostName() didn't blowup - good! + return sslSocket; + } + + @Deprecated + public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) { + if ( hostnameVerifier == null ) { + throw new IllegalArgumentException("Hostname verifier may not be null"); + } + this.hostnameVerifier = hostnameVerifier; + } + + public X509HostnameVerifier getHostnameVerifier() { + return this.hostnameVerifier; + } + + /** + * @deprecated Use {@link #connectSocket(Socket, InetSocketAddress, InetSocketAddress, HttpParams)} + */ + @Deprecated + public Socket connectSocket( + final Socket socket, + final String host, int port, + final InetAddress localAddress, int localPort, + final HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException { + InetSocketAddress local = null; + if (localAddress != null || localPort > 0) { + // we need to bind explicitly + if (localPort < 0) { + localPort = 0; // indicates "any" + } + local = new InetSocketAddress(localAddress, localPort); + } + InetAddress remoteAddress; + if (this.nameResolver != null) { + remoteAddress = this.nameResolver.resolve(host); + } else { + remoteAddress = InetAddress.getByName(host); + } + InetSocketAddress remote = new InetSocketAddress(remoteAddress, port); + return connectSocket(socket, remote, local, params); + } + + /** + * @deprecated Use {@link #createLayeredSocket(Socket, String, int, boolean)} + */ + @Deprecated + public Socket createSocket( + final Socket socket, + final String host, int port, + boolean autoClose) throws IOException, UnknownHostException { + return createLayeredSocket(socket, host, port, autoClose); + } + +} diff --git a/src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java b/src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java new file mode 100644 index 00000000..f2364368 --- /dev/null +++ b/src/github/daneren2005/dsub/service/ssl/TrustManagerDecorator.java @@ -0,0 +1,65 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package github.daneren2005.dsub.service.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + + +/** + * @since 4.1 + */ +class TrustManagerDecorator implements X509TrustManager { + + private final X509TrustManager trustManager; + private final TrustStrategy trustStrategy; + + TrustManagerDecorator(final X509TrustManager trustManager, final TrustStrategy trustStrategy) { + super(); + this.trustManager = trustManager; + this.trustStrategy = trustStrategy; + } + + public void checkClientTrusted( + final X509Certificate[] chain, final String authType) throws CertificateException { + this.trustManager.checkClientTrusted(chain, authType); + } + + public void checkServerTrusted( + final X509Certificate[] chain, final String authType) throws CertificateException { + if (!this.trustStrategy.isTrusted(chain, authType)) { + this.trustManager.checkServerTrusted(chain, authType); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return this.trustManager.getAcceptedIssuers(); + } + +} diff --git a/src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java b/src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java new file mode 100644 index 00000000..637a8931 --- /dev/null +++ b/src/github/daneren2005/dsub/service/ssl/TrustSelfSignedStrategy.java @@ -0,0 +1,44 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package github.daneren2005.dsub.service.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * A trust strategy that accepts self-signed certificates as trusted. Verification of all other + * certificates is done by the trust manager configured in the SSL context. + * + * @since 4.1 + */ +public class TrustSelfSignedStrategy implements TrustStrategy { + + public boolean isTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { + return true; + } + +} diff --git a/src/github/daneren2005/dsub/service/ssl/TrustStrategy.java b/src/github/daneren2005/dsub/service/ssl/TrustStrategy.java new file mode 100644 index 00000000..334a97c5 --- /dev/null +++ b/src/github/daneren2005/dsub/service/ssl/TrustStrategy.java @@ -0,0 +1,57 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package github.daneren2005.dsub.service.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * A strategy to establish trustworthiness of certificates without consulting the trust manager + * configured in the actual SSL context. This interface can be used to override the standard + * JSSE certificate verification process. + * + * @since 4.1 + */ +public interface TrustStrategy { + + /** + * Determines whether the certificate chain can be trusted without consulting the trust manager + * configured in the actual SSL context. This method can be used to override the standard JSSE + * certificate verification process. + * <p> + * Please note that, if this method returns <code>false</code>, the trust manager configured + * in the actual SSL context can still clear the certificate as trusted. + * + * @param chain the peer certificate chain + * @param authType the authentication type based on the client certificate + * @return <code>true</code> if the certificate can be trusted without verification by + * the trust manager, <code>false</code> otherwise. + * @throws CertificateException thrown if the certificate is not trusted or invalid. + */ + boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException; + +} diff --git a/src/github/daneren2005/dsub/updates/Updater.java b/src/github/daneren2005/dsub/updates/Updater.java new file mode 100644 index 00000000..60a17b67 --- /dev/null +++ b/src/github/daneren2005/dsub/updates/Updater.java @@ -0,0 +1,91 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.updates;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.util.Log;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.Util;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Scott
+ */
+public class Updater {
+ protected String TAG = Updater.class.getSimpleName();
+ protected int version;
+ protected Context context;
+
+ public Updater(int version) {
+ this.version = version;
+ }
+
+ public void checkUpdates(Context context) {
+ this.context = context;
+ List<Updater> updaters = new ArrayList<Updater>();
+ updaters.add(new Updater403());
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ int lastVersion = prefs.getInt(Constants.LAST_VERSION, 0);
+ if(lastVersion == 0) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
+ }
+ else if(version > lastVersion) {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.LAST_VERSION, version);
+ editor.commit();
+
+ Log.i(TAG, "Updating from version " + lastVersion + " to " + version);
+ for(Updater updater: updaters) {
+ if(updater.shouldUpdate(lastVersion)) {
+ new BackgroundUpdate().execute(updater);
+ }
+ }
+ }
+ }
+
+ public String getName() {
+ return this.TAG;
+ }
+
+ private class BackgroundUpdate extends AsyncTask<Updater, Void, Void> {
+ @Override
+ protected Void doInBackground(Updater... params) {
+ try {
+ params[0].update(context);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to run update for " + params[0].getName());
+ }
+ return null;
+ }
+ }
+
+ public boolean shouldUpdate(int version) {
+ return this.version > version;
+ }
+ public void update(Context context) {
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/updates/Updater403.java b/src/github/daneren2005/dsub/updates/Updater403.java new file mode 100644 index 00000000..17947ce5 --- /dev/null +++ b/src/github/daneren2005/dsub/updates/Updater403.java @@ -0,0 +1,58 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.updates;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.dsub.updates.Updater;
+import github.daneren2005.dsub.util.Constants;
+import github.daneren2005.dsub.util.FileUtil;
+import java.io.File;
+
+/**
+ *
+ * @author Scott
+ */
+public class Updater403 extends Updater {
+ public Updater403() {
+ super(403);
+ TAG = Updater403.class.getSimpleName();
+ }
+
+ @Override
+ public void update(Context context) {
+ // Rename cover.jpeg to cover.jpg
+ Log.i(TAG, "Running Updater403: updating cover.jpg to albumart.jpg");
+ File dir = FileUtil.getMusicDirectory(context);
+ if(dir != null) {
+ moveArt(dir);
+ }
+ }
+
+ private void moveArt(File dir) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ moveArt(file);
+ } else if("cover.jpg".equals(file.getName()) || "cover.jpeg".equals(file.getName())) {
+ File renamed = new File(dir, Constants.ALBUM_ART_FILE);
+ file.renameTo(renamed);
+ }
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/BackgroundTask.java b/src/github/daneren2005/dsub/util/BackgroundTask.java new file mode 100644 index 00000000..547bbd1e --- /dev/null +++ b/src/github/daneren2005/dsub/util/BackgroundTask.java @@ -0,0 +1,97 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.xmlpull.v1.XmlPullParserException; + +import android.app.Activity; +import android.os.Handler; +import android.util.Log; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.view.ErrorDialog; + +/** + * @author Sindre Mehus + */ +public abstract class BackgroundTask<T> implements ProgressListener { + + private static final String TAG = BackgroundTask.class.getSimpleName(); + private final Activity activity; + private final Handler handler; + + public BackgroundTask(Activity activity) { + this.activity = activity; + handler = new Handler(); + } + + protected Activity getActivity() { + return activity; + } + + protected Handler getHandler() { + return handler; + } + + public abstract void execute(); + + protected abstract T doInBackground() throws Throwable; + + protected abstract void done(T result); + + protected void error(Throwable error) { + Log.w(TAG, "Got exception: " + error, error); + new ErrorDialog(activity, getErrorMessage(error), true); + } + + protected String getErrorMessage(Throwable error) { + + if (error instanceof IOException && !Util.isNetworkConnected(activity)) { + return activity.getResources().getString(R.string.background_task_no_network); + } + + if (error instanceof FileNotFoundException) { + return activity.getResources().getString(R.string.background_task_not_found); + } + + if (error instanceof IOException) { + return activity.getResources().getString(R.string.background_task_network_error); + } + + if (error instanceof XmlPullParserException) { + return activity.getResources().getString(R.string.background_task_parse_error); + } + + String message = error.getMessage(); + if (message != null) { + return message; + } + return error.getClass().getSimpleName(); + } + + @Override + public abstract void updateProgress(final String message); + + @Override + public void updateProgress(int messageId) { + updateProgress(activity.getResources().getString(messageId)); + } +}
\ No newline at end of file diff --git a/src/github/daneren2005/dsub/util/CacheCleaner.java b/src/github/daneren2005/dsub/util/CacheCleaner.java new file mode 100644 index 00000000..62204c76 --- /dev/null +++ b/src/github/daneren2005/dsub/util/CacheCleaner.java @@ -0,0 +1,239 @@ +package github.daneren2005.dsub.util; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; +import android.os.StatFs; +import github.daneren2005.dsub.domain.Playlist; +import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.service.DownloadService; +import java.util.*; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class CacheCleaner { + + private static final String TAG = CacheCleaner.class.getSimpleName(); + private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L; + + private final Context context; + private final DownloadService downloadService; + + public CacheCleaner(Context context, DownloadService downloadService) { + this.context = context; + this.downloadService = downloadService; + } + + public void clean() { + new BackgroundCleanup().execute(); + } + public void cleanSpace() { + new BackgroundSpaceCleanup().execute(); + } + public void cleanPlaylists(List<Playlist> playlists) { + new BackgroundPlaylistsCleanup().execute(playlists); + } + + private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) { + for (File dir : dirs) { + if (undeletable.contains(dir)) { + continue; + } + + File[] children = dir.listFiles(); + + // No songs left in the folder + if(children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) { + Util.delete(children[0]); + children = dir.listFiles(); + } + + // Delete empty directory + if (children.length == 0) { + Util.delete(dir); + } + } + } + + private long getMinimumDelete(List<File> files) { + if(files.size() == 0) { + return 0L; + } + + long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L; + + long bytesUsedBySubsonic = 0L; + for (File file : files) { + bytesUsedBySubsonic += file.length(); + } + + // Ensure that file system is not more than 95% full. + StatFs stat = new StatFs(files.get(0).getPath()); + long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); + long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + long bytesUsedFs = bytesTotalFs - bytesAvailableFs; + long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE; + + long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L); + long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L); + long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit); + + Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available"); + Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes)); + Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic)); + Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete)); + + return bytesToDelete; + } + + private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) { + if (files.isEmpty()) { + return; + } + + long bytesDeleted = 0L; + for (File file : files) { + if(!deletePartials && bytesDeleted > bytesToDelete) break; + + if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) { + if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) { + long size = file.length(); + if (Util.delete(file)) { + bytesDeleted += size; + } + } + } + } + + Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted)); + } + + private void findCandidatesForDeletion(File file, List<File> files, List<File> dirs) { + if (file.isFile()) { + String name = file.getName(); + boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete."); + if (isCacheFile) { + files.add(file); + } + } else { + // Depth-first + for (File child : FileUtil.listFiles(file)) { + findCandidatesForDeletion(child, files, dirs); + } + dirs.add(file); + } + } + + private void sortByAscendingModificationTime(List<File> files) { + Collections.sort(files, new Comparator<File>() { + @Override + public int compare(File a, File b) { + if (a.lastModified() < b.lastModified()) { + return -1; + } + if (a.lastModified() > b.lastModified()) { + return 1; + } + return 0; + } + }); + } + + private Set<File> findUndeletableFiles() { + Set<File> undeletable = new HashSet<File>(5); + + for (DownloadFile downloadFile : downloadService.getDownloads()) { + undeletable.add(downloadFile.getPartialFile()); + undeletable.add(downloadFile.getCompleteFile()); + } + + undeletable.add(FileUtil.getMusicDirectory(context)); + return undeletable; + } + + private class BackgroundCleanup extends AsyncTask<Void, Void, Void> { + @Override + protected Void doInBackground(Void... params) { + if (downloadService == null) { + Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); + return null; + } + + try { + List<File> files = new ArrayList<File>(); + List<File> dirs = new ArrayList<File>(); + + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs); + sortByAscendingModificationTime(files); + + Set<File> undeletable = findUndeletableFiles(); + + deleteFiles(files, undeletable, getMinimumDelete(files), true); + deleteEmptyDirs(dirs, undeletable); + } catch (RuntimeException x) { + Log.e(TAG, "Error in cache cleaning.", x); + } + + return null; + } + } + + private class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void> { + @Override + protected Void doInBackground(Void... params) { + if (downloadService == null) { + Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); + return null; + } + + try { + List<File> files = new ArrayList<File>(); + List<File> dirs = new ArrayList<File>(); + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs); + + long bytesToDelete = getMinimumDelete(files); + if(bytesToDelete > 0L) { + sortByAscendingModificationTime(files); + Set<File> undeletable = findUndeletableFiles(); + deleteFiles(files, undeletable, bytesToDelete, false); + } + } catch (RuntimeException x) { + Log.e(TAG, "Error in cache cleaning.", x); + } + + return null; + } + } + + private class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void> { + @Override + protected Void doInBackground(List<Playlist>... params) { + try { + String server = Util.getServerName(context); + SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(server)); + List<Playlist> playlists = params[0]; + for (Playlist playlist : playlists) { + playlistFiles.remove(FileUtil.getPlaylistFile(server, playlist.getName())); + } + + for(File playlist : playlistFiles) { + playlist.delete(); + } + } catch (RuntimeException x) { + Log.e(TAG, "Error in playlist cache cleaning.", x); + } + + return null; + } + } +} diff --git a/src/github/daneren2005/dsub/util/CancellableTask.java b/src/github/daneren2005/dsub/util/CancellableTask.java new file mode 100644 index 00000000..933bc0c1 --- /dev/null +++ b/src/github/daneren2005/dsub/util/CancellableTask.java @@ -0,0 +1,87 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import android.util.Log; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public abstract class CancellableTask { + + private static final String TAG = CancellableTask.class.getSimpleName(); + + private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicBoolean cancelled = new AtomicBoolean(false); + private final AtomicReference<Thread> thread = new AtomicReference<Thread>(); + private final AtomicReference<OnCancelListener> cancelListener = new AtomicReference<OnCancelListener>(); + + public void cancel() { + Log.i(TAG, "Cancelling " + CancellableTask.this); + cancelled.set(true); + + OnCancelListener listener = cancelListener.get(); + if (listener != null) { + try { + listener.onCancel(); + } catch (Throwable x) { + Log.w(TAG, "Error when invoking OnCancelListener.", x); + } + } + } + + public boolean isCancelled() { + return cancelled.get(); + } + + public void setOnCancelListener(OnCancelListener listener) { + cancelListener.set(listener); + } + + public boolean isRunning() { + return running.get(); + } + + public abstract void execute(); + + public void start() { + thread.set(new Thread() { + @Override + public void run() { + running.set(true); + Log.i(TAG, "Starting thread for " + CancellableTask.this); + try { + execute(); + } finally { + running.set(false); + Log.i(TAG, "Stopping thread for " + CancellableTask.this); + } + } + }); + thread.get().start(); + } + + public static interface OnCancelListener { + void onCancel(); + } +} diff --git a/src/github/daneren2005/dsub/util/Constants.java b/src/github/daneren2005/dsub/util/Constants.java new file mode 100644 index 00000000..550749b9 --- /dev/null +++ b/src/github/daneren2005/dsub/util/Constants.java @@ -0,0 +1,149 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public final class Constants { + + // Character encoding used throughout. + public static final String UTF_8 = "UTF-8"; + + // REST protocol version and client ID. + // Note: Keep it as low as possible to maintain compatibility with older servers. + public static final String REST_PROTOCOL_VERSION = "1.2.0"; + public static final String REST_CLIENT_ID = "DSub"; + public static final String LAST_VERSION = "subsonic.version"; + + // Names for intent extras. + public static final String INTENT_EXTRA_NAME_ID = "subsonic.id"; + public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name"; + public static final String INTENT_EXTRA_NAME_PARENT_ID = "subsonic.parent_id"; + public static final String INTENT_EXTRA_NAME_PARENT_NAME = "subsonic.parent_name"; + public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist"; + public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title"; + public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall"; + public static final String INTENT_EXTRA_NAME_ERROR = "subsonic.error"; + public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query"; + public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id"; + public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA = "subsonic.albumlistextra"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize"; + public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset"; + public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle"; + public static final String INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh"; + public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch"; + public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ; + public static final String INTENT_EXTRA_NAME_DOWNLOAD = "subsonic.download"; + public static final String INTENT_EXTRA_VIEW_ALBUM = "subsonic.view_album"; + public static final String INTENT_EXTRA_NAME_PODCAST_ID = "subsonic.podcast.id"; + public static final String INTENT_EXTRA_NAME_PODCAST_NAME = "subsonic.podcast.name"; + public static final String INTENT_EXTRA_NAME_PODCAST_DESCRIPTION = "subsonic.podcast.description"; + + // Notification IDs. + public static final int NOTIFICATION_ID_PLAYING = 100; + public static final int NOTIFICATION_ID_ERROR = 101; + + // Preferences keys. + public static final String PREFERENCES_KEY_SERVER_KEY = "server"; + public static final String PREFERENCES_KEY_SERVER_COUNT = "serverCount"; + public static final String PREFERENCES_KEY_SERVER_ADD = "serverAdd"; + public static final String PREFERENCES_KEY_SERVER_REMOVE = "serverRemove"; + public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId"; + public static final String PREFERENCES_KEY_SERVER_NAME = "serverName"; + public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl"; + public static final String PREFERENCES_KEY_SERVER_VERSION = "serverVersion"; + public static final String PREFERENCES_KEY_TEST_CONNECTION = "serverTestConnection"; + public static final String PREFERENCES_KEY_OPEN_BROWSER = "openBrowser"; + public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId"; + public static final String PREFERENCES_KEY_USERNAME = "username"; + public static final String PREFERENCES_KEY_PASSWORD = "password"; + public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime"; + public static final String PREFERENCES_KEY_THEME = "theme"; + public static final String PREFERENCES_KEY_DISPLAY_TRACK = "displayTrack"; + public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi"; + public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile"; + public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI = "maxVideoBitrateWifi"; + public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE = "maxVideoBitrateMobile"; + public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout"; + public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize"; + public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation"; + public static final String PREFERENCES_KEY_PRELOAD_COUNT_WIFI = "preloadCountWifi"; + public static final String PREFERENCES_KEY_PRELOAD_COUNT_MOBILE = "preloadCountMobile"; + public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia"; + public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons"; + public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload"; + public static final String PREFERENCES_KEY_SCROBBLE = "scrobble"; + public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode"; + public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload"; + public static final String PREFERENCES_KEY_RANDOM_SIZE = "randomSize"; + public static final String PREFERENCES_KEY_SLEEP_TIMER = "sleepTimer"; + public static final String PREFERENCES_KEY_SLEEP_TIMER_DURATION = "sleepTimerDuration"; + public static final String PREFERENCES_KEY_OFFLINE = "offline"; + public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss"; + public static final String PREFERENCES_KEY_SHUFFLE_START_YEAR = "startYear"; + public static final String PREFERENCES_KEY_SHUFFLE_END_YEAR = "endYear"; + public static final String PREFERENCES_KEY_SHUFFLE_GENRE = "genre"; + public static final String PREFERENCES_KEY_KEEP_SCREEN_ON = "keepScreenOn"; + public static final String PREFERENCES_KEY_BUFFER_LENGTH = "bufferLength"; + public static final String PREFERENCES_EQUALIZER_ON = "equalizerOn"; + public static final String PREFERENCES_EQUALIZER_SETTINGS = "equalizerSettings"; + public static final String PREFERENCES_KEY_PERSISTENT_NOTIFICATION = "persistentNotification"; + public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback"; + public static final String PREFERENCES_KEY_SHUFFLE_MODE = "shuffleMode"; + public static final String PREFERENCES_KEY_CHAT_REFRESH = "chatRefreshRate"; + public static final String PREFERENCES_KEY_CHAT_ENABLED = "chatEnabled"; + public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer"; + + public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount"; + public static final String OFFLINE_SCROBBLE_ID = "scrobbleID"; + public static final String OFFLINE_SCROBBLE_SEARCH = "scrobbleTitle"; + public static final String OFFLINE_SCROBBLE_TIME = "scrobbleTime"; + public static final String OFFLINE_STAR_COUNT = "starCount"; + public static final String OFFLINE_STAR_ID = "starID"; + public static final String OFFLINE_STAR_SEARCH = "starTitle"; + public static final String OFFLINE_STAR_SETTING = "starSetting"; + + public static final String CACHE_KEY_IGNORE = "ignoreArticles"; + + public static final String MAIN_BACK_STACK = "backStackIds"; + public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize"; + public static final String MAIN_BACK_STACK_TABS = "backStackTabs"; + public static final String MAIN_BACK_STACK_POSITION = "backStackPosition"; + public static final String FRAGMENT_ID = "fragmentId"; + + // Name of the preferences file. + public static final String PREFERENCES_FILE_NAME = "github.daneren2005.dsub_preferences"; + public static final String OFFLINE_SYNC_NAME = "github.daneren2005.dsub.offline"; + public static final String OFFLINE_SYNC_DEFAULT = "syncDefaults"; + + // Number of free trial days for non-licensed servers. + public static final int FREE_TRIAL_DAYS = 30; + + // URL for project donations. + public static final String DONATION_URL = "http://subsonic.org/pages/android-donation.jsp"; + + public static final String ALBUM_ART_FILE = "albumart.jpg"; + + private Constants() { + } +} diff --git a/src/github/daneren2005/dsub/util/FileUtil.java b/src/github/daneren2005/dsub/util/FileUtil.java new file mode 100644 index 00000000..34bc82bd --- /dev/null +++ b/src/github/daneren2005/dsub/util/FileUtil.java @@ -0,0 +1,396 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.Iterator; +import java.util.List; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.util.Log; +import github.daneren2005.dsub.domain.Artist; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PodcastChannel; +import github.daneren2005.dsub.domain.PodcastEpisode; + +/** + * @author Sindre Mehus + */ +public class FileUtil { + + private static final String TAG = FileUtil.class.getSimpleName(); + private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"}; + private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"}; + private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma"); + private static final List<String> VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv"); + private static final List<String> PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u"); + private static final File DEFAULT_MUSIC_DIR = createDirectory("music"); + + public static File getAnySong(Context context) { + File dir = getMusicDirectory(context); + return getAnySong(context, dir); + } + private static File getAnySong(Context context, File dir) { + for(File file: dir.listFiles()) { + if(file.isDirectory()) { + return getAnySong(context, file); + } + + String extension = getExtension(file.getName()); + if(MUSIC_FILE_EXTENSIONS.contains(extension)) { + return file; + } + } + + return null; + } + + public static File getSongFile(Context context, MusicDirectory.Entry song) { + File dir = getAlbumDirectory(context, song); + + StringBuilder fileName = new StringBuilder(); + Integer track = song.getTrack(); + if (track != null) { + if (track < 10) { + fileName.append("0"); + } + fileName.append(track).append("-"); + } + + fileName.append(fileSystemSafe(song.getTitle())).append("."); + + if (song.getTranscodedSuffix() != null) { + fileName.append(song.getTranscodedSuffix()); + } else { + fileName.append(song.getSuffix()); + } + + return new File(dir, fileName.toString()); + } + + public static File getPlaylistFile(String server, String name) { + File playlistDir = getPlaylistDirectory(server); + return new File(playlistDir, fileSystemSafe(name) + ".m3u"); + } + public static File getPlaylistDirectory() { + File playlistDir = new File(getSubsonicDirectory(), "playlists"); + ensureDirectoryExistsAndIsReadWritable(playlistDir); + return playlistDir; + } + public static File getPlaylistDirectory(String server) { + File playlistDir = new File(getPlaylistDirectory(), server); + ensureDirectoryExistsAndIsReadWritable(playlistDir); + return playlistDir; + } + + public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) { + File albumDir = getAlbumDirectory(context, entry); + return getAlbumArtFile(albumDir); + } + + public static File getAlbumArtFile(File albumDir) { + return new File(albumDir, Constants.ALBUM_ART_FILE); + } + + public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) { + File albumArtFile = getAlbumArtFile(context, entry); + if (albumArtFile.exists()) { + Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath()); + return (bitmap == null) ? null : Bitmap.createScaledBitmap(bitmap, size, size, true); + } + return null; + } + + public static File getArtistDirectory(Context context, Artist artist) { + File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getName())); + return dir; + } + public static File getArtistDirectory(Context context, MusicDirectory.Entry artist) { + File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getTitle())); + return dir; + } + + public static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) { + File dir; + if (entry.getPath() != null) { + File f = new File(fileSystemSafeDir(entry.getPath())); + dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent())); + } else { + String artist = fileSystemSafe(entry.getArtist()); + String album = fileSystemSafe(entry.getAlbum()); + if("unnamed".equals(album)) { + album = fileSystemSafe(entry.getTitle()); + } + dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album); + } + return dir; + } + + public static String getPodcastPath(Context context, PodcastEpisode episode) { + return fileSystemSafe(episode.getArtist()) + "/" + fileSystemSafe(episode.getTitle()); + } + public static File getPodcastFile(Context context, String server) { + File dir = getPodcastDirectory(context); + return new File(dir.getPath() + "/" + fileSystemSafe(server)); + } + public static File getPodcastDirectory(Context context) { + File dir = new File(getSubsonicDirectory(), "podcasts"); + ensureDirectoryExistsAndIsReadWritable(dir); + return dir; + } + public static File getPodcastDirectory(Context context, PodcastChannel channel) { + File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel.getName())); + return dir; + } + public static File getPodcastDirectory(Context context, String channel) { + File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel)); + return dir; + } + + public static void createDirectoryForParent(File file) { + File dir = file.getParentFile(); + if (!dir.exists()) { + if (!dir.mkdirs()) { + Log.e(TAG, "Failed to create directory " + dir); + } + } + } + + private static File createDirectory(String name) { + File dir = new File(getSubsonicDirectory(), name); + if (!dir.exists() && !dir.mkdirs()) { + Log.e(TAG, "Failed to create " + name); + } + return dir; + } + + public static File getSubsonicDirectory() { + return new File(Environment.getExternalStorageDirectory(), "subsonic"); + } + + public static File getDefaultMusicDirectory() { + return DEFAULT_MUSIC_DIR; + } + + public static File getMusicDirectory(Context context) { + String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath()); + File dir = new File(path); + return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory(); + } + public static boolean deleteMusicDirectory(Context context) { + File musicDirectory = FileUtil.getMusicDirectory(context); + return Util.recursiveDelete(musicDirectory); + } + + public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) { + if (dir == null) { + return false; + } + + if (dir.exists()) { + if (!dir.isDirectory()) { + Log.w(TAG, dir + " exists but is not a directory."); + return false; + } + } else { + if (dir.mkdirs()) { + Log.i(TAG, "Created directory " + dir); + } else { + Log.w(TAG, "Failed to create directory " + dir); + return false; + } + } + + if (!dir.canRead()) { + Log.w(TAG, "No read permission for directory " + dir); + return false; + } + + if (!dir.canWrite()) { + Log.w(TAG, "No write permission for directory " + dir); + return false; + } + return true; + } + + /** + * Makes a given filename safe by replacing special characters like slashes ("/" and "\") + * with dashes ("-"). + * + * @param filename The filename in question. + * @return The filename with special characters replaced by hyphens. + */ + private static String fileSystemSafe(String filename) { + if (filename == null || filename.trim().length() == 0) { + return "unnamed"; + } + + for (String s : FILE_SYSTEM_UNSAFE) { + filename = filename.replace(s, "-"); + } + return filename; + } + + /** + * Makes a given filename safe by replacing special characters like colons (":") + * with dashes ("-"). + * + * @param path The path of the directory in question. + * @return The the directory name with special characters replaced by hyphens. + */ + private static String fileSystemSafeDir(String path) { + if (path == null || path.trim().length() == 0) { + return ""; + } + + for (String s : FILE_SYSTEM_UNSAFE_DIR) { + path = path.replace(s, "-"); + } + return path; + } + + /** + * Similar to {@link File#listFiles()}, but returns a sorted set. + * Never returns {@code null}, instead a warning is logged, and an empty set is returned. + */ + public static SortedSet<File> listFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + Log.w(TAG, "Failed to list children for " + dir.getPath()); + return new TreeSet<File>(); + } + + return new TreeSet<File>(Arrays.asList(files)); + } + + public static SortedSet<File> listMediaFiles(File dir) { + SortedSet<File> files = listFiles(dir); + Iterator<File> iterator = files.iterator(); + while (iterator.hasNext()) { + File file = iterator.next(); + if (!file.isDirectory() && !isMediaFile(file)) { + iterator.remove(); + } + } + return files; + } + + private static boolean isMediaFile(File file) { + String extension = getExtension(file.getName()); + return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension); + } + + public static boolean isMusicFile(File file) { + String extension = getExtension(file.getName()); + return MUSIC_FILE_EXTENSIONS.contains(extension); + } + public static boolean isVideoFile(File file) { + String extension = getExtension(file.getName()); + return VIDEO_FILE_EXTENSIONS.contains(extension); + } + + public static boolean isPlaylistFile(File file) { + String extension = getExtension(file.getName()); + return PLAYLIST_FILE_EXTENSIONS.contains(extension); + } + + /** + * Returns the extension (the substring after the last dot) of the given file. The dot + * is not included in the returned extension. + * + * @param name The filename in question. + * @return The extension, or an empty string if no extension is found. + */ + public static String getExtension(String name) { + int index = name.lastIndexOf('.'); + return index == -1 ? "" : name.substring(index + 1).toLowerCase(); + } + + /** + * Returns the base name (the substring before the last dot) of the given file. The dot + * is not included in the returned basename. + * + * @param name The filename in question. + * @return The base name, or an empty string if no basename is found. + */ + public static String getBaseName(String name) { + int index = name.lastIndexOf('.'); + return index == -1 ? name : name.substring(0, index); + } + + public static long getUsedSize(Context context, File file) { + long size = 0L; + + if(file.isFile()) { + return file.length(); + } else { + for (File child : FileUtil.listFiles(file)) { + size += getUsedSize(context, child); + } + return size; + } + } + + public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) { + File file = new File(context.getCacheDir(), fileName); + ObjectOutputStream out = null; + try { + out = new ObjectOutputStream(new FileOutputStream(file)); + out.writeObject(obj); + Log.i(TAG, "Serialized object to " + file); + return true; + } catch (Throwable x) { + Log.w(TAG, "Failed to serialize object to " + file); + return false; + } finally { + Util.close(out); + } + } + + public static <T extends Serializable> T deserialize(Context context, String fileName) { + File file = new File(context.getCacheDir(), fileName); + if (!file.exists() || !file.isFile()) { + return null; + } + + ObjectInputStream in = null; + try { + in = new ObjectInputStream(new FileInputStream(file)); + T result = (T) in.readObject(); + Log.i(TAG, "Deserialized object from " + file); + return result; + } catch (Throwable x) { + Log.w(TAG, "Failed to deserialize object from " + file, x); + return null; + } finally { + Util.close(in); + } + } +} diff --git a/src/github/daneren2005/dsub/util/ImageLoader.java b/src/github/daneren2005/dsub/util/ImageLoader.java new file mode 100644 index 00000000..331bc629 --- /dev/null +++ b/src/github/daneren2005/dsub/util/ImageLoader.java @@ -0,0 +1,332 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.media.RemoteControlClient; +import android.os.Handler; +import android.util.DisplayMetrics; +import android.util.Log; +import android.support.v4.util.LruCache; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Asynchronous loading of images, with caching. + * <p/> + * There should normally be only one instance of this class. + * + * @author Sindre Mehus + */ +@TargetApi(14) +public class ImageLoader implements Runnable { + + private static final String TAG = ImageLoader.class.getSimpleName(); + private static final int CONCURRENCY = 5; + + private Handler mHandler = new Handler(); + private Context context; + private LruCache<String, Bitmap> cache; + private Bitmap nowPlaying; + private final BlockingQueue<Task> queue; + private final int imageSizeDefault; + private final int imageSizeLarge; + private Drawable largeUnknownImage; + + public ImageLoader(Context context) { + this.context = context; + final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + final int cacheSize = maxMemory / 4; + cache = new LruCache<String, Bitmap>(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getRowBytes() * bitmap.getHeight() / 1024; + } + + @Override + protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) { + if(evicted && oldBitmap != nowPlaying) { + oldBitmap.recycle(); + } + } + }; + + queue = new LinkedBlockingQueue<Task>(500); + + // Determine the density-dependent image sizes. + imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); + + for (int i = 0; i < CONCURRENCY; i++) { + new Thread(this, "ImageLoader").start(); + } + + createLargeUnknownImage(context); + } + + private void createLargeUnknownImage(Context context) { + BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); + Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true); + largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap); + } + + public void 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) { + setUnknownImage(view, large); + return; + } + + int size = large ? imageSizeLarge : imageSizeDefault; + Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size)); + if (bitmap != null && !bitmap.isRecycled()) { + final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); + setImage(view, drawable, large); + if(large) { + nowPlaying = bitmap; + } + return; + } + + if (!large) { + setUnknownImage(view, large); + } + queue.offer(new Task(view.getContext(), entry, size, imageSizeLarge, large, new ViewTaskHandler(view, crossfade))); + } + + public void loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) { + if (largeUnknownImage != null && ((BitmapDrawable)largeUnknownImage).getBitmap().isRecycled()) + createLargeUnknownImage(context); + + if (entry == null || entry.getCoverArt() == null) { + setUnknownImage(remoteControl); + return; + } + + Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge)); + if (bitmap != null && !bitmap.isRecycled()) { + Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap); + setImage(remoteControl, drawable); + return; + } + + setUnknownImage(remoteControl); + queue.offer(new Task(context, entry, imageSizeLarge, imageSizeLarge, false, new RemoteControlClientTaskHandler(remoteControl))); + } + + private String getKey(String coverArtId, int size) { + return coverArtId + size; + } + + private void setImage(View view, Drawable drawable, boolean crossfade) { + if (view instanceof TextView) { + // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though. + TextView textView = (TextView) view; + textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + } else if (view instanceof ImageView) { + ImageView imageView = (ImageView) view; + if (crossfade) { + + Drawable existingDrawable = imageView.getDrawable(); + if (existingDrawable == null) { + Bitmap emptyImage; + if(drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) { + emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + } else { + emptyImage = Bitmap.createBitmap(imageSizeDefault, imageSizeDefault, Bitmap.Config.ARGB_8888); + } + existingDrawable = new BitmapDrawable(emptyImage); + } else { + // Try to get rid of old transitions + try { + TransitionDrawable tmp = (TransitionDrawable) existingDrawable; + int layers = tmp.getNumberOfLayers(); + existingDrawable = tmp.getDrawable(layers - 1); + } catch(Exception e) { + // Do nothing, just means that the drawable is a flat image + } + } + if (!(((BitmapDrawable)existingDrawable).getBitmap().isRecycled())) + { // We will flow through to the non-transition if the old image is recycled... Yay 4.3 + Drawable[] layers = new Drawable[]{existingDrawable, drawable}; + + TransitionDrawable transitionDrawable = new TransitionDrawable(layers); + imageView.setImageDrawable(transitionDrawable); + transitionDrawable.startTransition(250); + return; + } + } + imageView.setImageDrawable(drawable); + return; + } + } + + private void setImage(RemoteControlClient remoteControl, Drawable drawable) { + if(remoteControl != null && drawable != null) { + Bitmap origBitmap = ((BitmapDrawable)drawable).getBitmap(); + if ( origBitmap != null && !origBitmap.isRecycled()) { + remoteControl.editMetadata(false) + .putBitmap( + RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, + origBitmap) + .apply(); + } else { + Log.e(TAG, "Tried to load a recycled bitmap."); + remoteControl.editMetadata(false) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null) + .apply(); + } + } + } + + 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 void clear() { + queue.clear(); + } + + @Override + public void run() { + while (true) { + try { + Task task = queue.take(); + task.execute(); + } catch (Throwable x) { + Log.e(TAG, "Unexpected exception in ImageLoader.", x); + } + } + } + + private class Task { + private final Context mContext; + private final MusicDirectory.Entry mEntry; + private final int mSize; + private final int mSaveSize; + private final boolean mIsNowPlaying; + private ImageLoaderTaskHandler mTaskHandler; + + public Task(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, ImageLoaderTaskHandler taskHandler) { + mContext = context; + mEntry = entry; + mSize = size; + mSaveSize = saveSize; + mIsNowPlaying = isNowPlaying; + mTaskHandler = taskHandler; + } + + public void execute() { + try { + loadImage(); + } catch(OutOfMemoryError e) { + Log.w(TAG, "Ran out of memory trying to load image, try cleanup and retry"); + cache.evictAll(); + System.gc(); + } + } + public void loadImage() { + try { + MusicService musicService = MusicServiceFactory.getMusicService(mContext); + Bitmap bitmap = musicService.getCoverArt(mContext, mEntry, mSize, mSaveSize, null); + String key = getKey(mEntry.getCoverArt(), mSize); + cache.put(key, bitmap); + // Make sure key is the most recently "used" + cache.get(key); + if(mIsNowPlaying) { + nowPlaying = bitmap; + } + + final Drawable drawable = Util.createDrawableFromBitmap(mContext, bitmap); + mTaskHandler.setDrawable(drawable); + mHandler.post(mTaskHandler); + } catch (Throwable x) { + Log.e(TAG, "Failed to download album art.", x); + } + } + } + + private abstract class ImageLoaderTaskHandler implements Runnable { + + protected Drawable mDrawable; + + public void setDrawable(Drawable drawable) { + mDrawable = drawable; + } + + } + + private class ViewTaskHandler extends ImageLoaderTaskHandler { + + protected boolean mCrossfade; + private View mView; + + public ViewTaskHandler(View view, boolean crossfade) { + mCrossfade = crossfade; + mView = view; + } + + @Override + public void run() { + setImage(mView, mDrawable, mCrossfade); + } + } + + private class RemoteControlClientTaskHandler extends ImageLoaderTaskHandler { + + private RemoteControlClient mRemoteControl; + + public RemoteControlClientTaskHandler(RemoteControlClient remoteControl) { + mRemoteControl = remoteControl; + } + + @Override + public void run() { + setImage(mRemoteControl, mDrawable); + } + } +} diff --git a/src/github/daneren2005/dsub/util/LoadingTask.java b/src/github/daneren2005/dsub/util/LoadingTask.java new file mode 100644 index 00000000..9ab5c86d --- /dev/null +++ b/src/github/daneren2005/dsub/util/LoadingTask.java @@ -0,0 +1,97 @@ +package github.daneren2005.dsub.util;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class LoadingTask<T> extends BackgroundTask<T> {
+
+ private final Activity tabActivity;
+ private ProgressDialog loading;
+ private Thread thread;
+ private final boolean cancellable;
+ private boolean cancelled = false;
+
+ public LoadingTask(Activity activity) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = true;
+ }
+ public LoadingTask(Activity activity, final boolean cancellable) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = cancellable;
+ }
+
+ @Override
+ public void execute() {
+ loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ cancel();
+ }
+
+ });
+
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.cancel();
+ done(result);
+ }
+ });
+ } catch (final Throwable t) {
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.cancel();
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ protected void cancel() {
+ cancelled = true;
+ if (thread != null) {
+ thread.interrupt();
+ }
+ }
+
+ private boolean isCancelled() {
+ return (tabActivity instanceof SubsonicActivity && ((SubsonicActivity)tabActivity).isDestroyed()) || cancelled;
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ if(!cancelled) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.setMessage(message);
+ }
+ });
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/Pair.java b/src/github/daneren2005/dsub/util/Pair.java new file mode 100644 index 00000000..54386a62 --- /dev/null +++ b/src/github/daneren2005/dsub/util/Pair.java @@ -0,0 +1,54 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Pair<S, T> implements Serializable { + + private S first; + private T second; + + public Pair() { + } + + public Pair(S first, T second) { + this.first = first; + this.second = second; + } + + public S getFirst() { + return first; + } + + public void setFirst(S first) { + this.first = first; + } + + public T getSecond() { + return second; + } + + public void setSecond(T second) { + this.second = second; + } +} diff --git a/src/github/daneren2005/dsub/util/ProgressListener.java b/src/github/daneren2005/dsub/util/ProgressListener.java new file mode 100644 index 00000000..c6d58f42 --- /dev/null +++ b/src/github/daneren2005/dsub/util/ProgressListener.java @@ -0,0 +1,27 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +/** + * @author Sindre Mehus + */ +public interface ProgressListener { + void updateProgress(String message); + void updateProgress(int messageId); +} diff --git a/src/github/daneren2005/dsub/util/SettingsBackupAgent.java b/src/github/daneren2005/dsub/util/SettingsBackupAgent.java new file mode 100644 index 00000000..7eb6d137 --- /dev/null +++ b/src/github/daneren2005/dsub/util/SettingsBackupAgent.java @@ -0,0 +1,31 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus +*/ +package github.daneren2005.dsub.util; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.SharedPreferencesBackupHelper; +import github.daneren2005.dsub.util.Constants; + +public class SettingsBackupAgent extends BackupAgentHelper { + public void onCreate() { + super.onCreate(); + SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, Constants.PREFERENCES_FILE_NAME); + addHelper("mypreferences", helper); + } + } diff --git a/src/github/daneren2005/dsub/util/ShufflePlayBuffer.java b/src/github/daneren2005/dsub/util/ShufflePlayBuffer.java new file mode 100644 index 00000000..195fe913 --- /dev/null +++ b/src/github/daneren2005/dsub/util/ShufflePlayBuffer.java @@ -0,0 +1,127 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.service.MusicService; +import github.daneren2005.dsub.service.MusicServiceFactory; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class ShufflePlayBuffer { + + private static final String TAG = ShufflePlayBuffer.class.getSimpleName(); + private static final int CAPACITY = 50; + private static final int REFILL_THRESHOLD = 40; + + private final ScheduledExecutorService executorService; + private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>(); + private int lastCount = -1; + private Context context; + private int currentServer; + private String currentFolder = ""; + + private String genre = ""; + private String startYear = ""; + private String endYear = ""; + + public ShufflePlayBuffer(Context context) { + this.context = context; + executorService = Executors.newSingleThreadScheduledExecutor(); + Runnable runnable = new Runnable() { + @Override + public void run() { + refill(); + } + }; + executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS); + } + + public List<MusicDirectory.Entry> get(int size) { + clearBufferIfnecessary(); + + List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size); + synchronized (buffer) { + while (!buffer.isEmpty() && result.size() < size) { + result.add(buffer.remove(buffer.size() - 1)); + } + } + Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining."); + return result; + } + + public void shutdown() { + executorService.shutdown(); + } + + private void refill() { + + // Check if active server has changed. + clearBufferIfnecessary(); + + if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0) { + return; + } + + try { + MusicService service = MusicServiceFactory.getMusicService(context); + int n = CAPACITY - buffer.size(); + String folder = Util.getSelectedMusicFolderId(context); + MusicDirectory songs = service.getRandomSongs(n, folder, genre, startYear, endYear, context, null); + + synchronized (buffer) { + buffer.addAll(songs.getChildren()); + Log.i(TAG, "Refilled shuffle play buffer with " + songs.getChildrenSize() + " songs."); + lastCount = songs.getChildrenSize(); + } + } catch (Exception x) { + Log.w(TAG, "Failed to refill shuffle play buffer.", x); + } + } + + private void clearBufferIfnecessary() { + synchronized (buffer) { + final SharedPreferences prefs = Util.getPreferences(context); + if (currentServer != Util.getActiveServer(context) + || (currentFolder != null && !currentFolder.equals(Util.getSelectedMusicFolderId(context))) + || (genre != null && !genre.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, ""))) + || (startYear != null && !startYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, ""))) + || (endYear != null && !endYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "")))) { + lastCount = -1; + currentServer = Util.getActiveServer(context); + currentFolder = Util.getSelectedMusicFolderId(context); + genre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, ""); + startYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, ""); + endYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, ""); + buffer.clear(); + } + } + } +} diff --git a/src/github/daneren2005/dsub/util/SilentBackgroundTask.java b/src/github/daneren2005/dsub/util/SilentBackgroundTask.java new file mode 100644 index 00000000..7bceb467 --- /dev/null +++ b/src/github/daneren2005/dsub/util/SilentBackgroundTask.java @@ -0,0 +1,67 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import android.app.Activity; + +/** + * @author Sindre Mehus + */ +public abstract class SilentBackgroundTask<T> extends BackgroundTask<T> { + + public SilentBackgroundTask(Activity activity) { + super(activity); + } + + @Override + public void execute() { + Thread thread = new Thread() { + @Override + public void run() { + try { + final T result = doInBackground(); + + getHandler().post(new Runnable() { + @Override + public void run() { + done(result); + } + }); + + } catch (final Throwable t) { + getHandler().post(new Runnable() { + @Override + public void run() { + error(t); + } + }); + } + } + }; + thread.start(); + } + + @Override + public void updateProgress(int messageId) { + } + + @Override + public void updateProgress(String message) { + } +} diff --git a/src/github/daneren2005/dsub/util/SimpleServiceBinder.java b/src/github/daneren2005/dsub/util/SimpleServiceBinder.java new file mode 100644 index 00000000..9c0b36a9 --- /dev/null +++ b/src/github/daneren2005/dsub/util/SimpleServiceBinder.java @@ -0,0 +1,37 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import android.os.Binder; + +/** + * @author Sindre Mehus + */ +public class SimpleServiceBinder<S> extends Binder { + + private final S service; + + public SimpleServiceBinder(S service) { + this.service = service; + } + + public S getService() { + return service; + } +} diff --git a/src/github/daneren2005/dsub/util/TabBackgroundTask.java b/src/github/daneren2005/dsub/util/TabBackgroundTask.java new file mode 100644 index 00000000..c345b982 --- /dev/null +++ b/src/github/daneren2005/dsub/util/TabBackgroundTask.java @@ -0,0 +1,67 @@ +package github.daneren2005.dsub.util;
+
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class TabBackgroundTask<T> extends BackgroundTask<T> {
+
+ private final SubsonicFragment tabFragment;
+
+ public TabBackgroundTask(SubsonicFragment fragment) {
+ super(fragment.getActivity());
+ tabFragment = fragment;
+ }
+
+ @Override
+ public void execute() {
+ tabFragment.setProgressVisible(true);
+
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.setProgressVisible(false);
+ done(result);
+ }
+ });
+ } catch (final Throwable t) {
+ if (isCancelled()) {
+ return;
+ }
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.setProgressVisible(false);
+ error(t);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ private boolean isCancelled() {
+ return !tabFragment.isAdded();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.updateProgress(message);
+ }
+ });
+ }
+}
diff --git a/src/github/daneren2005/dsub/util/TimeLimitedCache.java b/src/github/daneren2005/dsub/util/TimeLimitedCache.java new file mode 100644 index 00000000..8b7df783 --- /dev/null +++ b/src/github/daneren2005/dsub/util/TimeLimitedCache.java @@ -0,0 +1,55 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import java.lang.ref.SoftReference; +import java.util.concurrent.TimeUnit; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class TimeLimitedCache<T> { + + private SoftReference<T> value; + private final long ttlMillis; + private long expires; + + public TimeLimitedCache(long ttl, TimeUnit timeUnit) { + this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit); + } + + public T get() { + return System.currentTimeMillis() < expires ? value.get() : null; + } + + public void set(T value) { + set(value, ttlMillis, TimeUnit.MILLISECONDS); + } + + public void set(T value, long ttl, TimeUnit timeUnit) { + this.value = new SoftReference<T>(value); + expires = System.currentTimeMillis() + timeUnit.toMillis(ttl); + } + + public void clear() { + expires = 0L; + value = null; + } +} diff --git a/src/github/daneren2005/dsub/util/Util.java b/src/github/daneren2005/dsub/util/Util.java new file mode 100644 index 00000000..a6fdd987 --- /dev/null +++ b/src/github/daneren2005/dsub/util/Util.java @@ -0,0 +1,1173 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.util; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.net.ConnectivityManager; +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.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.util.Log; +import android.view.Gravity; +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.MainActivity; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.domain.RepeatMode; +import github.daneren2005.dsub.domain.Version; +import github.daneren2005.dsub.provider.DSubWidgetProvider; +import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import org.apache.http.HttpEntity; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public final class Util { + private static final String TAG = Util.class.getSimpleName(); + + private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB"); + private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB"); + private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB"); + + private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; + + public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED"; + public static final String EVENT_PLAYSTATE_CHANGED = "github.daneren2005.dsub.EVENT_PLAYSTATE_CHANGED"; + + public static final String AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged"; + public static final String AVRCP_METADATA_CHANGED = "com.android.music.metachanged"; + + private static boolean hasFocus = false; + private static boolean pauseFocus = false; + private static boolean lowerFocus = false; + + private static final Map<Integer, Version> SERVER_REST_VERSIONS = new ConcurrentHashMap<Integer, Version>(); + + // Used by hexEncode() + private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private final static Pair<Integer, Integer> NOTIFICATION_TEXT_COLORS = new Pair<Integer, Integer>(); + private static Toast toast; + + private Util() { + } + + public static boolean isOffline(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false); + } + + public static void setOffline(Context context, boolean offline) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, offline); + editor.commit(); + } + + public static boolean isScreenLitOnDownload(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false); + } + + public static RepeatMode getRepeatMode(Context context) { + SharedPreferences prefs = getPreferences(context); + return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name())); + } + + public static void setRepeatMode(Context context, RepeatMode repeatMode) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name()); + editor.commit(); + } + + public static boolean isScrobblingEnabled(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false); + } + + public static void setActiveServer(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); + editor.commit(); + } + + public static int getActiveServer(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + } + + public static boolean checkServerVersion(Context context, String requiredVersion) { + Version version = Util.getServerRestVersion(context); + Version required = new Version(requiredVersion); + if(version != null && version.compareTo(required) >= 0) { + return true; + } else { + return false; + } + } + + public static int getServerCount(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1); + } + + public static void removeInstanceName(Context context, int instance, int activeInstance) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + + int newInstance = instance + 1; + + String server = prefs.getString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null); + String serverName = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null); + String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null); + String userName = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null); + + editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + instance, server); + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, serverName); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, serverUrl); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + instance, userName); + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + instance, password); + + editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null); + editor.putString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null); + editor.commit(); + + if (instance == activeInstance) { + if(instance != 1) { + Util.setActiveServer(context, 1); + } else { + Util.setOffline(context, true); + } + } else if (newInstance == activeInstance) { + Util.setActiveServer(context, instance); + } + } + + public static String getServerName(Context context) { + SharedPreferences prefs = getPreferences(context); + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + } + public static String getServerName(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + } + + public static String getUserName(Context context, int instance) { + if (instance == 0) { + return context.getResources().getString(R.string.main_offline); + } + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + } + + public static void setServerRestVersion(Context context, Version version) { + int instance = getActiveServer(context); + Version current = SERVER_REST_VERSIONS.get(instance); + if(current != version) { + SERVER_REST_VERSIONS.put(instance, version); + SharedPreferences.Editor editor = getPreferences(context).edit(); + editor.putString(Constants.PREFERENCES_KEY_SERVER_VERSION + instance, version.getVersion()); + editor.commit(); + } + } + + public static Version getServerRestVersion(Context context) { + int instance = getActiveServer(context); + Version version = SERVER_REST_VERSIONS.get(instance); + if(version == null) { + SharedPreferences prefs = getPreferences(context); + String versionString = prefs.getString(Constants.PREFERENCES_KEY_SERVER_VERSION + instance, null); + if(versionString != null) { + version = new Version(versionString); + SERVER_REST_VERSIONS.put(instance, version); + } + } + return version; + } + + public static void setSelectedMusicFolderId(Context context, String musicFolderId) { + int instance = getActiveServer(context); + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId); + editor.commit(); + } + + public static String getSelectedMusicFolderId(Context context) { + SharedPreferences prefs = getPreferences(context); + int instance = getActiveServer(context); + return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); + } + + public static String getTheme(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_THEME, null); + } + + public static boolean getDisplayTrack(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_DISPLAY_TRACK, false); + } + + public static int getMaxBitrate(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo == null) { + return 0; + } + + boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0")); + } + + public static int getMaxVideoBitrate(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo == null) { + return 0; + } + + boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE, "0")); + } + + public static int getPreloadCount(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo == null) { + return 3; + } + + SharedPreferences prefs = getPreferences(context); + boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + int preloadCount = Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI : Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE, "-1")); + return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount; + } + + public static int getCacheSizeMB(Context context) { + SharedPreferences prefs = getPreferences(context); + int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); + return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; + } + + public static String getRestUrl(Context context, String method) { + StringBuilder builder = new StringBuilder(); + + SharedPreferences prefs = getPreferences(context); + + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + + // Slightly obfuscate password + password = "enc:" + Util.utf8HexEncode(password); + + builder.append(serverUrl); + if (builder.charAt(builder.length() - 1) != '/') { + builder.append("/"); + } + builder.append("rest/").append(method).append(".view"); + builder.append("?u=").append(username); + builder.append("&p=").append(password); + builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION); + builder.append("&c=").append(Constants.REST_CLIENT_ID); + + return builder.toString(); + } + + public static String getVideoPlayerType(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_VIDEO_PLAYER, "raw"); + } + + public static SharedPreferences getPreferences(Context context) { + return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0); + } + public static SharedPreferences getOfflineSync(Context context) { + return context.getSharedPreferences(Constants.OFFLINE_SYNC_NAME, 0); + } + + public static String getSyncDefault(Context context) { + SharedPreferences prefs = Util.getOfflineSync(context); + return prefs.getString(Constants.OFFLINE_SYNC_DEFAULT, null); + } + public static void setSyncDefault(Context context, String defaultValue) { + SharedPreferences.Editor editor = Util.getOfflineSync(context).edit(); + editor.putString(Constants.OFFLINE_SYNC_DEFAULT, defaultValue); + editor.commit(); + } + + public static int offlineScrobblesCount(Context context) { + SharedPreferences offline = getOfflineSync(context); + return offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); + } + public static int offlineStarsCount(Context context) { + SharedPreferences offline = getOfflineSync(context); + return offline.getInt(Constants.OFFLINE_STAR_COUNT, 0); + } + + public static String parseOfflineIDSearch(Context context, String id, String cacheLocation) { + String name = id.replace(cacheLocation, ""); + if(name.startsWith("/")) { + name = name.substring(1); + } + name = name.replace(".complete", "").replace(".partial", ""); + int index = name.lastIndexOf("."); + name = index == -1 ? name : name.substring(0, index); + String[] details = name.split("/"); + + String title = details[details.length - 1]; + if(index == -1) { + if(details.length > 1) { + String artist = "artist:\"" + details[details.length - 2] + "\""; + String simpleArtist = "artist:\"" + title + "\""; + title = "album:\"" + title + "\""; + if(details[details.length - 1].equals(details[details.length - 2])) { + name = title; + } else { + name = "(" + artist + " AND " + title + ")" + " OR " + simpleArtist; + } + } else { + name = "artist:\"" + title + "\" OR album:\"" + title + "\""; + } + } else { + String artist; + if(details.length > 2) { + artist = "artist:\"" + details[details.length - 3] + "\""; + } else { + artist = "(artist:\"" + details[0] + "\" OR album:\"" + details[0] + "\")"; + } + title = "title:\"" + title.substring(title.indexOf('-') + 1) + "\""; + name = artist + " AND " + title; + } + + return name; + } + + public static String getContentType(HttpEntity entity) { + if (entity == null || entity.getContentType() == null) { + return null; + } + return entity.getContentType().getValue(); + } + + public static int getRemainingTrialDays(Context context) { + SharedPreferences prefs = getPreferences(context); + long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L); + + if (installTime == 0L) { + installTime = System.currentTimeMillis(); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime); + editor.commit(); + } + + long now = System.currentTimeMillis(); + long millisPerDay = 24L * 60L * 60L * 1000L; + int daysSinceInstall = (int) ((now - installTime) / millisPerDay); + return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall); + } + + /** + * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>. + * <p/> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + public static long copy(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[1024 * 4]; + long count = 0; + int n; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + public static void atomicCopy(File from, File to) throws IOException { + FileInputStream in = null; + FileOutputStream out = null; + File tmp = null; + try { + tmp = new File(to.getPath() + ".tmp"); + in = new FileInputStream(from); + out = new FileOutputStream(tmp); + in.getChannel().transferTo(0, from.length(), out.getChannel()); + out.close(); + if (!tmp.renameTo(to)) { + throw new IOException("Failed to rename " + tmp + " to " + to); + } + Log.i(TAG, "Copied " + from + " to " + to); + } catch (IOException x) { + close(out); + delete(to); + throw x; + } finally { + close(in); + close(out); + delete(tmp); + } + } + public static void renameFile(File from, File to) throws IOException { + if(from.renameTo(to)) { + Log.i(TAG, "Renamed " + from + " to " + to); + } else { + atomicCopy(from, to); + } + } + + public static void close(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (Throwable x) { + // Ignored + } + } + + public static boolean delete(File file) { + if (file != null && file.exists()) { + if (!file.delete()) { + Log.w(TAG, "Failed to delete file " + file); + return false; + } + Log.i(TAG, "Deleted file " + file); + } + return true; + } + public static boolean recursiveDelete(File dir) { + if (dir != null && dir.exists()) { + for(File file: dir.listFiles()) { + if(file.isDirectory()) { + if(!recursiveDelete(file)) { + return false; + } + } else if(file.exists()) { + if(!file.delete()) { + return false; + } + } + } + return dir.delete(); + } + return false; + } + + public static void toast(Context context, int messageId) { + toast(context, messageId, true); + } + + public static void toast(Context context, int messageId, boolean shortDuration) { + toast(context, context.getString(messageId), shortDuration); + } + + public static void toast(Context context, String message) { + toast(context, message, true); + } + + public static void toast(Context context, String message, boolean shortDuration) { + if (toast == null) { + toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + } else { + toast.setText(message); + toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + } + toast.show(); + } + + public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick) { + Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick); + } + public static void confirmDialog(Context context, String action, String subject, DialogInterface.OnClickListener onClick) { + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.common_confirm) + .setMessage(context.getResources().getString(R.string.common_confirm_message, action, subject)) + .setPositiveButton(R.string.common_ok, onClick) + .setNegativeButton(R.string.common_cancel, null) + .show(); + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + * <ul> + * <li><code>format(918)</code> returns <em>"918 B"</em>.</li> + * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li> + * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li> + * </ul> + * This method assumes that 1 KB is 1024 bytes. + * To get a localized string, please use formatLocalizedBytes instead. + * + * @param byteCount The number of bytes. + * @return The formatted string. + */ + public static synchronized String formatBytes(long byteCount) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT; + return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + NumberFormat megaByteFormat = MEGA_BYTE_FORMAT; + return megaByteFormat.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + NumberFormat kiloByteFormat = KILO_BYTE_FORMAT; + return kiloByteFormat.format((double) byteCount / 1024); + } + + return byteCount + " B"; + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + * <ul> + * <li><code>format(918)</code> returns <em>"918 B"</em>.</li> + * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li> + * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li> + * </ul> + * This method assumes that 1 KB is 1024 bytes. + * This version of the method returns a localized string. + * + * @param byteCount The number of bytes. + * @return The formatted string. + */ + public static synchronized String formatLocalizedBytes(long byteCount, Context context) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + if (GIGA_BYTE_LOCALIZED_FORMAT == null) { + GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte)); + } + + return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + if (MEGA_BYTE_LOCALIZED_FORMAT == null) { + MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte)); + } + + return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + if (KILO_BYTE_LOCALIZED_FORMAT == null) { + KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte)); + } + + return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024); + } + + if (BYTE_LOCALIZED_FORMAT == null) { + BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte)); + } + + return BYTE_LOCALIZED_FORMAT.format((double) byteCount); + } + + public static String formatDuration(Integer seconds) { + if (seconds == null) { + return null; + } + + int hours = seconds / 3600; + int minutes = (seconds / 60) % 60; + int secs = seconds % 60; + + StringBuilder builder = new StringBuilder(7); + if(hours > 0) { + builder.append(hours).append(":"); + if(minutes < 10) { + builder.append("0"); + } + } + builder.append(minutes).append(":"); + if (secs < 10) { + builder.append("0"); + } + builder.append(secs); + return builder.toString(); + } + + public static boolean equals(Object object1, Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + return object1.equals(object2); + + } + + /** + * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes. + * + * @param s The string to encode. + * @return The encoded string. + */ + public static String utf8HexEncode(String s) { + if (s == null) { + return null; + } + byte[] utf8; + try { + utf8 = s.getBytes(Constants.UTF_8); + } catch (UnsupportedEncodingException x) { + throw new RuntimeException(x); + } + return hexEncode(utf8); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data Bytes to convert to hexadecimal characters. + * @return A string containing hexadecimal characters. + */ + public static String hexEncode(byte[] data) { + int length = data.length; + char[] out = new char[length << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < length; i++) { + out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4]; + out[j++] = HEX_DIGITS[0x0F & data[i]]; + } + return new String(out); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param s Data to digest. + * @return MD5 digest as a hex string. + */ + public static String md5Hex(String s) { + if (s == null) { + return null; + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return hexEncode(md5.digest(s.getBytes(Constants.UTF_8))); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + + public static boolean isNullOrWhiteSpace(String string) { + return string == null || string.isEmpty() || string.trim().isEmpty(); + } + + public static boolean isNetworkConnected(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + boolean connected = networkInfo != null && networkInfo.isConnected(); + + boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + boolean wifiRequired = isWifiRequiredForDownload(context); + + return connected && (!wifiRequired || wifiConnected); + } + + public static boolean isExternalStoragePresent() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + private static boolean isWifiRequiredForDownload(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false); + } + + public static void info(Context context, int titleId, int messageId) { + showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId); + } + public static void info(Context context, int titleId, String message) { + showDialog(context, android.R.drawable.ic_dialog_info, titleId, message); + } + public static void info(Context context, String title, String message) { + showDialog(context, android.R.drawable.ic_dialog_info, title, message); + } + + private static void showDialog(Context context, int icon, int titleId, int messageId) { + showDialog(context, icon, context.getResources().getString(titleId), context.getResources().getString(messageId)); + } + private static void showDialog(Context context, int icon, int titleId, String message) { + showDialog(context, icon, context.getResources().getString(titleId), message); + } + private static void showDialog(Context context, int icon, String title, String message) { + SpannableString ss = new SpannableString(message); + Linkify.addLinks(ss, Linkify.ALL); + + AlertDialog dialog = new AlertDialog.Builder(context) + .setIcon(icon) + .setTitle(title) + .setMessage(ss) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + + ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); + } + + public static void showPlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler, MusicDirectory.Entry song) { + // Set the icon, scrolling text and timestamp + final Notification notification = new Notification(R.drawable.stat_notify_playing, song.getTitle(), System.currentTimeMillis()); + notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; + + boolean playing = downloadService.getPlayerState() == PlayerState.STARTED; + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){ + RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded); + setupViews(expandedContentView,context,song, playing); + notification.bigContentView = expandedContentView; + } + + RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification); + setupViews(smallContentView, context, song, playing); + notification.contentView = smallContentView; + + Intent notificationIntent = new Intent(context, MainActivity.class); + notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); + + handler.post(new Runnable() { + @Override + public void run() { + downloadService.startForeground(Constants.NOTIFICATION_ID_PLAYING, notification); + } + }); + + // Update widget + DSubWidgetProvider.notifyInstances(context, downloadService, true); + } + + private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean playing){ + + // Use the same text for the ticker and the expanded notification + String title = song.getTitle(); + String arist = song.getArtist(); + String album = song.getAlbum(); + + // Set the album art. + try { + int size = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size); + if (bitmap == null) { + // set default album art + rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } else { + rv.setImageViewBitmap(R.id.notification_image, bitmap); + } + } catch (Exception x) { + Log.w(TAG, "Failed to get notification cover art", x); + rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } + + // set the text for the notifications + rv.setTextViewText(R.id.notification_title, title); + 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()); + } + + if(!playing) { + rv.setImageViewResource(R.id.control_pause, R.drawable.notification_play); + rv.setImageViewResource(R.id.control_previous, R.drawable.notification_stop); + } + + // Create actions for media buttons + PendingIntent pendingIntent; + if(playing) { + Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS"); + prevIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); + rv.setOnClickPendingIntent(R.id.control_previous, pendingIntent); + } else { + Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP"); + prevIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_STOP)); + pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0); + rv.setOnClickPendingIntent(R.id.control_previous, pendingIntent); + } + + Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE"); + pauseIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0); + rv.setOnClickPendingIntent(R.id.control_pause, pendingIntent); + + Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT"); + nextIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)); + pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0); + rv.setOnClickPendingIntent(R.id.control_next, pendingIntent); + } + + public static void hidePlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler) { + // Remove notification and remove the service from the foreground + handler.post(new Runnable() { + @Override + public void run() { + downloadService.stopForeground(true); + } + }); + + // Update widget + DSubWidgetProvider.notifyInstances(context, downloadService, false); + } + + public static void sleepQuietly(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException x) { + Log.w(TAG, "Interrupted from sleep.", x); + } + } + + public static void startActivityWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivitiy) { + startActivityWithoutTransition(currentActivity, new Intent(currentActivity, newActivitiy)); + } + + public static void startActivityWithoutTransition(Activity currentActivity, Intent intent) { + currentActivity.startActivity(intent); + disablePendingTransition(currentActivity); + } + + public static void disablePendingTransition(Activity activity) { + + // Activity.overridePendingTransition() was introduced in Android 2.0. Use reflection to maintain + // compatibility with 1.5. + try { + Method method = Activity.class.getMethod("overridePendingTransition", int.class, int.class); + method.invoke(activity, 0, 0); + } catch (Throwable x) { + // Ignored + } + } + + public static Drawable createDrawableFromBitmap(Context context, Bitmap bitmap) { + // BitmapDrawable(Resources, Bitmap) was introduced in Android 1.6. Use reflection to maintain + // compatibility with 1.5. + try { + Constructor<BitmapDrawable> constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class); + return constructor.newInstance(context.getResources(), bitmap); + } catch (Throwable x) { + return new BitmapDrawable(bitmap); + } + } + + public static void registerMediaButtonEventReceiver(Context context) { + + // Only do it if enabled in the settings. + SharedPreferences prefs = getPreferences(context); + boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true); + + if (enabled) { + + // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2. + // Use reflection to maintain compatibility with 1.5. + try { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class); + method.invoke(audioManager, componentName); + } catch (Throwable x) { + // Ignored. + } + } + } + + public static void unregisterMediaButtonEventReceiver(Context context) { + // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2. + // Use reflection to maintain compatibility with 1.5. + try { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class); + method.invoke(audioManager, componentName); + } catch (Throwable x) { + // Ignored. + } + } + + @TargetApi(8) + public static void requestAudioFocus(final Context context) { + if (Build.VERSION.SDK_INT >= 8 && !hasFocus) { + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + hasFocus = true; + audioManager.requestAudioFocus(new OnAudioFocusChangeListener() { + public void onAudioFocusChange(int focusChange) { + DownloadServiceImpl downloadService = (DownloadServiceImpl)context; + if((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !downloadService.isJukeboxEnabled()) { + if(downloadService.getPlayerState() == PlayerState.STARTED) { + SharedPreferences prefs = getPreferences(context); + int lossPref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1")); + if(lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) { + lowerFocus = true; + downloadService.setVolume(0.1f); + } else if(lossPref == 0 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) { + pauseFocus = true; + downloadService.pause(); + } + } + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + if(pauseFocus) { + pauseFocus = false; + downloadService.start(); + } else if(lowerFocus) { + lowerFocus = false; + downloadService.setVolume(1.0f); + } + } else if(focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isJukeboxEnabled()) { + hasFocus = false; + downloadService.pause(); + audioManager.abandonAudioFocus(this); + } + } + }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + } + + /** + * <p>Broadcasts the given song info as the new song being played.</p> + */ + public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { + DownloadService downloadService = (DownloadServiceImpl)context; + Intent intent = new Intent(EVENT_META_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED); + + if (song != null) { + intent.putExtra("title", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); + + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + + avrcpIntent.putExtra("playing", true); + avrcpIntent.putExtra("track", song.getTitle()); + avrcpIntent.putExtra("artist", song.getArtist()); + avrcpIntent.putExtra("album", song.getAlbum()); + avrcpIntent.putExtra("ListSize",(long) downloadService.getSongs().size()); + avrcpIntent.putExtra("id", (long) downloadService.getCurrentPlayingIndex()+1); + avrcpIntent.putExtra("duration", (long) downloadService.getPlayerDuration()); + avrcpIntent.putExtra("position", (long) downloadService.getPlayerPosition()); + avrcpIntent.putExtra("coverart", albumArtFile.getAbsolutePath()); + } else { + intent.putExtra("title", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("coverart", ""); + + avrcpIntent.putExtra("playing", false); + avrcpIntent.putExtra("track", ""); + avrcpIntent.putExtra("artist", ""); + avrcpIntent.putExtra("album", ""); + avrcpIntent.putExtra("ListSize",(long)0); + avrcpIntent.putExtra("id", (long) 0); + avrcpIntent.putExtra("duration", (long )0); + avrcpIntent.putExtra("position", (long) 0); + avrcpIntent.putExtra("coverart", ""); + } + + context.sendBroadcast(intent); + context.sendBroadcast(avrcpIntent); + } + + /** + * <p>Broadcasts the given player state as the one being set.</p> + */ + public static void broadcastPlaybackStatusChange(Context context, PlayerState state) { + Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); + Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED); + + switch (state) { + case STARTED: + intent.putExtra("state", "play"); + avrcpIntent.putExtra("playing", true); + break; + case STOPPED: + intent.putExtra("state", "stop"); + avrcpIntent.putExtra("playing", false); + break; + case PAUSED: + intent.putExtra("state", "pause"); + avrcpIntent.putExtra("playing", false); + break; + case COMPLETED: + intent.putExtra("state", "complete"); + avrcpIntent.putExtra("playing", false); + break; + default: + return; // No need to broadcast. + } + + context.sendBroadcast(intent); + context.sendBroadcast(avrcpIntent); + } + + /** + * 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); + } + } + + public static WifiManager.WifiLock createWifiLock(Context context, String tag) { + WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + int lockType = WifiManager.WIFI_MODE_FULL; + if (Build.VERSION.SDK_INT >= 12) { + lockType = 3; + } + return wm.createWifiLock(lockType, tag); + } +} diff --git a/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java new file mode 100644 index 00000000..c3f3f70c --- /dev/null +++ b/src/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java @@ -0,0 +1,32 @@ +package github.daneren2005.dsub.util.compat; + +import github.daneren2005.dsub.domain.MusicDirectory.Entry; +import android.content.ComponentName; +import android.content.Context; +import android.util.Log; + +public class RemoteControlClientBase extends RemoteControlClientHelper { + + private static final String TAG = RemoteControlClientBase.class.getSimpleName(); + + @Override + public void register(Context context, ComponentName mediaButtonReceiverComponent) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + + @Override + public void unregister(Context context) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + + @Override + public void setPlaybackState(int state) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + + @Override + public void updateMetadata(Context context, Entry currentSong) { + Log.w(TAG, "RemoteControlClient requires Android API level 14 or higher."); + } + +} diff --git a/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java b/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java new file mode 100644 index 00000000..ddaa9f43 --- /dev/null +++ b/src/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java @@ -0,0 +1,27 @@ +package github.daneren2005.dsub.util.compat; + +import github.daneren2005.dsub.domain.MusicDirectory; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; + +public abstract class RemoteControlClientHelper { + + public static RemoteControlClientHelper createInstance() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return new RemoteControlClientBase(); + } else { + return new RemoteControlClientICS(); + } + } + + protected RemoteControlClientHelper() { + // Avoid instantiation + } + + public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent); + public abstract void unregister(final Context context); + public abstract void setPlaybackState(final int state); + public abstract void updateMetadata(final Context context, final MusicDirectory.Entry currentSong); + +} diff --git a/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java new file mode 100644 index 00000000..a8fed63d --- /dev/null +++ b/src/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java @@ -0,0 +1,79 @@ +package github.daneren2005.dsub.util.compat; + +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ImageLoader; +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; +import github.daneren2005.dsub.activity.SubsonicActivity; + +@TargetApi(14) +public class RemoteControlClientICS extends RemoteControlClientHelper { + + private RemoteControlClient mRemoteControl; + private ImageLoader imageLoader; + + public void register(final Context context, final ComponentName mediaButtonReceiverComponent) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + // build the PendingIntent for the remote control client + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(mediaButtonReceiverComponent); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, mediaButtonIntent, 0); + + // create and register the remote control client + mRemoteControl = new RemoteControlClient(mediaPendingIntent); + audioManager.registerRemoteControlClient(mRemoteControl); + + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + + mRemoteControl.setTransportControlFlags( + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_STOP); + + imageLoader = SubsonicActivity.getStaticImageLoader(context); + } + + public void unregister(final Context context) { + if (mRemoteControl != null) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.unregisterRemoteControlClient(mRemoteControl); + } + } + + public void setPlaybackState(final int state) { + mRemoteControl.setPlaybackState(state); + } + + public void updateMetadata(final Context context, final MusicDirectory.Entry currentSong) { + if(imageLoader == null) { + imageLoader = SubsonicActivity.getStaticImageLoader(context); + } + + // Update the remote controls + mRemoteControl.editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist()) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getArtist()) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle()) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ? + 0 : ((currentSong.getDuration() == null) ? 0 : currentSong.getDuration())) + .apply(); + if (currentSong == null || imageLoader == null) { + mRemoteControl.editMetadata(true) + .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null) + .apply(); + } else { + imageLoader.loadImage(context, mRemoteControl, currentSong); + } + } + +} diff --git a/src/github/daneren2005/dsub/view/AlbumListAdapter.java b/src/github/daneren2005/dsub/view/AlbumListAdapter.java new file mode 100644 index 00000000..3ff8350b --- /dev/null +++ b/src/github/daneren2005/dsub/view/AlbumListAdapter.java @@ -0,0 +1,83 @@ +/*
+ 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 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import com.commonsware.cwac.endless.EndlessAdapter;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import java.util.List;
+
+public class AlbumListAdapter extends EndlessAdapter {
+ Context context;
+ ArrayAdapter<MusicDirectory.Entry> adapter;
+ String type;
+ String extra;
+ int size;
+ int offset;
+ List<MusicDirectory.Entry> entries;
+
+ public AlbumListAdapter(Context context, ArrayAdapter<MusicDirectory.Entry> adapter, String type, String extra, int size) {
+ super(adapter);
+ this.context = context;
+ this.adapter = adapter;
+ this.type = type;
+ this.extra = extra;
+ this.size = size;
+ this.offset = size;
+ }
+
+ @Override
+ protected boolean cacheInBackground() throws Exception {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ MusicDirectory result;
+ if("genres".equals(type)) {
+ result = service.getSongsByGenre(extra, size, offset, context, null);
+ } else {
+ result = service.getAlbumList(type, size, offset, context, null);
+ }
+ entries = result.getChildren();
+ if(entries.size() > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void appendCachedData() {
+ for(MusicDirectory.Entry entry: entries) {
+ adapter.add(entry);
+ }
+ offset += entries.size();
+ }
+
+ @Override
+ protected View getPendingView(ViewGroup parent) {
+ View progress = LayoutInflater.from(context).inflate(R.layout.tab_progress, null);
+ progress.setVisibility(View.VISIBLE);
+ return progress;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/AlbumView.java b/src/github/daneren2005/dsub/view/AlbumView.java new file mode 100644 index 00000000..3a7b895e --- /dev/null +++ b/src/github/daneren2005/dsub/view/AlbumView.java @@ -0,0 +1,93 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.ImageLoader; +import github.daneren2005.dsub.util.Util; +import java.io.File; +/** + * Used to display albums in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class AlbumView extends UpdateView { + private static final String TAG = AlbumView.class.getSimpleName(); + + private Context context; + private MusicDirectory.Entry album; + + private TextView titleView; + private TextView artistView; + private View coverArtView; + private ImageButton starButton; + private ImageView moreButton; + + public AlbumView(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true); + + titleView = (TextView) findViewById(R.id.album_title); + artistView = (TextView) findViewById(R.id.album_artist); + coverArtView = findViewById(R.id.album_coverart); + starButton = (ImageButton) findViewById(R.id.album_star); + + moreButton = (ImageView) findViewById(R.id.album_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + public void setAlbum(MusicDirectory.Entry album, ImageLoader imageLoader) { + this.album = album; + + titleView.setText(album.getTitle()); + artistView.setText(album.getArtist()); + artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE); + imageLoader.loadImage(coverArtView, album, false, true); + + starButton.setVisibility(!album.isStarred() ? View.GONE : View.VISIBLE); + starButton.setFocusable(false); + + update(); + } + + @Override + protected void update() { + starButton.setVisibility(!album.isStarred() ? View.GONE : View.VISIBLE); + File file = FileUtil.getAlbumDirectory(context, album); + if(file.exists()) { + moreButton.setImageResource(R.drawable.list_item_more_shaded); + } else { + moreButton.setImageResource(R.drawable.list_item_more); + } + } +} diff --git a/src/github/daneren2005/dsub/view/ArtistAdapter.java b/src/github/daneren2005/dsub/view/ArtistAdapter.java new file mode 100644 index 00000000..7e9bf218 --- /dev/null +++ b/src/github/daneren2005/dsub/view/ArtistAdapter.java @@ -0,0 +1,95 @@ +/* + 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 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import github.daneren2005.dsub.R; +import java.util.List; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.SectionIndexer; +import github.daneren2005.dsub.domain.Artist; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * @author Sindre Mehus + */ +public class ArtistAdapter extends ArrayAdapter<Artist> implements SectionIndexer { + + private final Context activity; + + // Both arrays are indexed by section ID. + private final Object[] sections; + private final Integer[] positions; + + public ArtistAdapter(Context activity, List<Artist> artists) { + super(activity, R.layout.artist_list_item, artists); + this.activity = activity; + + Set<String> sectionSet = new LinkedHashSet<String>(30); + List<Integer> positionList = new ArrayList<Integer>(30); + for (int i = 0; i < artists.size(); i++) { + Artist artist = artists.get(i); + String index = artist.getIndex(); + if (!sectionSet.contains(index)) { + sectionSet.add(index); + positionList.add(i); + } + } + sections = sectionSet.toArray(new Object[sectionSet.size()]); + positions = positionList.toArray(new Integer[positionList.size()]); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Artist entry = getItem(position); + ArtistView view; + if (convertView != null && convertView instanceof ArtistView) { + view = (ArtistView) convertView; + } else { + view = new ArtistView(activity); + } + view.setArtist(entry); + return view; + } + + @Override + public Object[] getSections() { + return sections; + } + + @Override + public int getPositionForSection(int section) { + section = Math.min(section, positions.length - 1); + return positions[section]; + } + + @Override + public int getSectionForPosition(int pos) { + for (int i = 0; i < sections.length - 1; i++) { + if (pos < positions[i + 1]) { + return i; + } + } + return sections.length - 1; + } +} diff --git a/src/github/daneren2005/dsub/view/ArtistEntryView.java b/src/github/daneren2005/dsub/view/ArtistEntryView.java new file mode 100644 index 00000000..3b6a50e4 --- /dev/null +++ b/src/github/daneren2005/dsub/view/ArtistEntryView.java @@ -0,0 +1,83 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.ImageLoader;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistEntryView extends UpdateView {
+ private static final String TAG = AlbumView.class.getSimpleName();
+
+ private Context context;
+ private MusicDirectory.Entry artist;
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public ArtistEntryView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setArtist(MusicDirectory.Entry artist) {
+ this.artist = artist;
+
+ titleView.setText(artist.getTitle());
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getArtistDirectory(context, artist);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ArtistView.java b/src/github/daneren2005/dsub/view/ArtistView.java new file mode 100644 index 00000000..353be618 --- /dev/null +++ b/src/github/daneren2005/dsub/view/ArtistView.java @@ -0,0 +1,84 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistView extends UpdateView {
+ private static final String TAG = ArtistView.class.getSimpleName();
+
+ private Context context;
+ private Artist artist;
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public ArtistView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setArtist(Artist artist) {
+ this.artist = artist;
+
+ titleView.setText(artist.getName());
+
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ starButton.setFocusable(false);
+
+ update();
+ }
+
+ @Override
+ protected void update() {
+ starButton.setVisibility((Util.isOffline(getContext()) || !artist.isStarred()) ? View.GONE : View.VISIBLE);
+ File file = FileUtil.getArtistDirectory(context, artist);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/AutoRepeatButton.java b/src/github/daneren2005/dsub/view/AutoRepeatButton.java new file mode 100644 index 00000000..798c1649 --- /dev/null +++ b/src/github/daneren2005/dsub/view/AutoRepeatButton.java @@ -0,0 +1,86 @@ +package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+
+public class AutoRepeatButton extends ImageButton {
+
+ private long initialRepeatDelay = 1000;
+ private long repeatIntervalInMilliseconds = 300;
+ private boolean doClick = true;
+ private Runnable repeatEvent = null;
+
+ private Runnable repeatClickWhileButtonHeldRunnable = new Runnable() {
+ @Override
+ public void run() {
+ doClick = false;
+ //Perform the present repetition of the click action provided by the user
+ // in setOnClickListener().
+ if(repeatEvent != null)
+ repeatEvent.run();
+
+ //Schedule the next repetitions of the click action, using a faster repeat
+ // interval than the initial repeat delay interval.
+ postDelayed(repeatClickWhileButtonHeldRunnable, repeatIntervalInMilliseconds);
+ }
+ };
+
+ private void commonConstructorCode() {
+ this.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int action = event.getAction();
+ if(action == MotionEvent.ACTION_DOWN)
+ {
+ doClick = true;
+ //Just to be sure that we removed all callbacks,
+ // which should have occurred in the ACTION_UP
+ removeCallbacks(repeatClickWhileButtonHeldRunnable);
+
+ //Schedule the start of repetitions after a one half second delay.
+ postDelayed(repeatClickWhileButtonHeldRunnable, initialRepeatDelay);
+
+ setPressed(true);
+ }
+ else if(action == MotionEvent.ACTION_UP) {
+ //Cancel any repetition in progress.
+ removeCallbacks(repeatClickWhileButtonHeldRunnable);
+
+ if(doClick || repeatEvent == null) {
+ performClick();
+ }
+
+ setPressed(false);
+ }
+
+ //Returning true here prevents performClick() from getting called
+ // in the usual manner, which would be redundant, given that we are
+ // already calling it above.
+ return true;
+ }
+ });
+ }
+
+ public void setOnRepeatListener(Runnable runnable) {
+ repeatEvent = runnable;
+ }
+
+ public AutoRepeatButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ commonConstructorCode();
+ }
+
+
+ public AutoRepeatButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ commonConstructorCode();
+ }
+
+ public AutoRepeatButton(Context context) {
+ super(context);
+ commonConstructorCode();
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/ChangeLog.java b/src/github/daneren2005/dsub/view/ChangeLog.java new file mode 100644 index 00000000..b847733e --- /dev/null +++ b/src/github/daneren2005/dsub/view/ChangeLog.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2012 Christian Ketterer (cketti) + * + * Portions Copyright (C) 2012 Martin van Zuilekom (http://martin.cubeactive.com) + * + * 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. + * + * + * Based on android-change-log: + * + * Copyright (C) 2011, Karsten Priegnitz + * + * Permission to use, copy, modify, and distribute this piece of software + * for any purpose with or without fee is hereby granted, provided that + * the above copyright notice and this permission notice appear in the + * source code of all copies. + * + * It would be appreciated if you mention the author in your change log, + * contributors list or the like. + * + * http://code.google.com/p/android-change-log/ + */ +package github.daneren2005.dsub.view; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.SparseArray; +import android.webkit.WebView; +import github.daneren2005.dsub.R; + + +/** + * Display a dialog showing a full or partial (What's New) change log. + */ +public class ChangeLog { + /** + * Tag that is used when sending error/debug messages to the log. + */ + protected static final String LOG_TAG = "ckChangeLog"; + + /** + * This is the key used when storing the version code in SharedPreferences. + */ + protected static final String VERSION_KEY = "ckChangeLog_last_version_code"; + + /** + * Constant that used when no version code is available. + */ + protected static final int NO_VERSION = -1; + + /** + * Default CSS styles used to format the change log. + */ + private static final String DEFAULT_CSS = + "div.title { margin-left: 0px; font-size: 1.2em; text-align: center;}" + + "div.subtitle {margin-left: 0px; font-size: .8em; text-align: center;}" + + "li { margin-left: 0px;}" + + "ul { padding-left: 2em;}"; + + + /** + * Context that is used to access the resources and to create the ChangeLog dialogs. + */ + protected final Context mContext; + + /** + * Contains the CSS rules used to format the change log. + */ + protected final String mCss; + + /** + * Last version code read from {@code SharedPreferences} or {@link #NO_VERSION}. + */ + private int mLastVersionCode; + + /** + * Version code of the current installation. + */ + private int mCurrentVersionCode; + + /** + * Version name of the current installation. + */ + private String mCurrentVersionName; + + + /** + * Contains constants for the root element of {@code changelog.xml}. + */ + protected interface ChangeLogTag { + static final String NAME = "changelog"; + } + + /** + * Contains constants for the release element of {@code changelog.xml}. + */ + protected interface ReleaseTag { + static final String NAME = "release"; + static final String ATTRIBUTE_VERSION = "version"; + static final String ATTRIBUTE_VERSION_CODE = "versioncode"; + static final String ATTRIBUTE_RELEASE_DATE = "releasedate"; + } + + /** + * Contains constants for the change element of {@code changelog.xml}. + */ + protected interface ChangeTag { + static final String NAME = "change"; + } + + /** + * Create a {@code ChangeLog} instance using the default {@link SharedPreferences} file. + * + * @param context + * Context that is used to access the resources and to create the ChangeLog dialogs. + */ + public ChangeLog(Context context) { + this(context, PreferenceManager.getDefaultSharedPreferences(context), DEFAULT_CSS); + } + + /** + * Create a {@code ChangeLog} instance using the default {@link SharedPreferences} file. + * + * @param context + * Context that is used to access the resources and to create the ChangeLog dialogs. + * @param css + * CSS styles that will be used to format the change log. + */ + public ChangeLog(Context context, String css) { + this(context, PreferenceManager.getDefaultSharedPreferences(context), css); + } + + public ChangeLog(Context context, SharedPreferences preferences) { + this(context, preferences, DEFAULT_CSS); + } + + /** + * Create a {@code ChangeLog} instance using the supplied {@code SharedPreferences} instance. + * + * @param context + * Context that is used to access the resources and to create the ChangeLog dialogs. + * @param preferences + * {@code SharedPreferences} instance that is used to persist the last version code. + * @param css + * CSS styles used to format the change log (excluding {@code <style>} and + * {@code </style>}). + * + */ + public ChangeLog(Context context, SharedPreferences preferences, String css) { + mContext = context; + mCss = css; + + // Get last version code + mLastVersionCode = preferences.getInt(VERSION_KEY, NO_VERSION); + + // Get current version code and version name + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0); + + mCurrentVersionCode = packageInfo.versionCode; + mCurrentVersionName = packageInfo.versionName; + } catch (NameNotFoundException e) { + mCurrentVersionCode = NO_VERSION; + Log.e(LOG_TAG, "Could not get version information from manifest!", e); + } + } + + /** + * Get version code of last installation. + * + * @return The version code of the last installation of this app (as described in the former + * manifest). This will be the same as returned by {@link #getCurrentVersionCode()} the + * second time this version of the app is launched (more precisely: the second time + * {@code ChangeLog} is instantiated). + * + * @see AndroidManifest.xml#android:versionCode + */ + public int getLastVersionCode() { + return mLastVersionCode; + } + + /** + * Get version code of current installation. + * + * @return The version code of this app as described in the manifest. + * + * @see AndroidManifest.xml#android:versionCode + */ + public int getCurrentVersionCode() { + return mCurrentVersionCode; + } + + /** + * Get version name of current installation. + * + * @return The version name of this app as described in the manifest. + * + * @see AndroidManifest.xml#android:versionName + */ + public String getCurrentVersionName() { + return mCurrentVersionName; + } + + /** + * Check if this is the first execution of this app version. + * + * @return {@code true} if this version of your app is started the first time. + */ + public boolean isFirstRun() { + return mLastVersionCode < mCurrentVersionCode; + } + + /** + * Check if this is a new installation. + * + * @return {@code true} if your app including {@code ChangeLog} is started the first time ever. + * Also {@code true} if your app was uninstalled and installed again. + */ + public boolean isFirstRunEver() { + return mLastVersionCode == NO_VERSION; + } + + /** + * Get the "What's New" dialog. + * + * @return An AlertDialog displaying the changes since the previous installed version of your + * app (What's New). But when this is the first run of your app including + * {@code ChangeLog} then the full log dialog is show. + */ + public AlertDialog getLogDialog() { + return getDialog(isFirstRunEver()); + } + + /** + * Get a dialog with the full change log. + * + * @return An AlertDialog with a full change log displayed. + */ + public AlertDialog getFullLogDialog() { + return getDialog(true); + } + + /** + * Create a dialog containing (parts of the) change log. + * + * @param full + * If this is {@code true} the full change log is displayed. Otherwise only changes for + * versions newer than the last version are displayed. + * + * @return A dialog containing the (partial) change log. + */ + protected AlertDialog getDialog(boolean full) { + WebView wv = new WebView(mContext); + //wv.setBackgroundColor(0); // transparent + wv.loadDataWithBaseURL(null, getLog(full), "text/html", "UTF-8", null); + + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle( + mContext.getResources().getString( + full ? R.string.changelog_full_title : R.string.changelog_title)) + .setView(wv) + .setCancelable(false) + // OK button + .setPositiveButton( + mContext.getResources().getString(R.string.changelog_ok_button), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // The user clicked "OK" so save the current version code as + // "last version code". + updateVersionInPreferences(); + } + }); + + if (!full) { + // Show "More…" button if we're only displaying a partial change log. + builder.setNegativeButton(R.string.changelog_show_full, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + getFullLogDialog().show(); + } + }); + } + + return builder.create(); + } + + /** + * Write current version code to the preferences. + */ + protected void updateVersionInPreferences() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + SharedPreferences.Editor editor = sp.edit(); + editor.putInt(VERSION_KEY, mCurrentVersionCode); + + // TODO: Update preferences from a background thread + editor.commit(); + } + + /** + * Get changes since last version as HTML string. + * + * @return HTML string containing the changes since the previous installed version of your app + * (What's New). + */ + public String getLog() { + return getLog(false); + } + + /** + * Get full change log as HTML string. + * + * @return HTML string containing the full change log. + */ + public String getFullLog() { + return getLog(true); + } + + /** + * Get (partial) change log as HTML string. + * + * @param full + * If this is {@code true} the full change log is returned. Otherwise only changes for + * versions newer than the last version are returned. + * + * @return The (partial) change log. + */ + private String getLog(boolean full) { + StringBuilder sb = new StringBuilder(); + + sb.append("<html><head><style type=\"text/css\">"); + sb.append(mCss); + sb.append("</style></head><body>"); + + Resources resources = mContext.getResources(); + + // Read master change log from raw/changelog.xml + SparseArray<ReleaseItem> defaultChangelog; + try { + XmlPullParser xml = XmlPullParserFactory.newInstance().newPullParser(); + InputStreamReader reader = new InputStreamReader(resources.openRawResource(R.raw.changelog)); + xml.setInput(reader); + try { + defaultChangelog = readChangeLog(xml, full); + } finally { + try { reader.close(); } catch (Exception e) { /* do nothing */ } + } + } catch (XmlPullParserException e) { + Log.e(LOG_TAG, "Error reading raw/changelog.xml", e); + return null; + } + + // Read localized change log from xml[-lang]/changelog.xml + XmlResourceParser resXml = mContext.getResources().getXml(R.xml.changelog); + SparseArray<ReleaseItem> changelog; + try { + changelog = readChangeLog(resXml, full); + } finally { + resXml.close(); + } + + String versionFormat = resources.getString(R.string.changelog_version_format); + + // Get all version codes from the master change log... + List<Integer> versions = new ArrayList<Integer>(defaultChangelog.size()); + for (int i = 0, len = defaultChangelog.size(); i < len; i++) { + int key = defaultChangelog.keyAt(i); + versions.add(key); + } + + // ... and sort them (newest version first). + Collections.sort(versions, Collections.reverseOrder()); + + for (Integer version : versions) { + int key = version.intValue(); + + // Use release information from localized change log and fall back to the master file + // if necessary. + ReleaseItem release = changelog.get(key, defaultChangelog.get(key)); + + sb.append("<div class='title'>"); + sb.append(String.format(versionFormat, release.versionName)); + sb.append("</div>"); + if(release.releaseDate != null) { + sb.append("<div class='subtitle'>"); + sb.append(release.releaseDate); + sb.append("</div>"); + } + sb.append("<ul>"); + for (String change : release.changes) { + sb.append("<li>"); + sb.append(change); + sb.append("</li>"); + } + sb.append("</ul>"); + } + + sb.append("</body></html>"); + + return sb.toString(); + } + + /** + * Read the change log from an XML file. + * + * @param xml + * The {@code XmlPullParser} instance used to read the change log. + * @param full + * If {@code true} the full change log is read. Otherwise only the changes since the + * last (saved) version are read. + * + * @return A {@code SparseArray} mapping the version codes to release information. + */ + protected SparseArray<ReleaseItem> readChangeLog(XmlPullParser xml, boolean full) { + SparseArray<ReleaseItem> result = new SparseArray<ReleaseItem>(); + + try { + int eventType = xml.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG && xml.getName().equals(ReleaseTag.NAME)) { + if (parseReleaseTag(xml, full, result)) { + // Stop reading more elements if this entry is not newer than the last + // version. + break; + } + } + eventType = xml.next(); + } + } catch (XmlPullParserException e) { + Log.e(LOG_TAG, e.getMessage(), e); + } catch (IOException e) { + Log.e(LOG_TAG, e.getMessage(), e); + } + + return result; + } + + /** + * Parse the {@code release} tag of a change log XML file. + * + * @param xml + * The {@code XmlPullParser} instance used to read the change log. + * @param full + * If {@code true} the contents of the {@code release} tag are always added to + * {@code changelog}. Otherwise only if the item's {@code versioncode} attribute is + * higher than the last version code. + * @param changelog + * The {@code SparseArray} to add a new {@link ReleaseItem} instance to. + * + * @return {@code true} if the {@code release} element is describing changes of a version older + * or equal to the last version. In that case {@code changelog} won't be modified and + * {@link #readChangeLog(XmlPullParser, boolean)} will stop reading more elements from + * the change log file. + * + * @throws XmlPullParserException + * @throws IOException + */ + private boolean parseReleaseTag(XmlPullParser xml, boolean full, + SparseArray<ReleaseItem> changelog) throws XmlPullParserException, IOException { + + String version = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_VERSION); + + int versionCode; + try { + String versionCodeStr = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_VERSION_CODE); + versionCode = Integer.parseInt(versionCodeStr); + } catch (NumberFormatException e) { + versionCode = NO_VERSION; + } + + String releaseDate = xml.getAttributeValue(null, ReleaseTag.ATTRIBUTE_RELEASE_DATE); + + if (!full && versionCode <= mLastVersionCode) { + return true; + } + + int eventType = xml.getEventType(); + List<String> changes = new ArrayList<String>(); + while (eventType != XmlPullParser.END_TAG || xml.getName().equals(ChangeTag.NAME)) { + if (eventType == XmlPullParser.START_TAG && xml.getName().equals(ChangeTag.NAME)) { + eventType = xml.next(); + + changes.add(xml.getText()); + } + eventType = xml.next(); + } + + ReleaseItem release = new ReleaseItem(versionCode, version, releaseDate, changes); + changelog.put(versionCode, release); + + return false; + } + + /** + * Container used to store information about a release/version. + */ + protected static class ReleaseItem { + /** + * Version code of the release. + */ + public final int versionCode; + + /** + * Version name of the release. + */ + public final String versionName; + + public final String releaseDate; + + /** + * List of changes introduced with that release. + */ + public final List<String> changes; + + ReleaseItem(int versionCode, String versionName, String releaseDate, List<String> changes) { + this.versionCode = versionCode; + this.versionName = versionName; + this.releaseDate = releaseDate; + this.changes = changes; + } + } +} diff --git a/src/github/daneren2005/dsub/view/ChatAdapter.java b/src/github/daneren2005/dsub/view/ChatAdapter.java new file mode 100644 index 00000000..518f81ef --- /dev/null +++ b/src/github/daneren2005/dsub/view/ChatAdapter.java @@ -0,0 +1,100 @@ +package github.daneren2005.dsub.view;
+
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.domain.ChatMessage;
+import github.daneren2005.dsub.util.Util;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class ChatAdapter extends ArrayAdapter<ChatMessage> {
+
+ private final SubsonicActivity activity;
+ private ArrayList<ChatMessage> messages;
+
+ private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})"; //you can just place your support phone here
+ private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
+
+ public ChatAdapter(SubsonicActivity activity, ArrayList<ChatMessage> messages) {
+ super(activity, R.layout.chat_item, messages);
+ this.activity = activity;
+ this.messages = messages;
+ }
+
+ @Override
+ public int getCount() {
+ return messages.size();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ChatMessage message = this.getItem(position);
+
+ ViewHolder holder;
+ int layout;
+
+ String messageUser = message.getUsername();
+ Date messageTime = new java.util.Date(message.getTime());
+ String messageText = message.getMessage();
+
+ String me = Util.getUserName(activity, Util.getActiveServer(activity));
+
+ if (messageUser.equals(me)) {
+ layout = R.layout.chat_item_reverse;
+ } else {
+ layout = R.layout.chat_item;
+ }
+
+ if (convertView == null)
+ {
+ holder = new ViewHolder();
+
+ convertView = LayoutInflater.from(activity).inflate(layout, parent, false);
+
+ TextView usernameView = (TextView) convertView.findViewById(R.id.chat_username);
+ TextView timeView = (TextView) convertView.findViewById(R.id.chat_time);
+ TextView messageView = (TextView) convertView.findViewById(R.id.chat_message);
+
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ Linkify.addLinks(messageView, Linkify.EMAIL_ADDRESSES);
+ Linkify.addLinks(messageView, Linkify.WEB_URLS);
+ Linkify.addLinks(messageView, phoneMatcher, "tel:");
+
+ holder.message = messageView;
+ holder.username = usernameView;
+ holder.time = timeView;
+
+ convertView.setTag(holder);
+ }
+ else
+ {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
+ String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
+
+ holder.username.setText(messageUser);
+ holder.message.setText(messageText);
+ holder.time.setText(messageTimeFormatted);
+
+ return convertView;
+ }
+
+ private static class ViewHolder
+ {
+ TextView message;
+ TextView username;
+ TextView time;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/EntryAdapter.java b/src/github/daneren2005/dsub/view/EntryAdapter.java new file mode 100644 index 00000000..ff7393c6 --- /dev/null +++ b/src/github/daneren2005/dsub/view/EntryAdapter.java @@ -0,0 +1,79 @@ +/* + 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 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.util.Log; +import java.util.List; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.util.ImageLoader; + +/** + * @author Sindre Mehus + */ +public class EntryAdapter extends ArrayAdapter<MusicDirectory.Entry> { + private final static String TAG = EntryAdapter.class.getSimpleName(); + private final Context activity; + private final ImageLoader imageLoader; + private final boolean checkable; + private List<MusicDirectory.Entry> entries; + + public EntryAdapter(Context activity, ImageLoader imageLoader, List<MusicDirectory.Entry> entries, boolean checkable) { + super(activity, android.R.layout.simple_list_item_1, entries); + this.entries = entries; + this.activity = activity; + this.imageLoader = imageLoader; + this.checkable = checkable; + } + + public void removeAt(int position) { + entries.remove(position); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + MusicDirectory.Entry entry = getItem(position); + + if (entry.isDirectory()) { + if(entry.getArtist() != null || entry.getParent() != null) { + AlbumView view; + view = new AlbumView(activity); + view.setAlbum(entry, imageLoader); + return view; + } else { + ArtistEntryView view = new ArtistEntryView(activity); + view.setArtist(entry); + return view; + } + } else { + SongView view; + if (convertView != null && convertView instanceof SongView) { + view = (SongView) convertView; + } else { + view = new SongView(activity); + } + view.setSong(entry, checkable); + return view; + } + } +} diff --git a/src/github/daneren2005/dsub/view/ErrorDialog.java b/src/github/daneren2005/dsub/view/ErrorDialog.java new file mode 100644 index 00000000..e9f25a2d --- /dev/null +++ b/src/github/daneren2005/dsub/view/ErrorDialog.java @@ -0,0 +1,70 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import github.daneren2005.dsub.activity.MainActivity; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.util.Util; + +/** + * @author Sindre Mehus + */ +public class ErrorDialog { + + public ErrorDialog(Activity activity, int messageId, boolean finishActivityOnCancel) { + this(activity, activity.getResources().getString(messageId), finishActivityOnCancel); + } + + public ErrorDialog(final Activity activity, String message, final boolean finishActivityOnClose) { + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setTitle(R.string.error_label); + builder.setMessage(message); + builder.setCancelable(true); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + if (finishActivityOnClose) { + restart(activity); + } + } + }); + builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (finishActivityOnClose) { + restart(activity); + } + } + }); + + builder.create().show(); + } + + private void restart(Activity context) { + Intent intent = new Intent(context, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + Util.startActivityWithoutTransition(context, intent); + } +} diff --git a/src/github/daneren2005/dsub/view/FadeOutAnimation.java b/src/github/daneren2005/dsub/view/FadeOutAnimation.java new file mode 100644 index 00000000..292529e6 --- /dev/null +++ b/src/github/daneren2005/dsub/view/FadeOutAnimation.java @@ -0,0 +1,77 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+
+/**
+ * Fades a view out by changing its alpha value.
+ *
+ * @author Sindre Mehus
+ * @version $Id: Util.java 3203 2012-10-04 09:12:08Z sindre_mehus $
+ */
+public class FadeOutAnimation extends AlphaAnimation {
+
+ private boolean cancelled;
+
+ /**
+ * Creates and starts the fade out animation.
+ *
+ * @param view The view to fade out (or display).
+ * @param fadeOut If true, the view is faded out. Otherwise it is immediately made visible.
+ * @param durationMillis Fade duration.
+ */
+ public static void createAndStart(View view, boolean fadeOut, long durationMillis) {
+ if (fadeOut) {
+ view.clearAnimation();
+ view.startAnimation(new FadeOutAnimation(view, durationMillis));
+ } else {
+ Animation animation = view.getAnimation();
+ if (animation instanceof FadeOutAnimation) {
+ ((FadeOutAnimation) animation).cancelFadeOut();
+ }
+ view.clearAnimation();
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ FadeOutAnimation(final View view, long durationMillis) {
+ super(1.0F, 0.0F);
+ setDuration(durationMillis);
+ setAnimationListener(new AnimationListener() {
+ public void onAnimationStart(Animation animation) {
+ }
+
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ public void onAnimationEnd(Animation animation) {
+ if (!cancelled) {
+ view.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+ }
+
+ private void cancelFadeOut() {
+ cancelled = true;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/GenreAdapter.java b/src/github/daneren2005/dsub/view/GenreAdapter.java new file mode 100644 index 00000000..b98efd20 --- /dev/null +++ b/src/github/daneren2005/dsub/view/GenreAdapter.java @@ -0,0 +1,59 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class GenreAdapter extends ArrayAdapter<Genre>{
+ private Context activity;
+ private List<Genre> genres;
+
+ public GenreAdapter(Context context, List<Genre> genres) {
+ super(context, android.R.layout.simple_list_item_1, genres);
+ this.activity = context;
+ this.genres = genres;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Genre genre = genres.get(position);
+ GenreView view;
+ if (convertView != null && convertView instanceof GenreView) {
+ view = (GenreView) convertView;
+ } else {
+ view = new GenreView(activity);
+ }
+ view.setGenre(genre);
+ return view;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/GenreView.java b/src/github/daneren2005/dsub/view/GenreView.java new file mode 100644 index 00000000..dbb0248b --- /dev/null +++ b/src/github/daneren2005/dsub/view/GenreView.java @@ -0,0 +1,53 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Genre;
+
+public class GenreView extends UpdateView {
+ private static final String TAG = GenreView.class.getSimpleName();
+
+ private TextView titleView;
+ private ImageButton starButton;
+ private ImageView moreButton;
+
+ public GenreView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ starButton = (ImageButton) findViewById(R.id.artist_star);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setClickable(false);
+ }
+
+ public void setGenre(Genre genre) {
+ titleView.setText(genre.getName());
+
+ starButton.setVisibility(View.GONE);
+ starButton.setFocusable(false);
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/MergeAdapter.java b/src/github/daneren2005/dsub/view/MergeAdapter.java new file mode 100644 index 00000000..bfe777ea --- /dev/null +++ b/src/github/daneren2005/dsub/view/MergeAdapter.java @@ -0,0 +1,292 @@ +/*** + Copyright (c) 2008-2009 CommonsWare, LLC + Portions (c) 2009 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package github.daneren2005.dsub.view; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; + +import github.daneren2005.dsub.view.SackOfViewsAdapter; + +/** + * Adapter that merges multiple child adapters and views + * into a single contiguous whole. + * <p/> + * Adapters used as pieces within MergeAdapter must + * have view type IDs monotonically increasing from 0. Ideally, + * adapters also have distinct ranges for their row ids, as + * returned by getItemId(). + */ +public class MergeAdapter extends BaseAdapter { + + private final CascadeDataSetObserver observer = new CascadeDataSetObserver(); + private final ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>(); + + /** + * Stock constructor, simply chaining to the superclass. + */ + public MergeAdapter() { + super(); + } + + /** + * Adds a new adapter to the roster of things to appear + * in the aggregate list. + * + * @param adapter Source for row views for this section + */ + public void addAdapter(ListAdapter adapter) { + pieces.add(adapter); + adapter.registerDataSetObserver(observer); + } + + public void removeAdapter(ListAdapter adapter) { + adapter.unregisterDataSetObserver(observer); + pieces.remove(adapter); + } + + /** + * Adds a new View to the roster of things to appear + * in the aggregate list. + * + * @param view Single view to add + */ + public ListAdapter addView(View view) { + return addView(view, false); + } + + /** + * Adds a new View to the roster of things to appear + * in the aggregate list. + * + * @param view Single view to add + * @param enabled false if views are disabled, true if enabled + */ + public ListAdapter addView(View view, boolean enabled) { + return addViews(Arrays.asList(view), enabled); + } + + /** + * Adds a list of views to the roster of things to appear + * in the aggregate list. + * + * @param views List of views to add + */ + public ListAdapter addViews(List<View> views) { + return addViews(views, false); + } + + /** + * Adds a list of views to the roster of things to appear + * in the aggregate list. + * + * @param views List of views to add + * @param enabled false if views are disabled, true if enabled + */ + public ListAdapter addViews(List<View> views, boolean enabled) { + ListAdapter adapter = enabled ? new EnabledSackAdapter(views) : new SackOfViewsAdapter(views); + addAdapter(adapter); + return adapter; + } + + /** + * Get the data item associated with the specified + * position in the data set. + * + * @param position Position of the item whose data we want + */ + @Override + public Object getItem(int position) { + for (ListAdapter piece : pieces) { + int size = piece.getCount(); + + if (position < size) { + return (piece.getItem(position)); + } + + position -= size; + } + + return (null); + } + + /** + * How many items are in the data set represented by this + * Adapter. + */ + @Override + public int getCount() { + int total = 0; + + for (ListAdapter piece : pieces) { + total += piece.getCount(); + } + + return (total); + } + + /** + * Returns the number of types of Views that will be + * created by getView(). + */ + @Override + public int getViewTypeCount() { + int total = 0; + + for (ListAdapter piece : pieces) { + total += piece.getViewTypeCount(); + } + + return (Math.max(total, 1)); // needed for setListAdapter() before content add' + } + + /** + * Get the type of View that will be created by getView() + * for the specified item. + * + * @param position Position of the item whose data we want + */ + @Override + public int getItemViewType(int position) { + int typeOffset = 0; + int result = -1; + + for (ListAdapter piece : pieces) { + int size = piece.getCount(); + + if (position < size) { + result = typeOffset + piece.getItemViewType(position); + break; + } + + position -= size; + typeOffset += piece.getViewTypeCount(); + } + + return (result); + } + + /** + * Are all items in this ListAdapter enabled? If yes it + * means all items are selectable and clickable. + */ + @Override + public boolean areAllItemsEnabled() { + return (false); + } + + /** + * Returns true if the item at the specified position is + * not a separator. + * + * @param position Position of the item whose data we want + */ + @Override + public boolean isEnabled(int position) { + for (ListAdapter piece : pieces) { + int size = piece.getCount(); + + if (position < size) { + return (piece.isEnabled(position)); + } + + position -= size; + } + + return (false); + } + + /** + * Get a View that displays the data at the specified + * position in the data set. + * + * @param position Position of the item whose data we want + * @param convertView View to recycle, if not null + * @param parent ViewGroup containing the returned View + */ + @Override + public View getView(int position, View convertView, + ViewGroup parent) { + for (ListAdapter piece : pieces) { + int size = piece.getCount(); + + if (position < size) { + + return (piece.getView(position, convertView, parent)); + } + + position -= size; + } + + return (null); + } + + /** + * Get the row id associated with the specified position + * in the list. + * + * @param position Position of the item whose data we want + */ + @Override + public long getItemId(int position) { + for (ListAdapter piece : pieces) { + int size = piece.getCount(); + + if (position < size) { + return (piece.getItemId(position)); + } + + position -= size; + } + + return (-1); + } + + private static class EnabledSackAdapter extends SackOfViewsAdapter { + public EnabledSackAdapter(List<View> views) { + super(views); + } + + @Override + public boolean areAllItemsEnabled() { + return (true); + } + + @Override + public boolean isEnabled(int position) { + return (true); + } + } + + private class CascadeDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + notifyDataSetInvalidated(); + } + } +} + diff --git a/src/github/daneren2005/dsub/view/MyViewFlipper.java b/src/github/daneren2005/dsub/view/MyViewFlipper.java new file mode 100644 index 00000000..26a3de08 --- /dev/null +++ b/src/github/daneren2005/dsub/view/MyViewFlipper.java @@ -0,0 +1,53 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ViewFlipper; + +/** + * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191) + * + * @author Sindre Mehus + * @version $Id$ + */ +public class MyViewFlipper extends ViewFlipper { + + public MyViewFlipper(Context context) { + super(context); + } + + public MyViewFlipper(Context context, AttributeSet attrs) { + super(context, attrs); + } + + + @Override + protected void onDetachedFromWindow() { + try { + super.onDetachedFromWindow(); + } + catch (IllegalArgumentException e) { + // Call stopFlipping() in order to kick off updateRunning() + stopFlipping(); + } + } +} + diff --git a/src/github/daneren2005/dsub/view/PlaylistAdapter.java b/src/github/daneren2005/dsub/view/PlaylistAdapter.java new file mode 100644 index 00000000..71727c04 --- /dev/null +++ b/src/github/daneren2005/dsub/view/PlaylistAdapter.java @@ -0,0 +1,68 @@ +/* + 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 2010 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import github.daneren2005.dsub.R; +import java.util.List; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import github.daneren2005.dsub.domain.Playlist; +import java.util.Collections; +import java.util.Comparator; + +/** + * @author Sindre Mehus + */ +public class PlaylistAdapter extends ArrayAdapter<Playlist> { + + private final Context activity; + + public PlaylistAdapter(Context activity, List<Playlist> Playlists) { + super(activity, R.layout.playlist_list_item, Playlists); + this.activity = activity; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Playlist entry = getItem(position); + PlaylistView view; + if (convertView != null && convertView instanceof PlaylistView) { + view = (PlaylistView) convertView; + } else { + view = new PlaylistView(activity); + } + view.setPlaylist(entry); + return view; + } + + public static class PlaylistComparator implements Comparator<Playlist> { + @Override + public int compare(Playlist playlist1, Playlist playlist2) { + return playlist1.getName().compareToIgnoreCase(playlist2.getName()); + } + + public static List<Playlist> sort(List<Playlist> playlists) { + Collections.sort(playlists, new PlaylistComparator()); + return playlists; + } + + } +} diff --git a/src/github/daneren2005/dsub/view/PlaylistView.java b/src/github/daneren2005/dsub/view/PlaylistView.java new file mode 100644 index 00000000..876e0691 --- /dev/null +++ b/src/github/daneren2005/dsub/view/PlaylistView.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.util.FileUtil;
+import github.daneren2005.dsub.util.Util;
+import java.io.File;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistView extends UpdateView {
+ private static final String TAG = PlaylistView.class.getSimpleName();
+
+ private Context context;
+ private Playlist playlist;
+
+ private TextView titleView;
+ private ImageView moreButton;
+
+ public PlaylistView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.playlist_name);
+ moreButton = (ImageView) findViewById(R.id.playlist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setPlaylist(Playlist playlist) {
+ this.playlist = playlist;
+
+ titleView.setText(playlist.getName());
+ update();
+ }
+
+ @Override
+ protected void update() {
+ File file = FileUtil.getPlaylistFile(Util.getServerName(context), playlist.getName());
+ if(file.exists() || Util.isOffline(context)) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java b/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java new file mode 100644 index 00000000..6b7af991 --- /dev/null +++ b/src/github/daneren2005/dsub/view/PodcastChannelAdapter.java @@ -0,0 +1,59 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class PodcastChannelAdapter extends ArrayAdapter<PodcastChannel>{
+ private Context activity;
+ private List<PodcastChannel> podcasts;
+
+ public PodcastChannelAdapter(Context context, List<PodcastChannel> podcasts) {
+ super(context, android.R.layout.simple_list_item_1, podcasts);
+ this.activity = context;
+ this.podcasts = podcasts;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ PodcastChannel podcast = podcasts.get(position);
+ PodcastChannelView view;
+ if (convertView != null && convertView instanceof PodcastChannelView) {
+ view = (PodcastChannelView) convertView;
+ } else {
+ view = new PodcastChannelView(activity);
+ }
+ view.setPodcastChannel(podcast);
+ return view;
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/PodcastChannelView.java b/src/github/daneren2005/dsub/view/PodcastChannelView.java new file mode 100644 index 00000000..94eedf2c --- /dev/null +++ b/src/github/daneren2005/dsub/view/PodcastChannelView.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 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.util.FileUtil;
+import java.io.File;
+
+public class PodcastChannelView extends UpdateView {
+ private static final String TAG = PodcastChannelView.class.getSimpleName();
+
+ private Context context;
+ private PodcastChannel channel;
+
+ private TextView titleView;
+ private ImageView moreButton;
+
+ public PodcastChannelView(Context context) {
+ super(context);
+ this.context = context;
+ LayoutInflater.from(context).inflate(R.layout.artist_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.artist_name);
+ ImageButton starButton = (ImageButton) findViewById(R.id.artist_star);
+ starButton.setVisibility(View.GONE);
+ starButton.setFocusable(false);
+ moreButton = (ImageView) findViewById(R.id.artist_more);
+ moreButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ v.showContextMenu();
+ }
+ });
+ }
+
+ public void setPodcastChannel(PodcastChannel podcastChannel) {
+ channel = podcastChannel;
+ if(podcastChannel.getName() != null) {
+ titleView.setText(podcastChannel.getName());
+ } else {
+ titleView.setText(podcastChannel.getUrl());
+ }
+ }
+
+ @Override
+ protected void update() {
+ File file = FileUtil.getPodcastDirectory(context, channel);
+ if(file.exists()) {
+ moreButton.setImageResource(R.drawable.list_item_more_shaded);
+ } else {
+ moreButton.setImageResource(R.drawable.list_item_more);
+ }
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java b/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java new file mode 100644 index 00000000..ff2280c1 --- /dev/null +++ b/src/github/daneren2005/dsub/view/SackOfViewsAdapter.java @@ -0,0 +1,181 @@ +/*** + Copyright (c) 2008-2009 CommonsWare, LLC + Portions (c) 2009 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package github.daneren2005.dsub.view; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapter that simply returns row views from a list. + * <p/> + * If you supply a size, you must implement newView(), to + * create a required view. The adapter will then cache these + * views. + * <p/> + * If you supply a list of views in the constructor, that + * list will be used directly. If any elements in the list + * are null, then newView() will be called just for those + * slots. + * <p/> + * Subclasses may also wish to override areAllItemsEnabled() + * (default: false) and isEnabled() (default: false), if some + * of their rows should be selectable. + * <p/> + * It is assumed each view is unique, and therefore will not + * get recycled. + * <p/> + * Note that this adapter is not designed for long lists. It + * is more for screens that should behave like a list. This + * is particularly useful if you combine this with other + * adapters (e.g., SectionedAdapter) that might have an + * arbitrary number of rows, so it all appears seamless. + */ +public class SackOfViewsAdapter extends BaseAdapter { + private List<View> views = null; + + /** + * Constructor creating an empty list of views, but with + * a specified count. Subclasses must override newView(). + */ + public SackOfViewsAdapter(int count) { + super(); + + views = new ArrayList<View>(count); + + for (int i = 0; i < count; i++) { + views.add(null); + } + } + + /** + * Constructor wrapping a supplied list of views. + * Subclasses must override newView() if any of the elements + * in the list are null. + */ + public SackOfViewsAdapter(List<View> views) { + for (View view : views) { + view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + this.views = views; + } + + /** + * Get the data item associated with the specified + * position in the data set. + * + * @param position Position of the item whose data we want + */ + @Override + public Object getItem(int position) { + return (views.get(position)); + } + + /** + * How many items are in the data set represented by this + * Adapter. + */ + @Override + public int getCount() { + return (views.size()); + } + + /** + * Returns the number of types of Views that will be + * created by getView(). + */ + @Override + public int getViewTypeCount() { + return (getCount()); + } + + /** + * Get the type of View that will be created by getView() + * for the specified item. + * + * @param position Position of the item whose data we want + */ + @Override + public int getItemViewType(int position) { + return (position); + } + + /** + * Are all items in this ListAdapter enabled? If yes it + * means all items are selectable and clickable. + */ + @Override + public boolean areAllItemsEnabled() { + return (false); + } + + /** + * Returns true if the item at the specified position is + * not a separator. + * + * @param position Position of the item whose data we want + */ + @Override + public boolean isEnabled(int position) { + return (false); + } + + /** + * Get a View that displays the data at the specified + * position in the data set. + * + * @param position Position of the item whose data we want + * @param convertView View to recycle, if not null + * @param parent ViewGroup containing the returned View + */ + @Override + public View getView(int position, View convertView, + ViewGroup parent) { + View result = views.get(position); + + if (result == null) { + result = newView(position, parent); + views.set(position, result); + } + + return (result); + } + + /** + * Get the row id associated with the specified position + * in the list. + * + * @param position Position of the item whose data we want + */ + @Override + public long getItemId(int position) { + return (position); + } + + /** + * Create a new View to go into the list at the specified + * position. + * + * @param position Position of the item whose data we want + * @param parent ViewGroup containing the returned View + */ + protected View newView(int position, ViewGroup parent) { + throw new RuntimeException("You must override newView()!"); + } +} diff --git a/src/github/daneren2005/dsub/view/SongView.java b/src/github/daneren2005/dsub/view/SongView.java new file mode 100644 index 00000000..042d8031 --- /dev/null +++ b/src/github/daneren2005/dsub/view/SongView.java @@ -0,0 +1,241 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.*; +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PodcastEpisode; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.DownloadServiceImpl; +import github.daneren2005.dsub.service.DownloadFile; +import github.daneren2005.dsub.util.Util; + +import java.io.File; +import java.text.DateFormat; + +/** + * Used to display songs in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class SongView extends UpdateView implements Checkable { + private static final String TAG = SongView.class.getSimpleName(); + + private Context context; + private MusicDirectory.Entry song; + + private CheckedTextView checkedTextView; + private TextView titleTextView; + private TextView artistTextView; + private TextView durationTextView; + private TextView statusTextView; + private ImageButton starButton; + private ImageView moreButton; + + private DownloadService downloadService; + private long revision = -1; + private DownloadFile downloadFile; + + private boolean playing = false; + private int rightImage = 0; + private int moreImage = 0; + private boolean starred = false; + private boolean isWorkDone = false; + private boolean isSaved = false; + private File partialFile; + private boolean partialFileExists = false; + + public SongView(Context context) { + super(context); + this.context = context; + LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + + checkedTextView = (CheckedTextView) findViewById(R.id.song_check); + titleTextView = (TextView) findViewById(R.id.song_title); + artistTextView = (TextView) findViewById(R.id.song_artist); + durationTextView = (TextView) findViewById(R.id.song_duration); + statusTextView = (TextView) findViewById(R.id.song_status); + starButton = (ImageButton) findViewById(R.id.song_star); + starButton.setFocusable(false); + moreButton = (ImageView) findViewById(R.id.artist_more); + moreButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + public void setSong(MusicDirectory.Entry song, boolean checkable) { + this.song = song; + + StringBuilder artist = new StringBuilder(40); + + String bitRate = null; + if (song.getBitRate() != null) { + bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate()); + } + + String fileFormat = null; + if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) { + fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix()); + } else { + fileFormat = song.getSuffix(); + } + + if(!song.isVideo()) { + if(song instanceof PodcastEpisode) { + String date = ((PodcastEpisode)song).getDate(); + if(date != null) { + int index = date.indexOf(" "); + artist.append(date.substring(0, index != -1 ? index : date.length())); + } + } + else if(song.getArtist() != null) { + artist.append(song.getArtist()); + } + + String status = (song instanceof PodcastEpisode) ? ((PodcastEpisode)song).getStatus() : ""; + artist.append(" ("); + if("error".equals(status)) { + artist.append(getContext().getString(R.string.song_details_error)); + } else if("skipped".equals(status)) { + artist.append(getContext().getString(R.string.song_details_skipped)); + } else if("downloading".equals(status)) { + artist.append(getContext().getString(R.string.song_details_downloading)); + } else { + artist.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat)); + } + artist.append(")"); + } else { + artist.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat)); + } + + String title = song.getTitle(); + Integer track = song.getTrack(); + if(track != null && Util.getDisplayTrack(context)) { + title = String.format("%02d", track) + " " + title; + } + + titleTextView.setText(title); + artistTextView.setText(artist); + durationTextView.setText(Util.formatDuration(song.getDuration())); + checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE); + + revision = -1; + updateBackground(); + update(); + } + + @Override + protected void updateBackground() { + if (downloadService == null) { + downloadService = DownloadServiceImpl.getInstance(); + if(downloadService == null) { + return; + } + } + + long newRevision = downloadService.getDownloadListUpdateRevision(); + if(revision != newRevision || downloadFile == null) { + downloadFile = downloadService.forSong(song); + revision = newRevision; + } + + isWorkDone = downloadFile.isWorkDone(); + isSaved = downloadFile.isSaved(); + partialFile = downloadFile.getPartialFile(); + partialFileExists = partialFile.exists(); + } + + @Override + protected void update() { + if (downloadService == null) { + return; + } + + if(song.isStarred()) { + if(!starred) { + starButton.setVisibility(View.VISIBLE); + starred = true; + } + } else { + if(starred) { + starButton.setVisibility(View.GONE); + starred = false; + } + } + + int rightImage = 0; + if (isWorkDone) { + int moreImage = isSaved ? R.drawable.list_item_more_saved : R.drawable.list_item_more_shaded; + if(moreImage != this.moreImage) { + moreButton.setImageResource(moreImage); + this.moreImage = moreImage; + } + } else if(this.moreImage != R.drawable.list_item_more) { + moreButton.setImageResource(R.drawable.list_item_more); + this.moreImage = R.drawable.list_item_more; + } + + if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFileExists) { + statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext())); + rightImage = R.drawable.downloading; + } else if(this.rightImage != 0) { + statusTextView.setText(null); + } + if(this.rightImage != rightImage) { + statusTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, rightImage, 0); + this.rightImage = rightImage; + } + + boolean playing = downloadService.getCurrentPlaying() == downloadFile; + if (playing) { + if(!this.playing) { + this.playing = playing; + titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.stat_notify_playing, 0, 0, 0); + } + } else { + if(this.playing) { + this.playing = playing; + titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + } + + @Override + public void setChecked(boolean b) { + checkedTextView.setChecked(b); + } + + @Override + public boolean isChecked() { + return checkedTextView.isChecked(); + } + + @Override + public void toggle() { + checkedTextView.toggle(); + } +} diff --git a/src/github/daneren2005/dsub/view/UpdateView.java b/src/github/daneren2005/dsub/view/UpdateView.java new file mode 100644 index 00000000..7ce27f06 --- /dev/null +++ b/src/github/daneren2005/dsub/view/UpdateView.java @@ -0,0 +1,133 @@ +/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.view;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.LinearLayout;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.WeakHashMap;
+
+public class UpdateView extends LinearLayout {
+ private static final String TAG = UpdateView.class.getSimpleName();
+ private static final WeakHashMap<UpdateView, ?> INSTANCES = new WeakHashMap<UpdateView, Object>();
+
+ private static Handler backgroundHandler;
+ private static Handler uiHandler;
+ private static Runnable updateRunnable;
+
+ public UpdateView(Context context) {
+ super(context);
+
+ setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ INSTANCES.put(this, null);
+ int instanceCount = INSTANCES.size();
+ if (instanceCount > 50) {
+ Log.w(TAG, instanceCount + " live UpdateView instances");
+ }
+
+ startUpdater();
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+
+ }
+
+ private static synchronized void startUpdater() {
+ if(uiHandler != null) {
+ return;
+ }
+
+ uiHandler = new Handler();
+ updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateAll();
+ }
+ };
+
+ new Thread(new Runnable() {
+ public void run() {
+ Looper.prepare();
+ backgroundHandler = new Handler(Looper.myLooper());
+ uiHandler.post(updateRunnable);
+ Looper.loop();
+ }
+ }).start();
+ }
+
+ private static void updateAll() {
+ try {
+ List<UpdateView> views = new ArrayList<UpdateView>();;
+ for (UpdateView view : INSTANCES.keySet()) {
+ if (view.isShown()) {
+ views.add(view);
+ }
+ }
+ updateAllLive(views);
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+ private static void updateAllLive(final List<UpdateView> views) {
+ final Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for(UpdateView view: views) {
+ view.update();
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ uiHandler.postDelayed(updateRunnable, 1000L);
+ }
+ };
+
+ backgroundHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ for(UpdateView view: views) {
+ view.updateBackground();
+ }
+ uiHandler.post(runnable);
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+ });
+ }
+
+ protected void updateBackground() {
+
+ }
+ protected void update() {
+
+ }
+}
diff --git a/src/github/daneren2005/dsub/view/VisualizerView.java b/src/github/daneren2005/dsub/view/VisualizerView.java new file mode 100644 index 00000000..53ebc2ec --- /dev/null +++ b/src/github/daneren2005/dsub/view/VisualizerView.java @@ -0,0 +1,137 @@ +/* + 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 2011 (C) Sindre Mehus + */ +package github.daneren2005.dsub.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.media.audiofx.Visualizer; +import android.util.AttributeSet; +import android.view.View; +import github.daneren2005.dsub.audiofx.VisualizerController; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.DownloadServiceImpl; + +/** + * A simple class that draws waveform data received from a + * {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture} + * + * @author Sindre Mehus + * @version $Id$ + */ +public class VisualizerView extends View { + + private static final int PREFERRED_CAPTURE_RATE_MILLIHERTZ = 20000; + + private final Paint paint = new Paint(); + + private byte[] data; + private float[] points; + private boolean active = false; + + public VisualizerView(Context context) { + super(context); + + paint.setStrokeWidth(2f); + paint.setAntiAlias(true); + paint.setColor(Color.rgb(51, 181, 229)); + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + VisualizerController visualizerController = getVizualiser(); + Visualizer visualizer = visualizerController == null ? null : visualizerController.getVisualizer(); + if (visualizer == null) { + this.active = false; + return; + } + + int captureRate = Math.min(PREFERRED_CAPTURE_RATE_MILLIHERTZ, Visualizer.getMaxCaptureRate()); + if (active) { + visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() { + @Override + public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) { + updateVisualizer(waveform); + } + + @Override + public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { + } + }, captureRate, true, false); + } else { + visualizer.setDataCaptureListener(null, captureRate, false, false); + } + + visualizer.setEnabled(active); + if(!active) { + visualizerController.release(); + } + invalidate(); + } + + private VisualizerController getVizualiser() { + DownloadService downloadService = DownloadServiceImpl.getInstance(); + VisualizerController visualizerController = downloadService == null ? null : downloadService.getVisualizerController(); + return visualizerController; + } + + private void updateVisualizer(byte[] waveform) { + this.data = waveform; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!active) { + return; + } + DownloadService downloadService = DownloadServiceImpl.getInstance(); + if (downloadService != null && downloadService.getPlayerState() != PlayerState.STARTED) { + return; + } + + if (data == null) { + return; + } + + if (points == null || points.length < data.length * 4) { + points = new float[data.length * 4]; + } + + int w = getWidth(); + int h = getHeight(); + + for (int i = 0; i < data.length - 1; i++) { + points[i * 4] = w * i / (data.length - 1); + points[i * 4 + 1] = h / 2 + ((byte) (data[i] + 128)) * (h / 2) / 128; + points[i * 4 + 2] = w * (i + 1) / (data.length - 1); + points[i * 4 + 3] = h / 2 + ((byte) (data[i + 1] + 128)) * (h / 2) / 128; + } + + canvas.drawLines(points, paint); + } +} diff --git a/subsonic.keystore b/subsonic.keystore Binary files differnew file mode 100644 index 00000000..46996d4c --- /dev/null +++ b/subsonic.keystore diff --git a/subsonic.png b/subsonic.png Binary files differnew file mode 100644 index 00000000..e17a4540 --- /dev/null +++ b/subsonic.png diff --git a/subsonic2.png b/subsonic2.png Binary files differnew file mode 100644 index 00000000..561529a5 --- /dev/null +++ b/subsonic2.png |