diff --git a/.gitignore b/.gitignore index 8dba7e167..e3fd5ecd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,17 @@ .\#* *~ *.apk +build bin gen -.classpath +out local.properties build.properties -src/com/danga/squeezer/IServiceCallback.java -src/com/danga/squeezer/ISqueezeService.java -src/com/danga/squeezer/R.java +squeezer.properties +.classpath /ant.properties +.settings/ +*.iml +*.patch +.idea +.gradle diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index 7eab8cc4e..000000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 000000000..5b0d0c2c5 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,40 @@ +HACKING +======= + +This guide assumes you have already downloaded and installed the Android +SDK, in to a directory referred to as $SDK. + +Fetch the code +-------------- + + +Android Studio +-------------- + +* Install the Android Build Tools revision 17.0.0 if not already installed. + + Run $SDK/tools/android and select "Android SDK Build-tools", + +* Run Android Studio + +* If you have no projects open then choose "Import project" from the dialog + that appears. + + If you already have a project open then choose File > Import Project... + +* In the "Select File or Directory to Import" dialog that appears, navigate + to the directory that you fetched the Squeezer source code in to and + select the build.gradle file that ships with Squeezer. + +* In the "Import Project from Gradle" dialog tick "Use auto-import" and + make sure that "Use gradle wrapper (recommended)" is selected. + +* Copy ide/intellij/codestyles/AndroidStyle.xml to Android Studio's config + directory. + + Linux: ~/.AndroidStudioPreview/config/codestyles + OS X: ~/Library/Preferences/AndroidStudioPreview/codestyles + Windows: ~/.AndroidStudioPreview/config/codestyles + +* Go to Settings (or Preferences in Mac OS X) > Code Style > Java, select + "AndroidStyle", as well as Code Style > XML and select "AndroidStyle". diff --git a/Squeezer/build.gradle b/Squeezer/build.gradle new file mode 100644 index 000000000..bb265a1fd --- /dev/null +++ b/Squeezer/build.gradle @@ -0,0 +1,90 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.6.+' + } +} +apply plugin: 'android' + +repositories { + mavenCentral() +} + +dependencies { + compile fileTree(dir: 'libs', include: '*.jar') + + // Android support libraries + // Note: these libraries require the "Google Repository" and "Android + // Support Repository" to be installed via the SDK manager. + compile 'com.android.support:support-v4:19.0.+' + compile 'com.android.support:appcompat-v7:19.0.+' + + // Third party libraries + compile 'ch.acra:acra:4.5.0' + compile 'com.google.guava:guava:15.0' + // findbugs is required for Proguard to work with Guava. + compile 'com.google.code.findbugs:jsr305:2.0.2' + + // Changelogs, see https://github.com/cketti/ckChangeLog. + compile 'de.cketti.library.changelog:ckchangelog:1.2.0' +} + +android { + compileSdkVersion 19 + buildToolsVersion "19.0.0" + + defaultConfig { + minSdkVersion 7 + targetSdkVersion 19 + } + + buildTypes { + release { + runProguard true + // You could use 'proguardFile "proguard.cfg"' here and get the + // same effect, but this ensures that any changes to + // proguard-android-optimize.txt are automatically included. + proguardFile getDefaultProguardFile('proguard-android-optimize.txt') + proguardFile "proguard-acra.cfg" + proguardFile "proguard-guava.cfg" + proguardFile "proguard-squeezer.cfg" + } + } + + signingConfigs { + if(project.hasProperty("Squeezer.properties") + && file(project.property("Squeezer.properties")).exists()) { + Properties props = new Properties() + props.load(new FileInputStream(file(project.property("Squeezer.properties")))) + release { + storeFile file("keystore") + storePassword props['key.store.password'] + keyAlias "squeezer" + keyPassword props['key.alias.password'] + } + } else { + release { + storeFile file("keystore") + storePassword "fakeStorePassword" + keyAlias "squeezer" + keyPassword "fakeKeyPassword" + } + } + } + + productFlavors { + beta { + versionCode 16 + versionName "1.0-beta-3" + signingConfig android.signingConfigs.release + } + + live { + versionCode 13 + versionName "0.9.1" + signingConfig android.signingConfigs.release + } + } +} diff --git a/Squeezer/gradle.properties b/Squeezer/gradle.properties new file mode 100644 index 000000000..a00fedad5 --- /dev/null +++ b/Squeezer/gradle.properties @@ -0,0 +1 @@ +Squeezer.properties=squeezer.properties diff --git a/keystore b/Squeezer/keystore similarity index 100% rename from keystore rename to Squeezer/keystore diff --git a/libs/libGoogleAnalytics.jar b/Squeezer/libs/libGoogleAnalytics.jar similarity index 100% rename from libs/libGoogleAnalytics.jar rename to Squeezer/libs/libGoogleAnalytics.jar diff --git a/Squeezer/proguard-acra.cfg b/Squeezer/proguard-acra.cfg new file mode 100644 index 000000000..b93ed014a --- /dev/null +++ b/Squeezer/proguard-acra.cfg @@ -0,0 +1,42 @@ +# From: https://github.com/ACRA/acra/wiki/Proguard + +# ACRA specifics +# we need line numbers in our stack traces otherwise they are pretty useless +-renamesourcefileattribute SourceFile +-keepattributes SourceFile,LineNumberTable + +# ACRA needs "annotations" so add this... +-keepattributes *Annotation* + +# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'. +# Note: if you are removing log messages elsewhere in this file then this isn't necessary +-keep class org.acra.ACRA { + *; +} + +# keep this around for some enums that ACRA needs +-keep class org.acra.ReportingInteractionMode { + *; +} + +-keepnames class org.acra.sender.HttpSender$** { + *; +} + +-keepnames class org.acra.ReportField { + *; +} + +# keep this otherwise it is removed by ProGuard +-keep public class org.acra.ErrorReporter +{ + public void addCustomData(java.lang.String,java.lang.String); + public void putCustomData(java.lang.String,java.lang.String); + public void removeCustomData(java.lang.String); +} + +# keep this otherwise it is removed by ProGuard +-keep public class org.acra.ErrorReporter +{ + public void handleSilentException(java.lang.Throwable); +} diff --git a/Squeezer/proguard-guava.cfg b/Squeezer/proguard-guava.cfg new file mode 100644 index 000000000..71f9864b0 --- /dev/null +++ b/Squeezer/proguard-guava.cfg @@ -0,0 +1,7 @@ +# Proguard configuration for Guava +# +# Based on https://code.google.com/p/guava-libraries/wiki/UsingProGuardWithGuava. + +-dontwarn sun.misc.Unsafe +-dontwarn com.google.common.collect.MinMaxPriorityQueue +-dontwarn com.google.common.util.concurrent.** diff --git a/Squeezer/proguard-squeezer.cfg b/Squeezer/proguard-squeezer.cfg new file mode 100644 index 000000000..2ebf3fcfc --- /dev/null +++ b/Squeezer/proguard-squeezer.cfg @@ -0,0 +1,22 @@ +# No sense in obfuscating names since the code is freely available, +# and it makes debugging a little tricker +-keepnames class uk.org.ngo.squeezer.** { + ; +} + +# Explicitly keep the model class constructors. +# Without this you get NoSuchMethodExceptions when creating model objects. +-keep public class uk.org.ngo.squeezer.model.** { + (java.lang.String); + (java.util.Map); + (android.os.Parcel); +} + +# Needed to support the reflection in BaseItemView. +-keepattributes Signature + +# Strip out certain logging calls. +-assumenosideeffects class android.util.Log { + public static *** d(...); + public static *** v(...); +} diff --git a/Squeezer/proguard.cfg b/Squeezer/proguard.cfg new file mode 100644 index 000000000..3e3e4ba88 --- /dev/null +++ b/Squeezer/proguard.cfg @@ -0,0 +1,69 @@ +# This is a configuration file for ProGuard. +# http://proguard.sourceforge.net/index.html#manual/usage.html + +# Optimizations: If you don't want to optimize, use the +# proguard-android.txt configuration file instead of this one, which +# turns off the optimization flags. Adding optimization introduces +# certain risks, since for example not all optimizations performed by +# ProGuard works on all versions of Dalvik. The following flags turn +# off various optimizations known to have issues, but the list may not +# be complete or up to date. (The "arithmetic" optimization can be +# used if you are only targeting Android 2.0 or later.) Make sure you +# test thoroughly if you go this route. +-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* +-optimizationpasses 5 +-allowaccessmodification +-dontpreverify + +# The remainder of this file is identical to the non-optimized version +# of the Proguard configuration file (except that the other file has +# flags to turn off optimization). + +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-verbose + +-keepattributes *Annotation* +-keep public class com.google.vending.licensing.ILicensingService +-keep public class com.android.vending.licensing.ILicensingService + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native ; +} + +# keep setters in Views so that animations can still work. +# see http://proguard.sourceforge.net/manual/examples.html#beans +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} + +# We want to keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keepclassmembers class **.R$* { + public static ; +} + +# The support library contains references to newer platform versions. +# Don't warn about those in case this app is linking against an older +# platform version. We know about them, and they are safe. +-dontwarn android.support.** + +# Squeezer customisations +-include proguard-acra.cfg +-include proguard-guava.cfg +-include proguard-squeezer.cfg diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/mock/SqueezeboxServerMock.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/mock/SqueezeboxServerMock.java new file mode 100644 index 000000000..ce96f9db8 --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/mock/SqueezeboxServerMock.java @@ -0,0 +1,206 @@ + +package uk.org.ngo.squeezer.test.mock; + +import junit.framework.AssertionFailedError; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; + +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog.AlbumsSortOrder; + +/** + * Emulates LMS for testing purposes + *

+ * Each instance will wait for an incoming connection, accept it, read commands from the + * inputstream, and reply to the outputstream, until the connection is broken or the exit command is + * received, at which point the connection is terminated. + *

+ * To make a new connection a new instance must be started. + * + * @author Kurt Aaholst + */ +public class SqueezeboxServerMock extends Thread { + + private static final String TAG = SqueezeboxServerMock.class.getSimpleName(); + + public static final int CLI_PORT = 9091; + + private Object serverReadyMonitor = new Object(); + + private boolean accepting; + + public static class Starter { + + public SqueezeboxServerMock start() { + SqueezeboxServerMock server = new SqueezeboxServerMock(this); + server.start(); + synchronized (server.serverReadyMonitor) { + while (!server.accepting) { + try { + server.serverReadyMonitor.wait(2000); + if (!server.accepting) { + throw new AssertionFailedError("Expected the mock server to start"); + } + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while wating for the mock server to start"); + } + } + } + return server; + } + + public Starter username(String username) { + this.username = username; + return this; + } + + public Starter password(String password) { + this.password = password; + return this; + } + + public Starter canRandomplay(boolean canRandomplay) { + this.canRandomplay = canRandomplay; + return this; + } + + public Starter canMusicFolder(boolean canMusicFolder) { + this.canMusicFolder = canMusicFolder; + return this; + } + + public Starter albumsSortOrder(AlbumsSortOrder albumsSortOrder) { + this.albumsSortOrder = albumsSortOrder; + return this; + } + + private String username = null; + + private String password = null; + + private boolean canRandomplay = true; + + private boolean canMusicFolder = true; + + private AlbumsSortOrder albumsSortOrder = AlbumsSortOrder.album; + } + + public static Starter starter() { + return new Starter(); + } + + private SqueezeboxServerMock(Starter starter) { + username = starter.username; + password = starter.password; + canRamdomplay = starter.canRandomplay; + canMusicFolder = starter.canMusicFolder; + albumsSortOrder = starter.albumsSortOrder; + } + + private String username; + + private String password; + + private boolean canMusicFolder; + + private boolean canRamdomplay; + + private AlbumsSortOrder albumsSortOrder; + + @Override + public void run() { + ServerSocket serverSocket; + Socket socket; + BufferedReader in; + PrintWriter out; + try { + // Establish server socket + serverSocket = new ServerSocket(CLI_PORT); + + // Wait for incoming connection + Log.d(TAG, "Mock server listening on port: " + serverSocket.getLocalPort()); + synchronized (serverReadyMonitor) { + accepting = true; + serverReadyMonitor.notifyAll(); + } + socket = serverSocket.accept(); + + Log.d(TAG, "Mock server connected to: " + socket.getRemoteSocketAddress()); + in = new BufferedReader(new InputStreamReader(socket.getInputStream()), 128); + out = new PrintWriter(socket.getOutputStream(), true); + } catch (IOException e) { + throw new Error(e); + } + + boolean loggedIn = (username == null || password == null); + + while (true) { + // read data from Socket + String line; + try { + line = in.readLine(); + } catch (IOException e) { + line = null; + } + Log.d(TAG, "Mock server got: " + line); + if (line == null) { + break; // Client disconnected + } + + String[] tokens = line.split(" "); + + if (tokens[0].equals("login")) { + out.println(tokens[0] + " " + tokens[1] + " ******"); + if (username != null && password != null) { + if (tokens.length < 2 || !username.equals(tokens[1])) { + break; + } + if (tokens.length < 3 || !password.equals(tokens[2])) { + break; + } + } + loggedIn = true; + } else { + if (!loggedIn) { + break; + } + + if (line.equals("exit")) { + out.println(line); + break; + } else if (line.equals("listen 1")) { + //Just ignore, mock doesn't support server side events + out.println("listen 1"); + } else if (line.equals("can musicfolder ?")) { + out.println("can musicfolder " + (canMusicFolder ? 1 : 0)); + } else if (line.equals("can randomplay ?")) { + out.println("can randomplay " + (canRamdomplay ? 1 : 0)); + } else if (line.equals("pref httpport ?")) { + out.println("pref httpport 9092"); + } else if (line.equals("pref jivealbumsort ?")) { + out.println("pref jivealbumsort " + albumsSortOrder); + } else if (line.equals("version ?")) { + out.println("version 7.7.2"); + } else if (tokens[0].equals("players")) { + //TODO implement + } else { + out.println(line); + } + } + } + + try { + socket.close(); + serverSocket.close(); + } catch (IOException e) { + } + + } + +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/mock/SqueezeboxServerMockTest.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/mock/SqueezeboxServerMockTest.java new file mode 100644 index 000000000..0166ab7f8 --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/mock/SqueezeboxServerMockTest.java @@ -0,0 +1,194 @@ +package uk.org.ngo.squeezer.test.mock; + +import junit.framework.TestCase; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; + +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog.AlbumsSortOrder; + +/** + * This is a test of the mock server. + *

+ * It doesn't really test any Squeezer functionally, but it is handy when developing or debugging + * the mock server. + * + * @author Kurt Aaholst + */ +public class SqueezeboxServerMockTest extends TestCase { + + public void testDefaults() { + SqueezeboxServerMock.starter().start(); + + try { + SocketAddress sa = new InetSocketAddress("localhost", SqueezeboxServerMock.CLI_PORT); + Socket socket = new Socket(); + + socket.connect(sa, 10 * 1000); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()), + 128); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + + out.println("login testuser testpassword"); + assertEquals("login testuser ******", in.readLine()); + + out.println("abcd"); + assertEquals("abcd", in.readLine()); + + out.println("version"); + assertEquals("version", in.readLine()); + + out.println("can musicfolder ?"); + out.println("can randomplay ?"); + out.println("pref httpport ?"); + out.println("pref jivealbumsort ?"); + out.println("version ?"); + assertEquals("can musicfolder 1", in.readLine()); + assertEquals("can randomplay 1", in.readLine()); + assertEquals("pref httpport 9092", in.readLine()); + assertEquals("pref jivealbumsort album", in.readLine()); + assertEquals("version 7.7.2", in.readLine()); + + out.println("exit"); + assertEquals("exit", in.readLine()); + + assertNull(in.readLine()); + + in.close(); + out.close(); + socket.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void testSettings() { + SqueezeboxServerMock.starter().canRandomplay(false).albumsSortOrder(AlbumsSortOrder.artflow) + .start(); + + try { + SocketAddress sa = new InetSocketAddress("localhost", SqueezeboxServerMock.CLI_PORT); + Socket socket = new Socket(); + + socket.connect(sa, 10 * 1000); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()), + 128); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + + out.println("can musicfolder ?"); + out.println("can randomplay ?"); + out.println("pref httpport ?"); + out.println("pref jivealbumsort ?"); + out.println("version ?"); + assertEquals("can musicfolder 1", in.readLine()); + assertEquals("can randomplay 0", in.readLine()); + assertEquals("pref httpport 9092", in.readLine()); + assertEquals("pref jivealbumsort artflow", in.readLine()); + assertEquals("version 7.7.2", in.readLine()); + + out.println("exit"); + assertEquals("exit", in.readLine()); + + assertNull(in.readLine()); + + in.close(); + out.close(); + socket.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void testAuthentication() { + SqueezeboxServerMock.starter().username("user").password("1234").start(); + + try { + SocketAddress sa = new InetSocketAddress("localhost", SqueezeboxServerMock.CLI_PORT); + Socket socket = new Socket(); + + socket.connect(sa, 10 * 1000); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()), + 128); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + + out.println("login user 1234"); + assertEquals("login user ******", in.readLine()); + + out.println("exit"); + assertEquals("exit", in.readLine()); + + assertNull(in.readLine()); + + in.close(); + out.close(); + socket.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void testAuthenticationFailure() { + SqueezeboxServerMock.starter().username("user").password("1234").start(); + + try { + SocketAddress sa = new InetSocketAddress("localhost", SqueezeboxServerMock.CLI_PORT); + Socket socket = new Socket(); + + socket.connect(sa, 10 * 1000); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()), + 128); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + + out.println("login user wrongpassword"); + assertEquals("login user ******", in.readLine()); + + assertNull(in.readLine()); + + in.close(); + out.close(); + socket.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void testAuthenticatedServer() { + SqueezeboxServerMock.starter().username("user").password("1234").start(); + + try { + SocketAddress sa = new InetSocketAddress("localhost", SqueezeboxServerMock.CLI_PORT); + Socket socket = new Socket(); + + socket.connect(sa, 10 * 1000); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()), + 128); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + + out.println("version ?"); + assertNull(in.readLine()); + + in.close(); + out.close(); + socket.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/model/SqueezerSongTest.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/model/SqueezerSongTest.java new file mode 100644 index 000000000..23210d093 --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/model/SqueezerSongTest.java @@ -0,0 +1,66 @@ +package uk.org.ngo.squeezer.test.model; + +import android.test.AndroidTestCase; + +import java.util.HashMap; + +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Song; + +public class SqueezerSongTest extends AndroidTestCase { + + Song song1, song2, song3; + + HashMap record1, record2, record3; + + /** + * Verify that the equals() method compares correctly against nulls, other item types, and is + * reflexive (a = a), symmetric (a = b && b = a), and transitive (a = b && b = c && a = c). + */ + public void testEquals() { + record1 = new HashMap(); + record2 = new HashMap(); + record3 = new HashMap(); + + song1 = new Song(record1); + song2 = new Song(record2); + song3 = new Song(record3); + + assertTrue("A song equals itself (reflexive)", song1.equals(song1)); + + assertFalse("A song, even an empty one, is not equal to null", + song1.equals(null)); + + assertTrue("Two songs with null IDs are equal", song1.equals(song2)); + assertTrue("... and is symmetric", song2.equals(song1)); + + Album album1 = new Album(record1); + assertFalse("Null song does not equal a null album", song1.equals(album1)); + assertFalse("... and is symmetric", album1.equals(song1)); + + song1.setId("1"); + song2.setId("2"); + assertFalse("Songs with different IDs are different", song1.equals(song2)); + assertFalse("... and is symmetric", song2.equals(song1)); + + song1.setId("1"); + song2.setId("1"); + song3.setId("1"); + assertTrue("Songs with the same ID are equivalent", song1.equals(song2)); + assertTrue("... and is symmetric", song2.equals(song1)); + assertTrue("... and is transitive (1)", song2.equals(song3)); + assertTrue("... and is transitive (2)", song1.equals(song3)); + + song1.setId("1"); + song1.setName("Song"); + song2.setId("2"); + song2.setName("Song"); + assertFalse("Songs with same name but different IDs are different", song1.equals(song2)); + assertFalse("... and is symmetric", song2.equals(song1)); + + song1.setId("1"); + album1.setId("1"); + assertFalse("Songs and albums with the same ID are different", song1.equals(album1)); + assertFalse("... and is symmetric", album1.equals(song1)); + } +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/ServiceCallbackTest.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/ServiceCallbackTest.java new file mode 100644 index 000000000..edbad30ba --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/ServiceCallbackTest.java @@ -0,0 +1,64 @@ +package uk.org.ngo.squeezer.test.server; + +import android.os.RemoteException; + +import uk.org.ngo.squeezer.IServiceCallback; +import uk.org.ngo.squeezer.model.Player; + +public class ServiceCallbackTest extends IServiceCallback.Stub { + + int onPlayerChanged; + + Player currentPlayer; + + int onConnectionChanged; + + boolean isConnected; + + boolean isPostConnect; + + boolean isLoginFailed; + + + @Override + public void onPlayerChanged(Player player) throws RemoteException { + onPlayerChanged++; + currentPlayer = player; + } + + @Override + public void onConnectionChanged(boolean isConnected, boolean postConnect, boolean loginFailed) + throws RemoteException { + onConnectionChanged++; + this.isConnected = isConnected; + isPostConnect = postConnect; + isLoginFailed = loginFailed; + } + + @Override + public void onPlayStatusChanged(String playStatus) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void onShuffleStatusChanged(boolean initial, int shuffleStatus) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void onRepeatStatusChanged(boolean initial, int repeatStatus) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void onTimeInSongChange(int secondsIn, int secondsTotal) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void onPowerStatusChanged(boolean canPowerOn, boolean canPowerOff) + throws RemoteException { + // TODO Auto-generated method stub + } + +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/SqueezeServiceTest.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/SqueezeServiceTest.java new file mode 100644 index 000000000..5f5b3c8d3 --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/SqueezeServiceTest.java @@ -0,0 +1,88 @@ + +package uk.org.ngo.squeezer.test.server; + +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.test.ServiceTestCase; + +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog.AlbumsSortOrder; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.service.SqueezeService; +import uk.org.ngo.squeezer.test.mock.SqueezeboxServerMock; + +/** + * @author Kurt Aaholst + */ +public class SqueezeServiceTest extends ServiceTestCase { + + public SqueezeServiceTest() { + super(SqueezeService.class); + } + + public void testConnectionFailure() throws RemoteException, InterruptedException { + IBinder binder = bindService(new Intent(getContext(), SqueezeService.class)); + ISqueezeService service = ISqueezeService.Stub.asInterface(binder); + ServiceCallbackTest serviceCallback = new ServiceCallbackTest(); + + service.registerCallback(serviceCallback); + service.startConnect("localhost", "test", "test"); + Thread.sleep(1000); //TODO proper synchronization + + assertEquals(2, serviceCallback.onConnectionChanged); + assertFalse(serviceCallback.isConnected); + } + + public void testConnect() throws RemoteException, InterruptedException { + IBinder binder = bindService(new Intent(getContext(), SqueezeService.class)); + ISqueezeService service = ISqueezeService.Stub.asInterface(binder); + ServiceCallbackTest serviceCallback = new ServiceCallbackTest(); + WaitForHandshake waitForHandshake = new WaitForHandshake(service); + + SqueezeboxServerMock.starter().start(); + service.registerCallback(serviceCallback); + service.startConnect("localhost:" + SqueezeboxServerMock.CLI_PORT, "test", "test"); + waitForHandshake.waitForHandshakeCompleted(); + + assertEquals(2, serviceCallback.onConnectionChanged); + assertTrue(serviceCallback.isConnected); + assertTrue(service.canMusicfolder()); + assertTrue(service.canRandomplay()); + assertEquals(AlbumsSortOrder.album.name(), service.preferredAlbumSort()); + } + + public void testConnectProtectedServer() throws RemoteException, InterruptedException { + IBinder binder = bindService(new Intent(getContext(), SqueezeService.class)); + ISqueezeService service = ISqueezeService.Stub.asInterface(binder); + ServiceCallbackTest serviceCallback = new ServiceCallbackTest(); + + SqueezeboxServerMock.starter().username("user").password("1234").start(); + + service.registerCallback(serviceCallback); + WaitForHandshake waitForHandshake = new WaitForHandshake(service); + service.startConnect("localhost:" + SqueezeboxServerMock.CLI_PORT, "user", "1234"); + waitForHandshake.waitForHandshakeCompleted(); + + assertEquals(2, serviceCallback.onConnectionChanged); + assertTrue(serviceCallback.isConnected); + assertTrue(service.canMusicfolder()); + assertTrue(service.canRandomplay()); + assertEquals(AlbumsSortOrder.album.name(), service.preferredAlbumSort()); + } + + public void testAuthenticationFailure() throws RemoteException, InterruptedException { + IBinder binder = bindService(new Intent(getContext(), SqueezeService.class)); + ISqueezeService service = ISqueezeService.Stub.asInterface(binder); + ServiceCallbackTest serviceCallback = new ServiceCallbackTest(); + + SqueezeboxServerMock.starter().username("user").password("1234").start(); + + service.registerCallback(serviceCallback); + service.startConnect("localhost:" + SqueezeboxServerMock.CLI_PORT, "test", "test"); + Thread.sleep(1000); //TODO proper synchronization + + assertEquals(3, serviceCallback.onConnectionChanged); + assertFalse(serviceCallback.isConnected); + } + +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/WaitForHandshake.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/WaitForHandshake.java new file mode 100644 index 000000000..0bde5cefa --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/server/WaitForHandshake.java @@ -0,0 +1,44 @@ + +package uk.org.ngo.squeezer.test.server; + +import junit.framework.AssertionFailedError; + +import android.os.RemoteException; + +import uk.org.ngo.squeezer.IServiceHandshakeCallback; +import uk.org.ngo.squeezer.service.ISqueezeService; + +public class WaitForHandshake extends IServiceHandshakeCallback.Stub { + + private Object handshakeMonitor = new Object(); + + private boolean handshakeCompleted; + + public WaitForHandshake(ISqueezeService service) throws RemoteException { + service.registerHandshakeCallback(this); + } + + @Override + synchronized public void onHandshakeCompleted() throws RemoteException { + synchronized (handshakeMonitor) { + handshakeCompleted = true; + handshakeMonitor.notifyAll(); + } + } + + public void waitForHandshakeCompleted() { + synchronized (handshakeMonitor) { + while (!handshakeCompleted) { + try { + handshakeMonitor.wait(2000); + if (!handshakeCompleted) { + throw new AssertionFailedError("Expected handshake to complete"); + } + } catch (InterruptedException e) { + System.out.println("InterruptedException caught in waitForHandshakeCompleted"); + } + } + } + } + +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/util/ReflectionTest.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/util/ReflectionTest.java new file mode 100644 index 000000000..b60bcf8c4 --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/util/ReflectionTest.java @@ -0,0 +1,364 @@ +package uk.org.ngo.squeezer.test.util; + +import junit.framework.TestCase; + +import java.lang.reflect.Type; +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import uk.org.ngo.squeezer.util.Reflection; + +public class ReflectionTest extends TestCase { + + class Item { + + } + + class Item1 extends Item { + + } + + class Item2 extends Item { + + } + + class GroupItem extends Item { + + } + + class GroupItem1 extends GroupItem { + + } + + class GroupItem2 extends GroupItem { + + } + + + class A { + + } + + class B1 extends A { + + } + + class B2 extends A { + + } + + class C extends A { + + } + + class D1 extends C { + + } + + class D2 extends C { + + } + + + class AA { + + } + + class BB extends AA { + + } + + + interface I { + + } + + class AI implements I { + + } + + class BI1 extends AI { + + } + + class BI2 extends AI { + + } + + class BIG1 extends AI { + + } + + class BIG2 extends AI { + + } + + class CIG extends AI { + + } + + class CIG1 extends CIG { + + } + + class CIG2 extends CIG { + + } + + + // Wrong class def, doesn't resolve + class StrangeExtend extends A { + + } + + class Item1ToItem2 extends StrangeExtend { + + } + + + interface _I { + + } + + interface II extends _I { + + } + + class AII implements II { + + } + + class BII implements II { + + } + + class CII extends BII { + + } + + class BAII extends AII { + + } + + class BIII implements II, I { + + } + + class AIII implements II, I { + + } + + interface __I { + + } + + class BAIII extends AIII implements __I { + + } + + class SwapOrder1 extends AA { + + } + + class SwapOrder2 implements II { + + } + + class SwapOrder3 extends AII { + + } + + public void testGenericTypeResolver() { + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(new B1().getClass(), A.class)); + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(B1.class, A.class)); + assertTypesEquals(new Type[]{Item2.class}, + Reflection.genericTypeResolver(new B2().getClass(), A.class)); + assertTypesEquals(new Type[]{Item2.class}, + Reflection.genericTypeResolver(B2.class, A.class)); + + assertTypesEquals(new Type[]{GroupItem1.class}, + Reflection.genericTypeResolver(D1.class, A.class)); + assertTypesEquals(new Type[]{GroupItem2.class}, + Reflection.genericTypeResolver(D2.class, A.class)); + assertTypesEquals(new Type[]{GroupItem1.class}, + Reflection.genericTypeResolver(new D1().getClass(), C.class)); + assertTypesEquals(new Type[]{GroupItem2.class}, + Reflection.genericTypeResolver(new D2().getClass(), C.class)); + + assertTypesEquals(new Type[]{Item1.class, Item2.class}, + Reflection.genericTypeResolver(BII.class, II.class)); + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(BII.class, _I.class)); + assertTypesEquals(new Type[]{Item1.class, Item2.class}, + Reflection.genericTypeResolver(CII.class, II.class)); + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(CII.class, _I.class)); + assertTypesEquals(new Type[]{Item1.class, Item2.class}, + Reflection.genericTypeResolver(BAII.class, II.class)); + assertTypesEquals(new Type[]{Item1.class, Item2.class}, + Reflection.genericTypeResolver(BAII.class, AII.class)); + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(BAII.class, _I.class)); + assertTypesEquals(new Type[]{Item1.class, Item2.class}, + Reflection.genericTypeResolver(BIII.class, II.class)); + assertTypesEquals(new Type[]{Item2.class}, + Reflection.genericTypeResolver(BIII.class, I.class)); + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(BIII.class, _I.class)); + assertTypesEquals(new Type[]{Item1.class, GroupItem1.class}, + Reflection.genericTypeResolver(BAIII.class, II.class)); + assertTypesEquals(new Type[]{Item1.class, GroupItem1.class}, + Reflection.genericTypeResolver(BAIII.class, AIII.class)); + assertTypesEquals(new Type[]{GroupItem1.class}, + Reflection.genericTypeResolver(BAIII.class, I.class)); + assertTypesEquals(new Type[]{Item1.class}, + Reflection.genericTypeResolver(BAIII.class, _I.class)); + assertTypesEquals(new Type[]{GroupItem2.class}, + Reflection.genericTypeResolver(BAIII.class, __I.class)); + + assertTypesEquals(new Type[]{Item2.class, Item1.class}, + Reflection.genericTypeResolver(new SwapOrder1() { + }.getClass(), AA.class)); + assertTypesEquals(new Type[]{Item2.class, Item1.class}, + Reflection.genericTypeResolver(new SwapOrder2() { + }.getClass(), II.class)); + assertTypesEquals(new Type[]{Item2.class, Item1.class}, + Reflection.genericTypeResolver(new SwapOrder3() { + }.getClass(), II.class)); + assertTypesEquals(new Type[]{Item2.class, Item1.class}, + Reflection.genericTypeResolver(new SwapOrder3() { + }.getClass(), AII.class)); + } + + private void assertTypesEquals(Type[] expected, Type[] actual) { + assertEquals(typeArray(expected), typeArray(actual)); + } + + private String typeArray(Type[] types) { + StringBuilder sb = new StringBuilder(); + for (Type type : types) { + sb.append(sb.length() > 0 ? "," : "["); + sb.append(type); + } + return sb.append("]").toString(); + } + + public void testGetGenericClass() { + assertEquals(Item1.class, Reflection.getGenericClass(new B1().getClass(), A.class, 0)); + assertEquals(Item1.class, Reflection.getGenericClass(B1.class, A.class, 0)); + assertEquals(Item2.class, Reflection.getGenericClass(new B2().getClass(), A.class, 0)); + assertEquals(Item2.class, Reflection.getGenericClass(B2.class, A.class, 0)); + + assertEquals(GroupItem1.class, + Reflection.getGenericClass(new D1().getClass(), A.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(D1.class, A.class, 0)); + assertEquals(GroupItem2.class, + Reflection.getGenericClass(new D2().getClass(), A.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(D2.class, A.class, 0)); + + assertEquals(GroupItem1.class, + Reflection.getGenericClass(new D1().getClass(), C.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(D1.class, C.class, 0)); + assertEquals(GroupItem2.class, + Reflection.getGenericClass(new D2().getClass(), C.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(D2.class, C.class, 0)); + + assertEquals(Item1.class, Reflection.getGenericClass(BB.class, AA.class, 0)); + assertEquals(Item2.class, Reflection.getGenericClass(BB.class, AA.class, 1)); + + assertEquals(Item1.class, Reflection.getGenericClass(BI1.class, I.class, 0)); + assertEquals(Item2.class, Reflection.getGenericClass(BI2.class, I.class, 0)); + assertEquals(Item1.class, Reflection.getGenericClass(BI1.class, AI.class, 0)); + assertEquals(Item2.class, Reflection.getGenericClass(BI2.class, AI.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(BIG1.class, I.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(BIG2.class, I.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(BIG1.class, AI.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(BIG2.class, AI.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(CIG1.class, I.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(CIG2.class, I.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(CIG1.class, AI.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(CIG2.class, AI.class, 0)); + assertEquals(GroupItem1.class, Reflection.getGenericClass(CIG1.class, CIG.class, 0)); + assertEquals(GroupItem2.class, Reflection.getGenericClass(CIG2.class, CIG.class, 0)); + + assertEquals(null, Reflection.getGenericClass(Item1ToItem2.class, A.class, 0)); + assertEquals(Item2.class, + Reflection.getGenericClass(Item1ToItem2.class, StrangeExtend.class, 0)); + + assertEquals(Item1.class, Reflection.getGenericClass(BB.class, AA.class, 0)); + assertEquals(Item2.class, Reflection.getGenericClass(BB.class, AA.class, 1)); + } + + public void testResolveGenericCollections() { + List itemList = new ArrayList() { + private static final long serialVersionUID = 1L; + }; + Set itemSet = new HashSet() { + private static final long serialVersionUID = 1L; + }; + Map itemMap = new HashMap() { + private static final long serialVersionUID = 1L; + }; + List intList = new ArrayList() { + private static final long serialVersionUID = 1L; + }; + Set intSet = new HashSet() { + private static final long serialVersionUID = 1L; + }; + + assertEquals(Item1.class, + Reflection.getGenericClass(itemList.getClass(), Collection.class, 0)); + assertEquals(Item1.class, Reflection.getGenericClass(itemList.getClass(), List.class, 0)); + assertEquals(Item1.class, + Reflection.getGenericClass(itemList.getClass(), AbstractCollection.class, 0)); + assertEquals(Item1.class, + Reflection.getGenericClass(itemList.getClass(), AbstractList.class, 0)); + + assertEquals(Item1.class, + Reflection.getGenericClass(itemSet.getClass(), Collection.class, 0)); + assertEquals(Item1.class, Reflection.getGenericClass(itemSet.getClass(), Set.class, 0)); + assertEquals(Item1.class, + Reflection.getGenericClass(itemSet.getClass(), AbstractCollection.class, 0)); + assertEquals(Item1.class, + Reflection.getGenericClass(itemSet.getClass(), AbstractSet.class, 0)); + + assertEquals(String.class, Reflection.getGenericClass(itemMap.getClass(), Map.class, 0)); + assertEquals(Item1.class, Reflection.getGenericClass(itemMap.getClass(), Map.class, 1)); + assertEquals(String.class, + Reflection.getGenericClass(itemMap.getClass(), AbstractMap.class, 0)); + assertEquals(Item1.class, + Reflection.getGenericClass(itemMap.getClass(), AbstractMap.class, 1)); + + assertEquals(Integer.class, + Reflection.getGenericClass(intList.getClass(), Collection.class, 0)); + assertEquals(Integer.class, Reflection.getGenericClass(intList.getClass(), List.class, 0)); + assertEquals(Integer.class, + Reflection.getGenericClass(intList.getClass(), AbstractCollection.class, 0)); + assertEquals(Integer.class, + Reflection.getGenericClass(intList.getClass(), AbstractList.class, 0)); + + assertEquals(Integer.class, + Reflection.getGenericClass(intSet.getClass(), Collection.class, 0)); + assertEquals(Integer.class, Reflection.getGenericClass(intSet.getClass(), Set.class, 0)); + assertEquals(Integer.class, + Reflection.getGenericClass(intSet.getClass(), AbstractCollection.class, 0)); + assertEquals(Integer.class, + Reflection.getGenericClass(intSet.getClass(), AbstractSet.class, 0)); + + // Unsolvable + assertEquals(null, + Reflection.getGenericClass(new ArrayList().getClass(), List.class, 0)); + } + +} diff --git a/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/util/UtilTest.java b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/util/UtilTest.java new file mode 100644 index 000000000..e671fdb6f --- /dev/null +++ b/Squeezer/src/instrumentTest/java/uk/org/ngo/squeezer/test/util/UtilTest.java @@ -0,0 +1,116 @@ +package uk.org.ngo.squeezer.test.util; + +import junit.framework.TestCase; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Song; + +public class UtilTest extends TestCase { + + public void testAtomicReferenceUpdated() { + AtomicReference atomicString = new AtomicReference(); + assertFalse(Util.atomicReferenceUpdated(atomicString, null)); + assertEquals(null, atomicString.get()); + assertTrue(Util.atomicReferenceUpdated(atomicString, "test")); + assertEquals("test", atomicString.get()); + assertFalse(Util.atomicReferenceUpdated(atomicString, "test")); + assertEquals("test", atomicString.get()); + assertTrue(Util.atomicReferenceUpdated(atomicString, "change")); + assertEquals("change", atomicString.get()); + assertTrue(Util.atomicReferenceUpdated(atomicString, null)); + assertEquals(null, atomicString.get()); + assertTrue(Util.atomicReferenceUpdated(atomicString, "change")); + assertEquals("change", atomicString.get()); + assertTrue(Util.atomicReferenceUpdated(atomicString, null)); + assertEquals(null, atomicString.get()); + assertFalse(Util.atomicReferenceUpdated(atomicString, null)); + assertEquals(null, atomicString.get()); + + AtomicReference atomicItem = new AtomicReference(); + Album album = new Album("1", "album"); + Song song = new Song(new HashMap()); + song.setId("1"); + + assertFalse(Util.atomicReferenceUpdated(atomicItem, null)); + assertEquals(null, atomicItem.get()); + assertTrue(Util.atomicReferenceUpdated(atomicItem, album)); + assertEquals(album, atomicItem.get()); + + album.setName("newname"); + assertFalse(Util.atomicReferenceUpdated(atomicItem, album)); + assertEquals(album, atomicItem.get()); + + assertTrue(Util.atomicReferenceUpdated(atomicItem, song)); + assertEquals(song, atomicItem.get()); + + album.setId("2"); + assertTrue(Util.atomicReferenceUpdated(atomicItem, album)); + assertEquals(album, atomicItem.get()); + + assertTrue(Util.atomicReferenceUpdated(atomicItem, null)); + assertEquals(null, atomicItem.get()); + } + + public void testParseInt() { + assertEquals(2, Util.parseDecimalIntOrZero("2")); + assertEquals(0, Util.parseDecimalIntOrZero("2x")); + assertEquals(2, Util.parseDecimalIntOrZero("2.0")); + assertEquals(2, Util.parseDecimalIntOrZero("2.9")); + assertEquals(0, Util.parseDecimalIntOrZero(null)); + assertEquals(-2, Util.parseDecimalIntOrZero("-2")); + assertEquals(0, Util.parseDecimalIntOrZero("2,0")); + + assertEquals(123456789, Util.parseDecimalInt("123456789", -1)); + assertEquals(-1, Util.parseDecimalInt("0x8", -1)); + } + + public void testTimeString() { + assertEquals("0:00", Util.makeTimeString(0)); + assertEquals("0:01", Util.makeTimeString(1)); + assertEquals("0:10", Util.makeTimeString(10)); + assertEquals("0:59", Util.makeTimeString(59)); + assertEquals("1:00", Util.makeTimeString(60)); + assertEquals("1:01", Util.makeTimeString(61)); + assertEquals("1:59", Util.makeTimeString(119)); + assertEquals("2:00", Util.makeTimeString(120)); + assertEquals("2:01", Util.makeTimeString(121)); + assertEquals("18:39", Util.makeTimeString(1119)); + assertEquals("19:59", Util.makeTimeString(1199)); + assertEquals("20:00", Util.makeTimeString(1200)); + assertEquals("20:01", Util.makeTimeString(1201)); + assertEquals("20:11", Util.makeTimeString(1211)); + } + + public void testEncoding() { + assertEquals("test", Util.decode("test")); + assertEquals("test", Util.encode("test")); + assertEquals("test", Util.decode(Util.encode("test"))); + + assertEquals("test:test", Util.decode("test%3Atest")); + assertEquals("test%3Atest", Util.encode("test:test")); + assertEquals("test:test", Util.decode(Util.encode("test:test"))); + + assertEquals("test test", Util.decode("test%20test")); + assertEquals("test%20test", Util.encode("test test")); + assertEquals("test test", Util.decode(Util.encode("test test"))); + + assertEquals("test:æøåÆØÅ'éüõÛ-_/ ;.test", Util.decode( + "test%3A%C3%A6%C3%B8%C3%A5%C3%86%C3%98%C3%85%27%C3%A9%C3%BC%C3%B5%C3%9B-_%2F%20%3B.test")); + assertEquals( + "test%3A%C3%A6%C3%B8%C3%A5%C3%86%C3%98%C3%85%27%C3%A9%C3%BC%C3%B5%C3%9B-_%2F%20%3B.test", + Util.encode("test:æøåÆØÅ'éüõÛ-_/ ;.test")); + assertEquals("test:æøåÆØÅ'éüũÛ-_/ ;.test", + Util.decode(Util.encode("test:æøåÆØÅ'éüũÛ-_/ ;.test"))); + + // Apparently LMS doesn't encode all the characters our version does, but luckily we still decode correctly + assertEquals("album:#1's", Util.decode("album%3A%231's")); + assertEquals("album:100 80'er hits (disc 1)", + Util.decode("album%3A100%2080'er%20hits%20(disc%201)")); + } + +} diff --git a/Squeezer/src/instrumentTest/res/drawable-hdpi/ic_launcher.png b/Squeezer/src/instrumentTest/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..96a442e5b Binary files /dev/null and b/Squeezer/src/instrumentTest/res/drawable-hdpi/ic_launcher.png differ diff --git a/Squeezer/src/instrumentTest/res/drawable-ldpi/ic_launcher.png b/Squeezer/src/instrumentTest/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..99238729d Binary files /dev/null and b/Squeezer/src/instrumentTest/res/drawable-ldpi/ic_launcher.png differ diff --git a/Squeezer/src/instrumentTest/res/drawable-mdpi/ic_launcher.png b/Squeezer/src/instrumentTest/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..359047dfa Binary files /dev/null and b/Squeezer/src/instrumentTest/res/drawable-mdpi/ic_launcher.png differ diff --git a/Squeezer/src/instrumentTest/res/drawable-xhdpi/ic_launcher.png b/Squeezer/src/instrumentTest/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..71c6d760f Binary files /dev/null and b/Squeezer/src/instrumentTest/res/drawable-xhdpi/ic_launcher.png differ diff --git a/Squeezer/src/instrumentTest/res/values/strings.xml b/Squeezer/src/instrumentTest/res/values/strings.xml new file mode 100644 index 000000000..bd8131894 --- /dev/null +++ b/Squeezer/src/instrumentTest/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + SqueezerTestTest + + diff --git a/Squeezer/src/main/AndroidManifest.xml b/Squeezer/src/main/AndroidManifest.xml new file mode 100644 index 000000000..47bd6b546 --- /dev/null +++ b/Squeezer/src/main/AndroidManifest.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceCallback.aidl new file mode 100644 index 000000000..e033c6ee0 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceCallback.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import uk.org.ngo.squeezer.model.Player; + +oneway interface IServiceCallback { + // Empty strings to denote no default player. + void onPlayerChanged(in Player player); + + // postConnect is only true for the very first callback after a new initial connect. + // loginFailed is true if the server disconnects before handshaking is completed + void onConnectionChanged(boolean isConnected, boolean postConnect, boolean loginFailed); + + void onPlayStatusChanged(String playStatus); + void onShuffleStatusChanged(boolean initial, int shuffleStatus); + void onRepeatStatusChanged(boolean initial, int repeatStatus); + void onTimeInSongChange(int secondsIn, int secondsTotal); + void onPowerStatusChanged(boolean canPowerOn, boolean canPowerOff); +} + diff --git a/src/uk/org/ngo/squeezer/model/SqueezerArtist.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceHandshakeCallback.aidl similarity index 72% rename from src/uk/org/ngo/squeezer/model/SqueezerArtist.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceHandshakeCallback.aidl index 8a18c5f3e..430e1b90f 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerArtist.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceHandshakeCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Kurt Aaholst + * Copyright (c) 2009 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,5 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.model; -parcelable SqueezerArtist; +package uk.org.ngo.squeezer; + +oneway interface IServiceHandshakeCallback { + // Handshaking with the server has completed. + void onHandshakeCompleted(); +} + diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceMusicChangedCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceMusicChangedCallback.aidl new file mode 100644 index 000000000..cf46db6dd --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceMusicChangedCallback.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import uk.org.ngo.squeezer.model.PlayerState; + +oneway interface IServiceMusicChangedCallback { + void onMusicChanged(in PlayerState playerState); +} diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceVolumeCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceVolumeCallback.aidl new file mode 100644 index 000000000..3a41e818d --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/IServiceVolumeCallback.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2013 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import uk.org.ngo.squeezer.model.Player; + +oneway interface IServiceVolumeCallback { + void onVolumeChanged(int newVolume, in Player player); +} diff --git a/src/uk/org/ngo/squeezer/itemlists/IServiceAlbumListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceAlbumListCallback.aidl similarity index 80% rename from src/uk/org/ngo/squeezer/itemlists/IServiceAlbumListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceAlbumListCallback.aidl index de4056cd0..368c38c94 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServiceAlbumListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceAlbumListCallback.aidl @@ -14,10 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerAlbum; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Album; oneway interface IServiceAlbumListCallback { - void onAlbumsReceived(int count, int start, in List albums); + void onAlbumsReceived(int count, int start, in List albums); } diff --git a/src/uk/org/ngo/squeezer/itemlists/IServiceArtistListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceArtistListCallback.aidl similarity index 79% rename from src/uk/org/ngo/squeezer/itemlists/IServiceArtistListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceArtistListCallback.aidl index 1c62739e6..94f5bb046 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServiceArtistListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceArtistListCallback.aidl @@ -14,10 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerArtist; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Artist; oneway interface IServiceArtistListCallback { - void onArtistsReceived(int count, int start, in List artists); + void onArtistsReceived(int count, int start, in List artists); } diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceCurrentPlaylistCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceCurrentPlaylistCallback.aidl new file mode 100644 index 000000000..2ddf55863 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceCurrentPlaylistCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2013 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.itemlist; + +import uk.org.ngo.squeezer.model.PlayerState; + +oneway interface IServiceCurrentPlaylistCallback { + void onAddTracks(in PlayerState playerState); + void onDelete(in PlayerState playerState, int index); +} \ No newline at end of file diff --git a/src/uk/org/ngo/squeezer/itemlists/IServiceGenreListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceGenreListCallback.aidl similarity index 80% rename from src/uk/org/ngo/squeezer/itemlists/IServiceGenreListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceGenreListCallback.aidl index d8e9e8430..d4058da41 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServiceGenreListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceGenreListCallback.aidl @@ -14,9 +14,9 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerGenre; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Genre; oneway interface IServiceGenreListCallback { - void onGenresReceived(int count, int pos, in List albums); + void onGenresReceived(int count, int pos, in List genres); } \ No newline at end of file diff --git a/src/uk/org/ngo/squeezer/itemlists/IServiceMusicFolderListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceMusicFolderListCallback.aidl similarity index 76% rename from src/uk/org/ngo/squeezer/itemlists/IServiceMusicFolderListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceMusicFolderListCallback.aidl index affcd112b..6a33711ba 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServiceMusicFolderListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceMusicFolderListCallback.aidl @@ -14,9 +14,9 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerMusicFolderItem; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.MusicFolderItem; oneway interface IServiceMusicFolderListCallback { - void onMusicFoldersReceived(int count, int start, in List musicfolders); -} \ No newline at end of file + void onMusicFoldersReceived(int count, int start, in List musicfolders); +} diff --git a/src/uk/org/ngo/squeezer/itemlists/IServicePlayerListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlayerListCallback.aidl similarity index 80% rename from src/uk/org/ngo/squeezer/itemlists/IServicePlayerListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlayerListCallback.aidl index 874656e7c..5061638fc 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServicePlayerListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlayerListCallback.aidl @@ -14,9 +14,9 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerPlayer; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Player; oneway interface IServicePlayerListCallback { - void onPlayersReceived(int count, int pos, in List players); + void onPlayersReceived(int count, int pos, in List players); } \ No newline at end of file diff --git a/src/uk/org/ngo/squeezer/itemlists/IServicePlaylistMaintenanceCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlaylistMaintenanceCallback.aidl similarity index 81% rename from src/uk/org/ngo/squeezer/itemlists/IServicePlaylistMaintenanceCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlaylistMaintenanceCallback.aidl index f3c27e79f..4c398655e 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServicePlaylistMaintenanceCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlaylistMaintenanceCallback.aidl @@ -14,10 +14,9 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerSong; +package uk.org.ngo.squeezer.itemlist; oneway interface IServicePlaylistMaintenanceCallback { - void onRenameFailed(String msg); - void onCreateFailed(String msg); + void onRenameFailed(String msg); + void onCreateFailed(String msg); } \ No newline at end of file diff --git a/src/uk/org/ngo/squeezer/itemlists/IServicePlaylistsCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlaylistsCallback.aidl similarity index 79% rename from src/uk/org/ngo/squeezer/itemlists/IServicePlaylistsCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlaylistsCallback.aidl index 073985797..def42a805 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServicePlaylistsCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePlaylistsCallback.aidl @@ -14,9 +14,9 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerPlaylist; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Playlist; oneway interface IServicePlaylistsCallback { - void onPlaylistsReceived(int count, int pos, in List albums); + void onPlaylistsReceived(int count, int pos, in List albums); } \ No newline at end of file diff --git a/src/uk/org/ngo/squeezer/itemlists/IServicePluginItemListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePluginItemListCallback.aidl similarity index 77% rename from src/uk/org/ngo/squeezer/itemlists/IServicePluginItemListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePluginItemListCallback.aidl index d13a2fba4..28da7151f 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServicePluginItemListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePluginItemListCallback.aidl @@ -14,10 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerPluginItem; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.PluginItem; oneway interface IServicePluginItemListCallback { - void onPluginItemsReceived(int count, int pos, in Map parameters, in List albums); + void onPluginItemsReceived(int count, int pos, in Map parameters, in List albums); } diff --git a/src/uk/org/ngo/squeezer/itemlists/IServicePluginListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePluginListCallback.aidl similarity index 80% rename from src/uk/org/ngo/squeezer/itemlists/IServicePluginListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePluginListCallback.aidl index c6fe7c571..39ce4d91a 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServicePluginListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServicePluginListCallback.aidl @@ -14,10 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerPlugin; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Plugin; oneway interface IServicePluginListCallback { - void onPluginsReceived(int count, int pos, in List albums); + void onPluginsReceived(int count, int pos, in List albums); } diff --git a/src/uk/org/ngo/squeezer/itemlists/IServiceSongListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceSongListCallback.aidl similarity index 80% rename from src/uk/org/ngo/squeezer/itemlists/IServiceSongListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceSongListCallback.aidl index bab3c9cbb..0086af190 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServiceSongListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceSongListCallback.aidl @@ -14,10 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerSong; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Song; oneway interface IServiceSongListCallback { - void onSongsReceived(int count, int pos, in List songs); + void onSongsReceived(int count, int pos, in List songs); } diff --git a/src/uk/org/ngo/squeezer/itemlists/IServiceYearListCallback.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceYearListCallback.aidl similarity index 80% rename from src/uk/org/ngo/squeezer/itemlists/IServiceYearListCallback.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceYearListCallback.aidl index f10773636..917e0a452 100644 --- a/src/uk/org/ngo/squeezer/itemlists/IServiceYearListCallback.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/itemlist/IServiceYearListCallback.aidl @@ -14,10 +14,10 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; -import uk.org.ngo.squeezer.model.SqueezerYear; +package uk.org.ngo.squeezer.itemlist; +import uk.org.ngo.squeezer.model.Year; oneway interface IServiceYearListCallback { - void onYearsReceived(int count, int pos, in List albums); + void onYearsReceived(int count, int pos, in List years); } diff --git a/src/uk/org/ngo/squeezer/model/SqueezerSong.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Album.aidl similarity index 96% rename from src/uk/org/ngo/squeezer/model/SqueezerSong.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Album.aidl index bcb9fe05e..e90a7d004 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerSong.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Album.aidl @@ -15,4 +15,4 @@ */ package uk.org.ngo.squeezer.model; -parcelable SqueezerSong; +parcelable Album; diff --git a/src/uk/org/ngo/squeezer/model/SqueezerYear.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Artist.aidl similarity index 96% rename from src/uk/org/ngo/squeezer/model/SqueezerYear.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Artist.aidl index 7e0c50331..7a872f50c 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerYear.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Artist.aidl @@ -15,4 +15,4 @@ */ package uk.org.ngo.squeezer.model; -parcelable SqueezerYear; +parcelable Artist; diff --git a/src/uk/org/ngo/squeezer/model/SqueezerGenre.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Genre.aidl similarity index 96% rename from src/uk/org/ngo/squeezer/model/SqueezerGenre.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Genre.aidl index 7b7a0f223..698dfa73c 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerGenre.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Genre.aidl @@ -15,4 +15,4 @@ */ package uk.org.ngo.squeezer.model; -parcelable SqueezerGenre; +parcelable Genre; diff --git a/src/uk/org/ngo/squeezer/model/SqueezerMusicFolderItem.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/MusicFolderItem.aidl similarity index 94% rename from src/uk/org/ngo/squeezer/model/SqueezerMusicFolderItem.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/MusicFolderItem.aidl index a76d125e6..862ecd4ff 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerMusicFolderItem.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/MusicFolderItem.aidl @@ -15,4 +15,4 @@ */ package uk.org.ngo.squeezer.model; -parcelable SqueezerMusicFolderItem; \ No newline at end of file +parcelable MusicFolderItem; diff --git a/src/uk/org/ngo/squeezer/model/SqueezerAlbum.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Player.aidl similarity index 96% rename from src/uk/org/ngo/squeezer/model/SqueezerAlbum.aidl rename to Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Player.aidl index cfa2b0393..d75afbe09 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerAlbum.aidl +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Player.aidl @@ -15,4 +15,4 @@ */ package uk.org.ngo.squeezer.model; -parcelable SqueezerAlbum; +parcelable Player; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/PlayerState.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/PlayerState.aidl new file mode 100644 index 000000000..270360b00 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/PlayerState.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; +parcelable PlayerState; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Playlist.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Playlist.aidl new file mode 100644 index 000000000..decf6a83b --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Playlist.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; +parcelable Playlist; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Plugin.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Plugin.aidl new file mode 100644 index 000000000..4639d6189 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Plugin.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; +parcelable Plugin; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/PluginItem.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/PluginItem.aidl new file mode 100644 index 000000000..96ed07c09 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/PluginItem.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; +parcelable PluginItem; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Song.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Song.aidl new file mode 100644 index 000000000..ace0d1da3 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Song.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; +parcelable Song; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Year.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Year.aidl new file mode 100644 index 000000000..17671ce56 --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/model/Year.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; +parcelable Year; diff --git a/Squeezer/src/main/aidl/uk/org/ngo/squeezer/service/ISqueezeService.aidl b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/service/ISqueezeService.aidl new file mode 100644 index 000000000..2204192df --- /dev/null +++ b/Squeezer/src/main/aidl/uk/org/ngo/squeezer/service/ISqueezeService.aidl @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.service; + +import uk.org.ngo.squeezer.IServiceCallback; +import uk.org.ngo.squeezer.IServiceMusicChangedCallback; +import uk.org.ngo.squeezer.IServiceHandshakeCallback; +import uk.org.ngo.squeezer.IServiceVolumeCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlayerListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceAlbumListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceArtistListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceCurrentPlaylistCallback; +import uk.org.ngo.squeezer.itemlist.IServiceYearListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceGenreListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceMusicFolderListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceSongListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlaylistsCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlaylistMaintenanceCallback; +import uk.org.ngo.squeezer.itemlist.IServicePluginListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePluginItemListCallback; +import uk.org.ngo.squeezer.model.PlayerState; +import uk.org.ngo.squeezer.model.Player; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Year; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Playlist; +import uk.org.ngo.squeezer.model.Plugin; +import uk.org.ngo.squeezer.model.PluginItem; + +interface ISqueezeService { + // For the activity to get callbacks on interesting events + void registerCallback(IServiceCallback callback); + void unregisterCallback(IServiceCallback callback); + + // For the activity to get callback when the current playlist is modified + void registerCurrentPlaylistCallback(IServiceCurrentPlaylistCallback callback); + void unregisterCurrentPlaylistCallback(IServiceCurrentPlaylistCallback callback); + + // For the activity to get callback when music changes + void registerMusicChangedCallback(IServiceMusicChangedCallback callback); + void unregisterMusicChangedCallback(IServiceMusicChangedCallback callback); + + // For the activity to get callback when handshake completes + void registerHandshakeCallback(IServiceHandshakeCallback callback); + void unregisterHandshakeCallback(IServiceHandshakeCallback callback); + + // For the activity to get callback when the volume changes. + void registerVolumeCallback(IServiceVolumeCallback callback); + void unregisterVolumeCallback(IServiceVolumeCallback callback); + + // Instructing the service to connect to the SqueezeCenter server: + // hostPort is the port of the CLI interface. + void startConnect(String hostPort, String userName, String password); + void disconnect(); + boolean isConnected(); + boolean isConnectInProgress(); + + // For the SettingsActivity to notify the Service that a setting changed. + void preferenceChanged(String key); + + // Call this to change the player we are controlling + void setActivePlayer(in Player player); + + // Returns the player we are currently controlling + Player getActivePlayer(); + + //////////////////// + // Depends on active player: + + boolean canPowerOn(); + boolean canPowerOff(); + boolean powerOn(); + boolean powerOff(); + boolean canMusicfolder(); + boolean canRandomplay(); + String preferredAlbumSort(); + boolean togglePausePlay(); + boolean play(); + boolean stop(); + boolean nextTrack(); + boolean previousTrack(); + boolean toggleShuffle(); + boolean toggleRepeat(); + boolean playlistControl(String cmd, String className, String id); + boolean randomPlay(String type); + boolean playlistIndex(int index); + boolean playlistRemove(int index); + boolean playlistMove(int fromIndex, int toIndex); + boolean playlistClear(); + boolean playlistSave(String name); + boolean pluginPlaylistControl(in Plugin plugin, String cmd, String id); + + boolean setSecondsElapsed(int seconds); + + PlayerState getPlayerState(); + String getCurrentPlaylist(); + String getAlbumArtUrl(String artworkTrackId); + String getIconUrl(String icon); + + String getSongDownloadUrl(String songTrackId); + + /** + * Sets the volume to the absolute volume in newVolume, which will be clamped to the + * interval [0, 100]. + * + * @param newVolume + * @throws RemoteException + */ + void adjustVolumeTo(int newVolume); + void adjustVolumeBy(int delta); + + // Player list + boolean players(int start); + void registerPlayerListCallback(IServicePlayerListCallback callback); + void unregisterPlayerListCallback(IServicePlayerListCallback callback); + + // Album list + /** + * Starts an asynchronous fetch of album data from the server. Any + * callback registered with {@link registerAlbumListCallBack} will + * be called when the data is fetched. + * + * @param start + * @param sortOrder + * @param searchString + * @param artist + * @param year + * @param genre + */ + boolean albums(int start, String sortOrder, String searchString, in Artist artist, in Year year, in Genre genre, in Song song); + void registerAlbumListCallback(IServiceAlbumListCallback callback); + void unregisterAlbumListCallback(IServiceAlbumListCallback callback); + + // Artist list + boolean artists(int start, String searchString, in Album album, in Genre genre); + void registerArtistListCallback(IServiceArtistListCallback callback); + void unregisterArtistListCallback(IServiceArtistListCallback callback); + + // Year list + boolean years(int start); + void registerYearListCallback(IServiceYearListCallback callback); + void unregisterYearListCallback(IServiceYearListCallback callback); + + // Genre list + boolean genres(int start, String searchString); + void registerGenreListCallback(IServiceGenreListCallback callback); + void unregisterGenreListCallback(IServiceGenreListCallback callback); + + // MusicFolder list + boolean musicFolders(int start, String folderId); + void registerMusicFolderListCallback(IServiceMusicFolderListCallback callback); + void unregisterMusicFolderListCallback(IServiceMusicFolderListCallback callback); + + // Song list + boolean songs(int start, String sortOrder, String searchString, in Album album, in Artist artist, in Year year, in Genre genre); + boolean currentPlaylist(int start); + boolean playlistSongs(int start, in Playlist playlist); + void registerSongListCallback(IServiceSongListCallback callback); + void unregisterSongListCallback(IServiceSongListCallback callback); + + // Playlists + boolean playlists(int start); + void registerPlaylistsCallback(IServicePlaylistsCallback callback); + void unregisterPlaylistsCallback(IServicePlaylistsCallback callback); + + // Named playlist maintenance + void registerPlaylistMaintenanceCallback(IServicePlaylistMaintenanceCallback callback); + void unregisterPlaylistMaintenanceCallback(IServicePlaylistMaintenanceCallback callback); + boolean playlistsNew(String name); + boolean playlistsRename(in Playlist playlist, String newname); + boolean playlistsDelete(in Playlist playlist); + boolean playlistsMove(in Playlist playlist, int index, int toindex); + boolean playlistsRemove(in Playlist playlist, int index); + + // Search + boolean search(int start, String searchString); + + // Radios/plugins + boolean radios(int start); + boolean apps(int start); + void registerPluginListCallback(IServicePluginListCallback callback); + void unregisterPluginListCallback(IServicePluginListCallback callback); + + boolean pluginItems(int start, in Plugin plugin, in PluginItem parent, String search); + void registerPluginItemListCallback(IServicePluginItemListCallback callback); + void unregisterPluginItemListCallback(IServicePluginItemListCallback callback); +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/DisconnectedActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/DisconnectedActivity.java new file mode 100644 index 000000000..c330b8750 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/DisconnectedActivity.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2013 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; +import android.widget.Button; + +import uk.org.ngo.squeezer.framework.BaseActivity; +import uk.org.ngo.squeezer.model.Player; + +/** + * An activity for when the user is not connected to a Squeezeserver. + *

+ * Provide a UI for connecting to the configured server, launch HomeActivity when the user + * connects. + */ +public class DisconnectedActivity extends BaseActivity { + + private final String TAG = "DisconnectedActivity"; + + /** + * Keep track of whether callbacks have been registered + */ + private boolean mRegisteredCallbacks; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.disconnected); + + Button btnConnect = (Button) findViewById(R.id.btn_connect); + String ipPort = getSharedPreferences(Preferences.NAME, Context.MODE_PRIVATE).getString( + Preferences.KEY_SERVERADDR, null); + btnConnect.setText(getString(R.string.connect_to_text, ipPort)); + } + + @Override + public void onResume() { + super.onResume(); + if (getService() != null) { + maybeRegisterCallbacks(); + } + } + + @Override + public void onPause() { + if (mRegisteredCallbacks) { + try { + getService().unregisterCallback(serviceCallback); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in onPause(): " + e); + } + mRegisteredCallbacks = false; + } + super.onPause(); + } + + /** + * Show this activity. + *

+ * Flags are set to clear the previous activities, as trying to go back while disconnected makes + * no sense. + *

+ * The pending transition is overridden to animate the activity in place, rather than having it + * appear to move in from off-screen. + * + * @param activity + */ + public static void show(Activity activity) { + final Intent intent = new Intent(activity, DisconnectedActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } + + /** + * Act on the user requesting a server connection through the activity's UI. + * + * @param view The view the user pressed. + */ + public void onUserInitiatesConnect(View view) { + NowPlayingFragment fragment = (NowPlayingFragment) getSupportFragmentManager() + .findFragmentById( + R.id.now_playing_fragment); + fragment.startVisibleConnection(); + } + + @Override + protected void onServiceConnected() { + maybeRegisterCallbacks(); + } + + /** + * Register callbacks with the server, if not already registered. + *

+ * This is called when the service is first connected, and whenever the activity is resumed. + */ + private void maybeRegisterCallbacks() { + if (!mRegisteredCallbacks) { + try { + getService().registerCallback(serviceCallback); + } catch (RemoteException e) { + Log.e(getTag(), "Error registering callback: " + e); + } + mRegisteredCallbacks = true; + } + } + + private final IServiceCallback serviceCallback = new IServiceCallback.Stub() { + // TODO: Maybe move onConnectionChanged to its own callback. + + @Override + public void onConnectionChanged(final boolean isConnected, final boolean postConnect, + final boolean loginFailed) + throws RemoteException { + if (isConnected) { + // The user requested a connection to the server, which succeeded. There's + // no prior activity to go to, so launch HomeActivity, with flags to + // clear other activities so hitting "back" won't show this activity again. + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + final Intent intent = new Intent(DisconnectedActivity.this, + HomeActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + DisconnectedActivity.this.startActivity(intent); + DisconnectedActivity.this.overridePendingTransition(android.R.anim.fade_in, + android.R.anim.fade_out); + } + }); + } + } + + @Override + public void onPlayerChanged(Player player) throws RemoteException { + } + + @Override + public void onPlayStatusChanged(final String playStatus) { + } + + @Override + public void onShuffleStatusChanged(final boolean initial, final int shuffleStatus) { + } + + @Override + public void onRepeatStatusChanged(final boolean initial, final int repeatStatus) { + } + + @Override + public void onTimeInSongChange(final int secondsIn, final int secondsTotal) + throws RemoteException { + } + + @Override + public void onPowerStatusChanged(final boolean canPowerOn, final boolean canPowerOff) + throws RemoteException { + } + }; +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/HomeActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/HomeActivity.java new file mode 100644 index 000000000..2fa1e8065 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/HomeActivity.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + + +import com.google.android.apps.analytics.GoogleAnalyticsTracker; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import de.cketti.library.changelog.ChangeLog; +import uk.org.ngo.squeezer.dialog.TipsDialog; +import uk.org.ngo.squeezer.framework.BaseActivity; +import uk.org.ngo.squeezer.itemlist.AlbumListActivity; +import uk.org.ngo.squeezer.itemlist.ArtistListActivity; +import uk.org.ngo.squeezer.itemlist.FavoriteListActivity; +import uk.org.ngo.squeezer.itemlist.GenreListActivity; +import uk.org.ngo.squeezer.itemlist.MusicFolderListActivity; +import uk.org.ngo.squeezer.itemlist.PlaylistsActivity; +import uk.org.ngo.squeezer.itemlist.RadioListActivity; +import uk.org.ngo.squeezer.itemlist.SongListActivity; +import uk.org.ngo.squeezer.itemlist.YearListActivity; +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog; + +public class HomeActivity extends BaseActivity { + + private final String TAG = "HomeActivity"; + + private static final int ARTISTS = 0; + + private static final int ALBUMS = 1; + + private static final int SONGS = 2; + + private static final int GENRES = 3; + + private static final int YEARS = 4; + + private static final int NEW_MUSIC = 5; + + private static final int MUSIC_FOLDER = 6; + + private static final int RANDOM_MIX = 7; + + private static final int PLAYLISTS = 8; + + private static final int INTERNET_RADIO = 9; + + private static final int FAVORITES = 10; + + private static final int APPS = 11; + + private boolean mRegisteredCallbacks; + + private boolean mCanMusicfolder = false; + + private boolean mCanRandomplay = false; + + private ListView listView; + + private GoogleAnalyticsTracker tracker; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.item_list); + listView = (ListView) findViewById(R.id.item_list); + + final SharedPreferences preferences = getSharedPreferences(Preferences.NAME, 0); + + // Enable Analytics if the option is on, and we're not running in debug + // mode so that debug tests don't pollute the stats. + if (preferences.getBoolean(Preferences.KEY_ANALYTICS_ENABLED, true)) { + if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) { + Log.v("NowPlayingActivity", "Tracking page view 'HomeActivity"); + // Start the tracker in manual dispatch mode... + tracker = GoogleAnalyticsTracker.getInstance(); + tracker.startNewSession("UA-26457780-1", this); + tracker.trackPageView("HomeActivity"); + } + } + + // Show the change log if necessary. + ChangeLog changeLog = new ChangeLog(this); + if (changeLog.isFirstRun()) { + changeLog.getLogDialog().show(); + } + + // Show a tip about volume controls, if this is the first time this app + // has run. TODO: Add more robust and general 'tips' functionality. + PackageInfo pInfo; + try { + pInfo = getPackageManager().getPackageInfo(getPackageName(), + PackageManager.GET_META_DATA); + if (preferences.getLong("lastRunVersionCode", 0) < pInfo.versionCode) { + new TipsDialog().show(getSupportFragmentManager(), "TipsDialog"); + SharedPreferences.Editor editor = preferences.edit(); + editor.putLong("lastRunVersionCode", pInfo.versionCode); + editor.commit(); + } + } catch (PackageManager.NameNotFoundException e) { + // Nothing to do, don't crash. + } + } + + @Override + protected void onServiceConnected() { + maybeRegisterCallbacks(); + } + + private void maybeRegisterCallbacks() { + if (!mRegisteredCallbacks) { + try { + getService().registerHandshakeCallback(mCallback); + } catch (RemoteException e) { + Log.e(getTag(), "Error registering callback: " + e); + } + mRegisteredCallbacks = true; + } + } + + + private final IServiceHandshakeCallback mCallback = new IServiceHandshakeCallback.Stub() { + + /** + * Sets the menu after handshaking with the SqueezeServer has completed. + *

+ * This is necessary because the service doesn't know whether the server + * supports music folder browsing and random play ability until the + * handshake completes, and the menu is adjusted depending on whether or + * not those abilities exist. + */ + @Override + public void onHandshakeCompleted() throws RemoteException { + runOnUiThread(new Runnable() { + @Override + public void run() { + createListItems(); + } + }); + } + + }; + + /** + * Creates the list of items to show in the activity. + *

+ * Must be run on the UI thread. + */ + private void createListItems() { + int[] icons = new int[]{ + R.drawable.ic_artists, + R.drawable.ic_albums, R.drawable.ic_songs, + R.drawable.ic_genres, R.drawable.ic_years, R.drawable.ic_new_music, + R.drawable.ic_music_folder, R.drawable.ic_random, + R.drawable.ic_playlists, R.drawable.ic_internet_radio, + R.drawable.ic_favorites, R.drawable.ic_my_apps + }; + + String[] items = getResources().getStringArray(R.array.home_items); + + if (getService() != null) { + try { + mCanMusicfolder = getService().canMusicfolder(); + } catch (RemoteException e) { + Log.e(getTag(), "Error requesting musicfolder ability: " + e); + } + } + + if (getService() != null) { + try { + mCanRandomplay = getService().canRandomplay(); + } catch (RemoteException e) { + Log.e(getTag(), "Error requesting randomplay ability: " + e); + } + } + + List rows = new ArrayList(); + for (int i = ARTISTS; i <= FAVORITES; i++) { + if (i == MUSIC_FOLDER && !mCanMusicfolder) { + continue; + } + + if (i == RANDOM_MIX && !mCanRandomplay) { + continue; + } + + if (i == APPS) { + continue; // APPS not implemented. + } + + rows.add(new IconRowAdapter.IconRow(i, items[i], icons[i])); + } + + listView.setAdapter(new IconRowAdapter(this, rows)); + listView.setOnItemClickListener(onHomeItemClick); + } + + private final OnItemClickListener onHomeItemClick = new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + + switch ((int) id) { + case ARTISTS: + ArtistListActivity.show(HomeActivity.this); + break; + case ALBUMS: + AlbumListActivity.show(HomeActivity.this); + break; + case SONGS: + SongListActivity.show(HomeActivity.this); + break; + case GENRES: + GenreListActivity.show(HomeActivity.this); + break; + case YEARS: + YearListActivity.show(HomeActivity.this); + break; + case NEW_MUSIC: + AlbumListActivity.show(HomeActivity.this, + AlbumViewDialog.AlbumsSortOrder.__new); + break; + case MUSIC_FOLDER: + MusicFolderListActivity.show(HomeActivity.this); + break; + case RANDOM_MIX: + RandomplayActivity.show(HomeActivity.this); + break; + case PLAYLISTS: + PlaylistsActivity.show(HomeActivity.this); + break; + case INTERNET_RADIO: + // Uncomment these next two lines as an easy way to check + // crash reporting functionality. + + // String sCrashString = null; + // Log.e("MyApp", sCrashString.toString()); + RadioListActivity.show(HomeActivity.this); + break; + case APPS: + // TODO (kaa) implement + // Currently hidden, by commenting out the entry in + // strings.xml. + // ApplicationListActivity.show(HomeActivity.this); + break; + case FAVORITES: + FavoriteListActivity.show(HomeActivity.this); + break; + } + } + }; + + @Override + public void onResume() { + super.onResume(); + if (getService() != null) { + maybeRegisterCallbacks(); + } + } + + @Override + public void onPause() { + if (mRegisteredCallbacks) { + if (getService() != null) { + try { + getService().unregisterHandshakeCallback(mCallback); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in onPause(): " + e); + } + } + mRegisteredCallbacks = false; + } + super.onPause(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + // Send analytics stats (if enabled). + if (tracker != null) { + tracker.dispatch(); + tracker.stopSession(); + } + } + + public static void show(Context context) { + final Intent intent = new Intent(context, HomeActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + context.startActivity(intent); + } + +} diff --git a/src/uk/org/ngo/squeezer/IconRowAdapter.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/IconRowAdapter.java similarity index 74% rename from src/uk/org/ngo/squeezer/IconRowAdapter.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/IconRowAdapter.java index c50bd0533..259407e64 100644 --- a/src/uk/org/ngo/squeezer/IconRowAdapter.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/IconRowAdapter.java @@ -16,9 +16,6 @@ package uk.org.ngo.squeezer; -import java.util.ArrayList; -import java.util.List; - import android.app.Activity; import android.view.View; import android.view.ViewGroup; @@ -26,42 +23,51 @@ import android.widget.ImageView; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; + /** * Simple list adapter to display corresponding lists of images and labels. * * @author Kurt Aaholst */ public class IconRowAdapter extends BaseAdapter { - private final Activity activity; - private final int rowLayout = R.layout.icon_large_row_layout; - private final int iconId = R.id.icon; - private final int textId = R.id.label; - /** Rows to display in the list. */ + private final Activity activity; + + private final int rowLayout = R.layout.list_item; + + private final int iconId = R.id.icon; + + private final int textId = R.id.text1; + + /** + * Rows to display in the list. + */ List mRows = new ArrayList(); - public int getCount() { + public int getCount() { return mRows.size(); - } + } - public int getImage(int position) { + public int getImage(int position) { return mRows.get(position).getIcon(); - } + } public CharSequence getItem(int position) { return mRows.get(position).getText(); - } + } - public long getItemId(int position) { + public long getItemId(int position) { return mRows.get(position).getId(); - } + } /** - * Creates an IconRowAdapter where the id of each item corresponds to its - * index in items. - *

+ * Creates an IconRowAdapter where the id of each item corresponds to its index in + * items. + *

* items and icons must be the same size. - * + * * @param context * @param items Item text. * @param images Image resources. @@ -77,41 +83,42 @@ public IconRowAdapter(Activity context, CharSequence[] items, int[] icons) { /** * Creates an IconRowAdapter from the list of rows. - * + * * @param context * @param rows Rows to appear in the list. */ public IconRowAdapter(Activity context, List rows) { this.activity = context; mRows = rows; - } + } - public View getView(int position, View convertView, ViewGroup parent) { - View row = getActivity().getLayoutInflater().inflate(rowLayout, null); - TextView label = (TextView) row.findViewById(textId); - ImageView icon = (ImageView) row.findViewById(iconId); + public View getView(int position, View convertView, ViewGroup parent) { + View row = getActivity().getLayoutInflater().inflate(rowLayout, parent, false); + TextView text1 = (TextView) row.findViewById(textId); + ImageView icon = (ImageView) row.findViewById(iconId); - label.setText(mRows.get(position).getText()); + text1.setText(mRows.get(position).getText()); icon.setImageResource(mRows.get(position).getIcon()); - return row; - } + return row; + } - public Activity getActivity() { - return activity; - } + public Activity getActivity() { + return activity; + } /** - * Helper class to represent a row. Each row has an identifier, a string, - * and an icon. - *

- * The identifier should be unique across all rows in a given - * {@link IconRowAdapter}, and will be used as the id paramter - * to the OnItemClickListener. + * Helper class to represent a row. Each row has an identifier, a string, and an icon. + *

+ * The identifier should be unique across all rows in a given {@link IconRowAdapter}, and will + * be used as the id parameter to the OnItemClickListener. */ public static class IconRow { + private long mId; + private CharSequence mText; + private int mIcon; IconRow(long id, CharSequence text, int icon) { @@ -144,4 +151,4 @@ public void setIcon(int icon) { mIcon = icon; } } -} \ No newline at end of file +} diff --git a/src/uk/org/ngo/squeezer/IntEditTextPreference.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/IntEditTextPreference.java similarity index 92% rename from src/uk/org/ngo/squeezer/IntEditTextPreference.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/IntEditTextPreference.java index 9eca86dda..cd45b2519 100644 --- a/src/uk/org/ngo/squeezer/IntEditTextPreference.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/IntEditTextPreference.java @@ -36,11 +36,11 @@ public IntEditTextPreference(Context context, AttributeSet attrs, int defStyle) @Override protected String getPersistedString(String defaultReturnValue) { - return String.valueOf(getPersistedInt(-1)); + return String.valueOf(getPersistedInt(0)); } @Override protected boolean persistString(String value) { - return persistInt(Integer.valueOf(value)); + return persistInt(Util.parseDecimalIntOrZero(value)); } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/NowPlayingActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/NowPlayingActivity.java new file mode 100644 index 000000000..971692b77 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/NowPlayingActivity.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import uk.org.ngo.squeezer.framework.BaseActivity; + +public class NowPlayingActivity extends BaseActivity { + + protected static final int HOME_REQUESTCODE = 0; + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.now_playing); + } + + /* + * (non-Javadoc) + * @see + * uk.org.ngo.squeezer.framework.BaseActivity#onServiceConnected() + */ + @Override + protected void onServiceConnected() { + // Does nothing + } + + public static void show(Context context) { + final Intent intent = new Intent(context, NowPlayingActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + context.startActivity(intent); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/NowPlayingFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/NowPlayingFragment.java new file mode 100644 index 000000000..69c76f948 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/NowPlayingFragment.java @@ -0,0 +1,1242 @@ +/* + * Copyright (c) 2012 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; +import android.widget.Toast; + +import java.lang.ref.WeakReference; + +import uk.org.ngo.squeezer.dialog.AboutDialog; +import uk.org.ngo.squeezer.dialog.AuthenticationDialog; +import uk.org.ngo.squeezer.dialog.EnableWifiDialog; +import uk.org.ngo.squeezer.framework.BaseActivity; +import uk.org.ngo.squeezer.framework.HasUiThread; +import uk.org.ngo.squeezer.itemlist.AlbumListActivity; +import uk.org.ngo.squeezer.itemlist.CurrentPlaylistActivity; +import uk.org.ngo.squeezer.itemlist.PlayerListActivity; +import uk.org.ngo.squeezer.itemlist.SongListActivity; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Player; +import uk.org.ngo.squeezer.model.PlayerState; +import uk.org.ngo.squeezer.model.PlayerState.PlayStatus; +import uk.org.ngo.squeezer.model.PlayerState.RepeatStatus; +import uk.org.ngo.squeezer.model.PlayerState.ShuffleStatus; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.service.SqueezeService; +import uk.org.ngo.squeezer.util.ImageCache.ImageCacheParams; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public class NowPlayingFragment extends Fragment implements + HasUiThread, View.OnCreateContextMenuListener { + + private final String TAG = "NowPlayingFragment"; + + private BaseActivity mActivity; + + private ISqueezeService mService = null; + + private TextView albumText; + + private TextView artistText; + + private TextView trackText; + + ImageView btnContextMenu; + + private TextView currentTime; + + private TextView totalTime; + + private MenuItem menu_item_connect; + + private MenuItem menu_item_disconnect; + + private MenuItem menu_item_poweron; + + private MenuItem menu_item_poweroff; + + private MenuItem menu_item_players; + + private MenuItem menu_item_playlists; + + private MenuItem menu_item_search; + + private MenuItem menu_item_volume; + + private ImageButton playPauseButton; + + private ImageButton nextButton; + + private ImageButton prevButton; + + private ImageButton shuffleButton; + + private ImageButton repeatButton; + + private ImageView albumArt; + + private SeekBar seekBar; + + /** + * Volume control panel. + */ + private VolumePanel mVolumePanel; + + // Updating the seekbar + private boolean updateSeekBar = true; + + private int secondsIn; + + private int secondsTotal; + + private final static int UPDATE_TIME = 1; + + /** + * ImageFetcher for album cover art + */ + private ImageFetcher mImageFetcher; + + /** + * ImageCache parameters for the album art. + */ + private ImageCacheParams mImageCacheParams; + + private final Handler uiThreadHandler = new UiThreadHandler(this); + + private final static class UiThreadHandler extends Handler { + + WeakReference mFragment; + + public UiThreadHandler(NowPlayingFragment fragment) { + mFragment = new WeakReference(fragment); + } + + // Normally I'm lazy and just post Runnables to the uiThreadHandler + // but time updating is special enough (it happens every second) to + // take care not to allocate so much memory which forces Dalvik to GC + // all the time. + @Override + public void handleMessage(Message message) { + if (message.what == UPDATE_TIME) { + mFragment.get().updateTimeDisplayTo(mFragment.get().secondsIn, + mFragment.get().secondsTotal); + } + } + } + + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ConnectivityManager connMgr = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + if (networkInfo.isConnected()) { + Log.v(TAG, "Received WIFI connected broadcast"); + if (!isConnected()) { + // Requires a serviceStub. Else we'll do this on the service + // connection callback. + if (mService != null && !isManualDisconnect()) { + Log.v(TAG, "Initiated connect on WIFI connected"); + startVisibleConnection(); + } + } + } + } + }; + + private ProgressDialog connectingDialog = null; + + private void clearConnectingDialog() { + if (connectingDialog != null && connectingDialog.isShowing()) { + connectingDialog.dismiss(); + } + connectingDialog = null; + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + Log.v(TAG, "ServiceConnection.onServiceConnected()"); + mService = ISqueezeService.Stub.asInterface(binder); + NowPlayingFragment.this.onServiceConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + private boolean mFullHeightLayout; + + /** + * Called before onAttach. Pull out the layout spec to figure out which layout to use later. + */ + @Override + public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { + super.onInflate(activity, attrs, savedInstanceState); + + int layout_height = attrs.getAttributeUnsignedIntValue( + "http://schemas.android.com/apk/res/android", + "layout_height", 0); + + mFullHeightLayout = (layout_height == ViewGroup.LayoutParams.FILL_PARENT); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (BaseActivity) activity; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + // Set up a server connection, if it is not present + if (getConfiguredCliIpPort(getSharedPreferences()) == null) { + SettingsActivity.show(mActivity); + } + + mActivity.bindService(new Intent(mActivity, SqueezeService.class), serviceConnection, + Context.BIND_AUTO_CREATE); + Log.d(TAG, "did bindService; serviceStub = " + mService); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v; + + if (mFullHeightLayout) { + v = inflater.inflate(R.layout.now_playing_fragment_full, container, false); + + artistText = (TextView) v.findViewById(R.id.artistname); + nextButton = (ImageButton) v.findViewById(R.id.next); + prevButton = (ImageButton) v.findViewById(R.id.prev); + shuffleButton = (ImageButton) v.findViewById(R.id.shuffle); + repeatButton = (ImageButton) v.findViewById(R.id.repeat); + currentTime = (TextView) v.findViewById(R.id.currenttime); + totalTime = (TextView) v.findViewById(R.id.totaltime); + seekBar = (SeekBar) v.findViewById(R.id.seekbar); + + btnContextMenu = (ImageView) v.findViewById(R.id.context_menu); + btnContextMenu.setOnCreateContextMenuListener(this); + btnContextMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + v.showContextMenu(); + } + }); + + // Calculate the size of the album art to display, which will be the shorter + // of the device's two dimensions. + Display display = mActivity.getWindowManager().getDefaultDisplay(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + display.getMetrics(displayMetrics); + mImageFetcher = new ImageFetcher(mActivity, + Math.min(displayMetrics.heightPixels, displayMetrics.widthPixels)); + } else { + v = inflater.inflate(R.layout.now_playing_fragment_mini, container, false); + + // Get an ImageFetcher to scale artwork to the size of the icon view. + Resources resources = getResources(); + int iconSize = (Math.max( + resources.getDimensionPixelSize(R.dimen.album_art_icon_height), + resources.getDimensionPixelSize(R.dimen.album_art_icon_width))); + mImageFetcher = new ImageFetcher(mActivity, iconSize); + } + + // TODO: Clean this up. I think a better approach is to create the cache + // in the activity that hosts the fragment, and make the cache available to + // the fragment (or, make the cache a singleton across the whole app). + mImageFetcher.setLoadingImage(R.drawable.icon_pending_artwork); + mImageCacheParams = new ImageCacheParams(mActivity, "artwork"); + mImageCacheParams.setMemCacheSizePercent(mActivity, 0.12f); + + albumArt = (ImageView) v.findViewById(R.id.album); + trackText = (TextView) v.findViewById(R.id.trackname); + albumText = (TextView) v.findViewById(R.id.albumname); + playPauseButton = (ImageButton) v.findViewById(R.id.pause); + + // Marquee effect on TextViews only works if they're focused. + trackText.requestFocus(); + + playPauseButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mService == null) { + return; + } + try { + if (isConnected()) { + Log.v(TAG, "Pause..."); + mService.togglePausePlay(); + } else { + // When we're not connected, the play/pause + // button turns into a green connect button. + onUserInitiatesConnect(); + } + } catch (RemoteException e) { + Log.e(TAG, "Service exception from togglePausePlay(): " + e); + } + } + }); + + if (mFullHeightLayout) { + /* + * TODO: Simplify these following the notes at + * http://developer.android.com/resources/articles/ui-1.6.html. + * Maybe. because the TextView resources don't support the + * android:onClick attribute. + */ + nextButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mService == null) { + return; + } + try { + mService.nextTrack(); + } catch (RemoteException e) { + } + } + }); + + prevButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mService == null) { + return; + } + try { + mService.previousTrack(); + } catch (RemoteException e) { + } + } + }); + + shuffleButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mService == null) { + return; + } + try { + mService.toggleShuffle(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception from toggleShuffle(): " + e); + } + } + }); + + repeatButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mService == null) { + return; + } + try { + mService.toggleRepeat(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception from toggleRepeat(): " + e); + } + } + }); + + seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + Song seekingSong; + + // Update the time indicator to reflect the dragged thumb + // position. + public void onProgressChanged(SeekBar s, int progress, boolean fromUser) { + if (fromUser) { + currentTime.setText(Util.makeTimeString(progress)); + } + } + + // Disable updates when user drags the thumb. + public void onStartTrackingTouch(SeekBar s) { + seekingSong = getCurrentSong(); + updateSeekBar = false; + } + + // Re-enable updates. If the current song is the same as when + // we started seeking then jump to the new point in the track, + // otherwise ignore the seek. + public void onStopTrackingTouch(SeekBar s) { + Song thisSong = getCurrentSong(); + + updateSeekBar = true; + + if (seekingSong == thisSong) { + setSecondsElapsed(s.getProgress()); + } + } + }); + } else { + // Clicking on the layout goes to NowPlayingActivity. + v.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + NowPlayingActivity.show(mActivity); + } + }); + } + + return v; + } + + /** + * Use this to post Runnables to work off thread + */ + public Handler getUIThreadHandler() { + return uiThreadHandler; + } + + // Should only be called the UI thread. + private void setConnected(boolean connected, boolean postConnect, boolean loginFailure) { + Log.v(TAG, "setConnected(" + connected + ", " + postConnect + ", " + loginFailure + ")"); + + // The fragment may have been detached from the parent activity in the intervening + // time. If so, do nothing. + if (isDetached()) { + return; + } + + if (postConnect) { + clearConnectingDialog(); + if (!connected) { + // TODO: Make this a dialog? Allow the user to correct the + // server settings here? + try { + Toast.makeText(mActivity, getText(R.string.connection_failed_text), + Toast.LENGTH_LONG) + .show(); + } catch (IllegalStateException e) { + // We are not allowed to show a toast at this point, but + // the Toast is not important so we ignore it. + Log.i(TAG, "Toast was not allowed: " + e); + } + } + } + if (loginFailure) { + Toast.makeText(mActivity, getText(R.string.login_failed_text), Toast.LENGTH_LONG) + .show(); + new AuthenticationDialog() + .show(mActivity.getSupportFragmentManager(), "AuthenticationDialog"); + } + + setMenuItemStateFromConnection(); + + if (mFullHeightLayout) { + nextButton.setEnabled(connected); + prevButton.setEnabled(connected); + shuffleButton.setEnabled(connected); + repeatButton.setEnabled(connected); + } + + if (!connected) { + updateSongInfo(null); + + playPauseButton.setImageResource(R.drawable.presence_online); // green circle + + if (mFullHeightLayout) { + albumArt.setImageResource(R.drawable.icon_album_noart_fullscreen); + nextButton.setImageResource(0); + prevButton.setImageResource(0); + shuffleButton.setImageResource(0); + repeatButton.setImageResource(0); + updateUIForPlayer(null); + artistText.setText(getText(R.string.disconnected_text)); + currentTime.setText("--:--"); + totalTime.setText("--:--"); + seekBar.setEnabled(false); + seekBar.setProgress(0); + } else { + albumArt.setImageResource(R.drawable.icon_album_noart); + } + } else { + if (mFullHeightLayout) { + nextButton.setImageResource(R.drawable.ic_action_next); + prevButton.setImageResource(R.drawable.ic_action_previous); + seekBar.setEnabled(true); + } + } + } + + private void updatePlayPauseIcon(PlayStatus playStatus) { + playPauseButton + .setImageResource((playStatus == PlayStatus.play) ? R.drawable.ic_action_pause + : R.drawable.ic_action_play); + } + + private void updateShuffleStatus(ShuffleStatus shuffleStatus) { + if (mFullHeightLayout && shuffleStatus != null) { + shuffleButton.setImageResource(shuffleStatus.getIcon()); + } + } + + private void updateRepeatStatus(RepeatStatus repeatStatus) { + if (mFullHeightLayout && repeatStatus != null) { + repeatButton.setImageResource(repeatStatus.getIcon()); + } + } + + private void updateUIForPlayer(Player player) { + if (mFullHeightLayout) { + mActivity.setTitle(player != null ? player.getName() : getText(R.string.app_name)); + } + } + + private void updatePowerMenuItems(boolean canPowerOn, boolean canPowerOff) { + boolean connected = isConnected(); + + // The fragment may have been detached from the parent activity in the intervening + // time. If so, do nothing. + if (isDetached()) { + return; + } + + if (menu_item_poweron != null) { + if (canPowerOn && connected) { + Player player = getActivePlayer(); + String playerName = player != null ? player.getName() : ""; + menu_item_poweron.setTitle(getString(R.string.menu_item_poweron, playerName)); + menu_item_poweron.setVisible(true); + } else { + menu_item_poweron.setVisible(false); + } + } + + if (menu_item_poweroff != null) { + if (canPowerOff && connected) { + Player player = getActivePlayer(); + String playerName = player != null ? player.getName() : ""; + menu_item_poweroff.setTitle(getString(R.string.menu_item_poweroff, playerName)); + menu_item_poweroff.setVisible(true); + } else { + menu_item_poweroff.setVisible(false); + } + } + } + + protected void onServiceConnected() { + Log.v(TAG, "Service bound"); + maybeRegisterCallbacks(); + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + updateUIFromServiceState(); + } + }); + + // Assume they want to connect (unless manually disconnected). + if (!isConnected() && !isManualDisconnect()) { + startVisibleConnection(); + } + } + + @Override + public void onResume() { + super.onResume(); + Log.d(TAG, "onResume..."); + + mVolumePanel = new VolumePanel(mActivity); + + mImageFetcher.addImageCache(mActivity.getSupportFragmentManager(), mImageCacheParams); + + // Start it and have it run forever (until it shuts itself down). + // This is required so swapping out the activity (and unbinding the + // service connection in onDestroy) doesn't cause the service to be + // killed due to zero refcount. This is our signal that we want + // it running in the background. + mActivity.startService(new Intent(mActivity, SqueezeService.class)); + + if (mService != null) { + maybeRegisterCallbacks(); + updateUIFromServiceState(); + } + + if (isAutoConnect(getSharedPreferences())) { + mActivity.registerReceiver(broadcastReceiver, new IntentFilter( + ConnectivityManager.CONNECTIVITY_ACTION)); + } + } + + /** + * Keep track of whether callbacks have been registered + */ + private boolean mRegisteredCallbacks; + + /** + * This is called when the service is first connected, and whenever the activity is resumed. + */ + private void maybeRegisterCallbacks() { + if (!mRegisteredCallbacks) { + try { + mService.registerCallback(serviceCallback); + mService.registerHandshakeCallback(handshakeCallback); + mService.registerMusicChangedCallback(musicChangedCallback); + mService.registerVolumeCallback(volumeCallback); + } catch (RemoteException e) { + Log.e(getTag(), "Error registering callback: " + e); + } + mRegisteredCallbacks = true; + } + } + + // Should only be called from the UI thread. + private void updateUIFromServiceState() { + // Update the UI to reflect connection state. Basically just for + // the initial display, as changing the prev/next buttons to empty + // doesn't seem to work in onCreate. (LayoutInflator still running?) + Log.d(TAG, "updateUIFromServiceState"); + boolean connected = isConnected(); + setConnected(connected, false, false); + if (connected) { + PlayerState playerState = getPlayerState(); + updateSongInfo(playerState.getCurrentSong()); + updatePlayPauseIcon(playerState.getPlayStatus()); + updateTimeDisplayTo(playerState.getCurrentTimeSecond(), + playerState.getCurrentSongDuration()); + updateUIForPlayer(getActivePlayer()); + updateShuffleStatus(playerState.getShuffleStatus()); + updateRepeatStatus(playerState.getRepeatStatus()); + } + } + + private void updateTimeDisplayTo(int secondsIn, int secondsTotal) { + if (mFullHeightLayout) { + if (updateSeekBar) { + if (seekBar.getMax() != secondsTotal) { + seekBar.setMax(secondsTotal); + totalTime.setText(Util.makeTimeString(secondsTotal)); + } + seekBar.setProgress(secondsIn); + currentTime.setText(Util.makeTimeString(secondsIn)); + } + } + } + + // Should only be called from the UI thread. + private void updateSongInfo(Song song) { + Log.v(TAG, "updateSongInfo " + song); + if (song != null) { + albumText.setText(song.getAlbumName()); + trackText.setText(song.getName()); + if (mFullHeightLayout) { + artistText.setText(song.getArtist()); + if (song.isRemote()) { + btnContextMenu.setVisibility(View.GONE); + } else { + btnContextMenu.setVisibility(View.VISIBLE); + } + } + } else { + albumText.setText(""); + trackText.setText(""); + if (mFullHeightLayout) { + artistText.setText(""); + btnContextMenu.setVisibility(View.GONE); + } + } + updateAlbumArt(song); + } + + // Should only be called from the UI thread. + private void updateAlbumArt(Song song) { + if (song == null || song.getArtworkUrl(mService) == null) { + if (mFullHeightLayout) { + albumArt.setImageResource(song != null && song.isRemote() + ? R.drawable.icon_iradio_noart_fullscreen + : R.drawable.icon_album_noart_fullscreen); + } else { + albumArt.setImageResource(song != null && song.isRemote() + ? R.drawable.icon_iradio_noart + : R.drawable.icon_album_noart); + } + return; + } + + // The image fetcher might not be ready yet. + if (mImageFetcher == null) { + return; + } + + mImageFetcher.loadImage(song.getArtworkUrl(mService), albumArt); + } + + private boolean setSecondsElapsed(int seconds) { + if (mService == null) { + return false; + } + try { + return mService.setSecondsElapsed(seconds); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in setSecondsElapsed(" + seconds + "): " + e); + } + return true; + } + + private PlayerState getPlayerState() { + if (mService == null) { + return null; + } + try { + return mService.getPlayerState(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in getPlayerState(): " + e); + } + return null; + } + + private Player getActivePlayer() { + if (mService == null) { + return null; + } + try { + return mService.getActivePlayer(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in getActivePlayer(): " + e); + } + return null; + } + + private Song getCurrentSong() { + PlayerState playerState = getPlayerState(); + return playerState != null ? playerState.getCurrentSong() : null; + } + + private boolean isConnected() { + if (mService == null) { + return false; + } + try { + return mService.isConnected(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in isConnected(): " + e); + } + return false; + } + + private boolean isConnectInProgress() { + if (mService == null) { + return false; + } + try { + return mService.isConnectInProgress(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in isConnectInProgress(): " + e); + } + return false; + } + + private boolean canPowerOn() { + if (mService == null) { + return false; + } + try { + return mService.canPowerOn(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in canPowerOn(): " + e); + } + return false; + } + + private boolean canPowerOff() { + if (mService == null) { + return false; + } + try { + return mService.canPowerOff(); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in canPowerOff(): " + e); + } + return false; + } + + @Override + public void onPause() { + Log.d(TAG, "onPause..."); + + mVolumePanel.dismiss(); + clearConnectingDialog(); + mImageFetcher.closeCache(); + + if (isAutoConnect(getSharedPreferences())) { + mActivity.unregisterReceiver(broadcastReceiver); + } + + if (mRegisteredCallbacks) { + try { + mService.unregisterCallback(serviceCallback); + mService.unregisterMusicChangedCallback(musicChangedCallback); + mService.unregisterHandshakeCallback(handshakeCallback); + mService.unregisterVolumeCallback(volumeCallback); + } catch (RemoteException e) { + Log.e(TAG, "Service exception in onPause(): " + e); + } + mRegisteredCallbacks = false; + } + + super.onPause(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mService != null) { + if (serviceConnection != null) { + mActivity.unbindService(serviceConnection); + } + } + } + + /** + * Builds a context menu suitable for the currently playing song. + *

+ * Takes the general song context menu, and disables items that make no sense for the song that + * is currently playing. + *

+ * {@inheritDoc} + * + * @param menu + * @param v + * @param menuInfo + */ + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.songcontextmenu, menu); + + menu.findItem(R.id.play_now).setVisible(false); + menu.findItem(R.id.play_next).setVisible(false); + menu.findItem(R.id.add_to_playlist).setVisible(false); + + menu.findItem(R.id.view_this_album).setVisible(true); + menu.findItem(R.id.view_albums_by_song).setVisible(true); + menu.findItem(R.id.view_songs_by_artist).setVisible(true); + } + + /** + * Handles clicks on the context menu. + *

+ * {@inheritDoc} + * + * @param item + * + * @return + */ + public boolean onContextItemSelected(MenuItem item) { + Song song = getCurrentSong(); + if (song == null || song.isRemote()) { + return false; + } + + // Note: Very similar to code in SongView:doItemContext(). Refactor? + switch (item.getItemId()) { + case R.id.download: + mActivity.downloadSong(song); + return true; + + case R.id.view_this_album: + SongListActivity.show(getActivity(), song.getAlbum()); + return true; + + case R.id.view_albums_by_song: + AlbumListActivity.show(getActivity(), + new Artist(song.getArtist_id(), song.getArtist())); + return true; + + case R.id.view_songs_by_artist: + SongListActivity.show(getActivity(), + new Artist(song.getArtist_id(), song.getArtist())); + return true; + + default: + throw new IllegalStateException("Unknown menu ID."); + } + } + + /** + * @see android.support.v4.app.Fragment#onCreateOptionsMenu(android.view.Menu, + * android.view.MenuInflater) + */ + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // I confess that I don't understand why using the inflater passed as + // an argument here doesn't work -- but if you do it crashes without + // a stracktrace on API 7. + MenuInflater i = mActivity.getMenuInflater(); + i.inflate(R.menu.squeezer, menu); + + menu_item_connect = menu.findItem(R.id.menu_item_connect); + menu_item_disconnect = menu.findItem(R.id.menu_item_disconnect); + menu_item_poweron = menu.findItem(R.id.menu_item_poweron); + menu_item_poweroff = menu.findItem(R.id.menu_item_poweroff); + menu_item_players = menu.findItem(R.id.menu_item_players); + menu_item_playlists = menu.findItem(R.id.menu_item_playlist); + menu_item_search = menu.findItem(R.id.menu_item_search); + menu_item_volume = menu.findItem(R.id.menu_item_volume); + + // On Android 2.3.x and lower onCreateOptionsMenu() is called when the menu is opened, + // almost certainly post-connection to the service. On 3.0 and higher it's called when + // the activity is created, before the service connection is made. Set the visibility + // of the menu items accordingly. + // XXX: onPrepareOptionsMenu() instead? + setMenuItemStateFromConnection(); + } + + /** + * Sets the state of assorted option menu items based on whether or not there is a connection to + * the server. + */ + private void setMenuItemStateFromConnection() { + boolean connected = isConnected(); + + // These are all set at the same time, so one check is sufficient + if (menu_item_connect != null) { + menu_item_connect.setVisible(!connected); + menu_item_disconnect.setVisible(connected); + menu_item_players.setEnabled(connected); + menu_item_playlists.setEnabled(connected); + menu_item_search.setEnabled(connected); + menu_item_volume.setEnabled(connected); + } + + updatePowerMenuItems(canPowerOn(), canPowerOff()); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_item_settings: + SettingsActivity.show(mActivity); + return true; + case R.id.menu_item_search: + mActivity.onSearchRequested(); + return true; + case R.id.menu_item_connect: + onUserInitiatesConnect(); + return true; + case R.id.menu_item_disconnect: + try { + mService.disconnect(); + DisconnectedActivity.show(mActivity); + } catch (RemoteException e) { + Toast.makeText(mActivity, e.toString(), + Toast.LENGTH_LONG).show(); + } + return true; + case R.id.menu_item_poweron: + try { + mService.powerOn(); + } catch (RemoteException e) { + Toast.makeText(mActivity, e.toString(), Toast.LENGTH_LONG).show(); + } + return true; + case R.id.menu_item_poweroff: + try { + mService.powerOff(); + } catch (RemoteException e) { + Toast.makeText(mActivity, e.toString(), + Toast.LENGTH_LONG).show(); + } + return true; + case R.id.menu_item_playlist: + CurrentPlaylistActivity.show(mActivity); + break; + case R.id.menu_item_players: + PlayerListActivity.show(mActivity); + return true; + case R.id.menu_item_about: + new AboutDialog().show(getFragmentManager(), "AboutDialog"); + return true; + case R.id.menu_item_volume: + // Show the volume dialog + PlayerState playerState = getPlayerState(); + Player player = getActivePlayer(); + + if (playerState != null) { + mVolumePanel.postVolumeChanged(playerState.getCurrentVolume(), + player == null ? "" : player.getName()); + } + return true; + } + return false; + } + + private SharedPreferences getSharedPreferences() { + return mActivity.getSharedPreferences(Preferences.NAME, Context.MODE_PRIVATE); + } + + private String getConfiguredCliIpPort(final SharedPreferences preferences) { + return getStringPreference(preferences, Preferences.KEY_SERVERADDR, null); + } + + private String getConfiguredUserName(final SharedPreferences preferences) { + return getStringPreference(preferences, Preferences.KEY_USERNAME, "test"); + } + + private String getConfiguredPassword(final SharedPreferences preferences) { + return getStringPreference(preferences, Preferences.KEY_PASSWORD, "test1"); + } + + private String getStringPreference(final SharedPreferences preferences, String preference, + String defaultValue) { + final String pref = preferences.getString(preference, null); + if (pref == null || pref.length() == 0) { + return defaultValue; + } + return pref; + } + + private boolean isAutoConnect(final SharedPreferences preferences) { + return preferences.getBoolean(Preferences.KEY_AUTO_CONNECT, true); + } + + /** + * Has the user manually disconnected from the server? + * + * @return true if they have, false otherwise. + */ + private boolean isManualDisconnect() { + return getActivity() instanceof DisconnectedActivity; + } + + private void onUserInitiatesConnect() { + // Set up a server connection, if it is not present + if (getConfiguredCliIpPort(getSharedPreferences()) == null) { + SettingsActivity.show(mActivity); + return; + } + + if (mService == null) { + Log.e(TAG, "serviceStub is null."); + return; + } + startVisibleConnection(); + } + + public void startVisibleConnection() { + Log.v(TAG, "startVisibleConnection"); + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + SharedPreferences preferences = getSharedPreferences(); + String ipPort = getConfiguredCliIpPort(preferences); + if (ipPort == null) { + return; + } + + // If we are configured to automatically connect on Wi-Fi availability + // we will also give the user the opportunity to enable Wi-Fi + if (isAutoConnect(preferences)) { + WifiManager wifiManager = (WifiManager) mActivity + .getSystemService(Context.WIFI_SERVICE); + if (!wifiManager.isWifiEnabled()) { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager != null) { + EnableWifiDialog.show(getFragmentManager()); + } else { + Log.i(getTag(), + "fragment manager is null so we can't show EnableWifiDialog"); + } + return; + // When a Wi-Fi connection is made this method will be called again by the + // broadcastReceiver + } + } + + if (isConnectInProgress()) { + Log.v(TAG, "Connection is already in progress, connecting aborted"); + return; + } + try { + connectingDialog = ProgressDialog.show(mActivity, + getText(R.string.connecting_text), + getString(R.string.connecting_to_text, ipPort), true, false); + Log.v(TAG, "startConnect, ipPort: " + ipPort); + try { + getConfiguredCliIpPort(preferences); + mService.startConnect(ipPort, getConfiguredUserName(preferences), + getConfiguredPassword(preferences)); + } catch (RemoteException e) { + Toast.makeText(mActivity, "startConnection error: " + e, + Toast.LENGTH_LONG).show(); + } + } catch (IllegalStateException e) { + Log.i(TAG, "ProgressDialog.show() was not allowed, connecting aborted: " + e); + connectingDialog = null; + } + } + }); + } + + private final IServiceCallback serviceCallback = new IServiceCallback.Stub() { + @Override + public void onConnectionChanged(final boolean isConnected, + final boolean postConnect, + final boolean loginFailed) + throws RemoteException { + Log.v(TAG, "Connected == " + isConnected + " (postConnect==" + postConnect + ")"); + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + setConnected(isConnected, postConnect, loginFailed); + } + }); + } + + @Override + public void onPlayerChanged(final Player player) throws RemoteException { + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + updateUIForPlayer(player); + } + }); + } + + @Override + public void onPlayStatusChanged(final String playStatusName) { + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + updatePlayPauseIcon(PlayStatus.valueOf(playStatusName)); + } + }); + } + + @Override + public void onShuffleStatusChanged(final boolean initial, final int shuffleStatusId) { + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + ShuffleStatus shuffleStatus = ShuffleStatus.valueOf(shuffleStatusId); + updateShuffleStatus(shuffleStatus); + if (!initial) { + Toast.makeText(mActivity, + mActivity.getServerString(shuffleStatus.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + } + + @Override + public void onRepeatStatusChanged(final boolean initial, final int repeatStatusId) { + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + RepeatStatus repeatStatus = RepeatStatus.valueOf(repeatStatusId); + updateRepeatStatus(repeatStatus); + if (!initial) { + Toast.makeText(mActivity, mActivity.getServerString(repeatStatus.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + } + + @Override + public void onTimeInSongChange(final int secondsIn, final int secondsTotal) + throws RemoteException { + NowPlayingFragment.this.secondsIn = secondsIn; + NowPlayingFragment.this.secondsTotal = secondsTotal; + uiThreadHandler.sendEmptyMessage(UPDATE_TIME); + } + + @Override + public void onPowerStatusChanged(final boolean canPowerOn, final boolean canPowerOff) + throws RemoteException { + uiThreadHandler.post(new Runnable() { + public void run() { + updatePowerMenuItems(canPowerOn, canPowerOff); + } + }); + } + }; + + private final IServiceMusicChangedCallback musicChangedCallback + = new IServiceMusicChangedCallback.Stub() { + @Override + public void onMusicChanged(final PlayerState playerState) throws RemoteException { + uiThreadHandler.post(new Runnable() { + public void run() { + updateSongInfo(playerState.getCurrentSong()); + } + }); + } + }; + + private final IServiceHandshakeCallback handshakeCallback + = new IServiceHandshakeCallback.Stub() { + @Override + public void onHandshakeCompleted() throws RemoteException { + uiThreadHandler.post(new Runnable() { + @Override + public void run() { + updatePowerMenuItems(canPowerOn(), canPowerOff()); + } + }); + } + }; + + private final IServiceVolumeCallback volumeCallback = new IServiceVolumeCallback.Stub() { + @Override + public void onVolumeChanged(final int newVolume, final Player player) + throws RemoteException { + mVolumePanel.postVolumeChanged(newVolume, player == null ? "" : player.getName()); + } + }; +} diff --git a/src/uk/org/ngo/squeezer/Preferences.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/Preferences.java similarity index 62% rename from src/uk/org/ngo/squeezer/Preferences.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/Preferences.java index 34699b11b..16af68316 100644 --- a/src/uk/org/ngo/squeezer/Preferences.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/Preferences.java @@ -17,12 +17,19 @@ package uk.org.ngo.squeezer; public final class Preferences { - public static final String NAME = "Squeezer"; - // e.g. "10.0.0.81:9090" - public static final String KEY_SERVERADDR = "squeezer.serveraddr"; + public static final String NAME = "Squeezer"; - // The playerId that we were last connected to. e.g. "00:04:20:17:04:7f" + // e.g. "10.0.0.81:9090" + public static final String KEY_SERVERADDR = "squeezer.serveraddr"; + + // Optional Squeezebox Server user name + public static final String KEY_USERNAME = "squeezer.username"; + + // Optional Squeezebox Server password + public static final String KEY_PASSWORD = "squeezer.password"; + + // The playerId that we were last connected to. e.g. "00:04:20:17:04:7f" public static final String KEY_LASTPLAYER = "squeezer.lastplayer"; // Do we automatically try and connect on WiFi availability? @@ -41,11 +48,21 @@ public final class Preferences { // Type of underlying preference is bool / CheckBox public static final String KEY_SCROBBLE_ENABLED = "squeezer.scrobble.enabled"; - public static final String KEY_DEBUG_LOGGING = "squeezer.debuglogging"; - // Do we send anonymous usage statistics? public static final String KEY_ANALYTICS_ENABLED = "squeezer.analytics.enabled"; - private Preferences() { - } + // Fade-in period? (0 = disable fade-in) + public static final String KEY_FADE_IN_SECS = "squeezer.fadeInSecs"; + + // What do to when an album is selected in the list view + public static final String KEY_ON_SELECT_ALBUM_ACTION = "squeezer.action.onselect.album"; + + // What do to when a song is selected in the list view + public static final String KEY_ON_SELECT_SONG_ACTION = "squeezer.action.onselect.song"; + + // Preferred album list layout. + public static final String KEY_ALBUM_LIST_LAYOUT = "squeezer.album.list.layout"; + + private Preferences() { + } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/RandomplayActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/RandomplayActivity.java new file mode 100644 index 000000000..e0ec7df7b --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/RandomplayActivity.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +import java.util.Arrays; + +import uk.org.ngo.squeezer.framework.BaseActivity; + +public class RandomplayActivity extends BaseActivity { + + private ListView listView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.item_list); + listView = (ListView) findViewById(R.id.item_list); + setRandomplayMenu(); + } + + @Override + protected void onServiceConnected() { + } + + + private void setRandomplayMenu() { + String[] values = getResources().getStringArray(R.array.randomplay_items); + int[] icons = new int[values.length]; + Arrays.fill(icons, R.drawable.ic_random); + + // XXX: Implement the "Choose the genres that the random mix will be + // drawn from" functionality. + // icons[icons.length - 1] = R.drawable.ic_genres; + listView.setAdapter(new IconRowAdapter(this, values, icons)); + listView.setOnItemClickListener(onRandomplayItemClick); + } + + private final OnItemClickListener onRandomplayItemClick = new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (position < RandomPlayType.values().length) { + try { + getService().randomPlay(RandomPlayType.values()[position].toString()); + } catch (RemoteException e) { + Log.e(getTag(), "Error registering list callback: " + e); + } + NowPlayingActivity.show(RandomplayActivity.this); + } + } + }; + + static void show(Context context) { + final Intent intent = new Intent(context, RandomplayActivity.class); + context.startActivity(intent); + } + + public enum RandomPlayType { + tracks, + albums, + contributors, + year + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/SearchActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/SearchActivity.java new file mode 100644 index 000000000..949b63283 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/SearchActivity.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.ExpandableListContextMenuInfo; +import android.widget.ExpandableListView.OnChildClickListener; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.itemlist.IServiceAlbumListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceArtistListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceGenreListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceSongListCallback; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Song; + +public class SearchActivity extends ItemListActivity { + + private static final String TAG = SearchActivity.class.getSimpleName(); + + private View loadingLabel; + + private ExpandableListView resultsExpandableListView; + + private SearchAdapter searchResultsAdapter; + + private String searchString; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.search_layout); + + loadingLabel = findViewById(R.id.loading_label); + + searchResultsAdapter = new SearchAdapter(this, getImageFetcher()); + resultsExpandableListView = (ExpandableListView) findViewById(R.id.search_expandable_list); + resultsExpandableListView.setAdapter(searchResultsAdapter); + + resultsExpandableListView.setOnChildClickListener(new OnChildClickListener() { + @Override + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id) { + searchResultsAdapter.onChildClick(groupPosition, childPosition); + return true; + } + }); + + resultsExpandableListView.setOnCreateContextMenuListener(searchResultsAdapter); + resultsExpandableListView.setOnScrollListener(new ScrollListener()); + + handleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + String query = intent.getStringExtra(SearchManager.QUERY); + doSearch(query); + } + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerArtistListCallback(artistsCallback); + getService().registerAlbumListCallback(albumsCallback); + getService().registerGenreListCallback(genresCallback); + getService().registerSongListCallback(songsCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterArtistListCallback(artistsCallback); + getService().unregisterAlbumListCallback(albumsCallback); + getService().unregisterGenreListCallback(genresCallback); + getService().unregisterSongListCallback(songsCallback); + } + + @Override + public final boolean onContextItemSelected(MenuItem menuItem) { + if (getService() != null) { + ExpandableListContextMenuInfo contextMenuInfo = (ExpandableListContextMenuInfo) menuItem + .getMenuInfo(); + long packedPosition = contextMenuInfo.packedPosition; + int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition); + int childPosition = ExpandableListView.getPackedPositionChild(packedPosition); + if (ExpandableListView.getPackedPositionType(packedPosition) + == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + return searchResultsAdapter.doItemContext(menuItem, groupPosition, childPosition); + } + } + return false; + } + + /** + * Performs the search now that the service connection is active. + */ + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + doSearch(); + } + + @Override + protected void orderPage(int start) { + try { + getService().search(start, searchString); + } catch (RemoteException e) { + Log.e(getTag(), "Error performing search: " + e); + } + } + + /** + * Saves the search query, and attempts to query the service for searchString. If + * the service binding has not completed yet then {@link #onServiceConnected()} will re-query + * for the saved search query. + * + * @param searchString The string to search fo. + */ + private void doSearch(String searchString) { + this.searchString = searchString; + if (searchString != null && searchString.length() > 0 && getService() != null) { + clearAndReOrderItems(); + } + } + + @Override + protected void clearItemAdapter() { + resultsExpandableListView.setVisibility(View.GONE); + loadingLabel.setVisibility(View.VISIBLE); + searchResultsAdapter.clear(); + } + + /** + * Searches for the saved search query. + */ + private void doSearch() { + doSearch(searchString); + } + + private void onItemsReceived(final int count, final int start, + final List items, final Class clazz) { + super.onItemsReceived(count, start, items.size()); + + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + searchResultsAdapter.updateItems(count, start, items, clazz); + loadingLabel.setVisibility(View.GONE); + resultsExpandableListView.setVisibility(View.VISIBLE); + } + }); + } + + private final IServiceArtistListCallback artistsCallback + = new IServiceArtistListCallback.Stub() { + @Override + public void onArtistsReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items, Artist.class); + } + }; + + private final IServiceAlbumListCallback albumsCallback = new IServiceAlbumListCallback.Stub() { + @Override + public void onAlbumsReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items, Album.class); + } + }; + + private final IServiceGenreListCallback genresCallback = new IServiceGenreListCallback.Stub() { + @Override + public void onGenresReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items, Genre.class); + } + }; + + private final IServiceSongListCallback songsCallback = new IServiceSongListCallback.Stub() { + @Override + public void onSongsReceived(int count, int start, List items) throws RemoteException { + onItemsReceived(count, start, items, Song.class); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/SearchAdapter.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/SearchAdapter.java new file mode 100644 index 000000000..5c618665e --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/SearchAdapter.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer; + +import android.graphics.drawable.Drawable; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.ExpandableListContextMenuInfo; +import android.widget.TextView; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.framework.ItemAdapter; +import uk.org.ngo.squeezer.framework.PlaylistItem; +import uk.org.ngo.squeezer.itemlist.AlbumView; +import uk.org.ngo.squeezer.itemlist.ArtistView; +import uk.org.ngo.squeezer.itemlist.GenreView; +import uk.org.ngo.squeezer.itemlist.SongView; +import uk.org.ngo.squeezer.itemlist.SongViewWithArt; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public class SearchAdapter extends BaseExpandableListAdapter implements + OnCreateContextMenuListener { + + private final int[] groupIcons = { + R.drawable.ic_songs, R.drawable.ic_albums, R.drawable.ic_artists, R.drawable.ic_genres + }; + + private final SearchActivity activity; + + private final ItemAdapter[] childAdapters; + + private final Map, ItemAdapter> childAdapterMap + = new HashMap, ItemAdapter>(); + + public SearchAdapter(SearchActivity activity, ImageFetcher imageFetcher) { + this.activity = activity; + + ItemAdapter[] adapters = { + new ItemAdapter(new SongViewWithArt(activity), imageFetcher), + new ItemAdapter(new AlbumView(activity), imageFetcher), + new ItemAdapter(new ArtistView(activity)), + new ItemAdapter(new GenreView(activity)), + }; + + ((SongViewWithArt) adapters[0].getItemView()).setDetails(EnumSet.of( + SongView.Details.DURATION, + SongView.Details.ALBUM, + SongView.Details.ARTIST)); + + ((AlbumView) adapters[1].getItemView()).setDetails(EnumSet.of( + AlbumView.Details.ARTIST, + AlbumView.Details.YEAR)); + + childAdapters = adapters; + for (ItemAdapter itemAdapter : childAdapters) { + childAdapterMap.put(itemAdapter.getItemView().getItemClass(), itemAdapter); + } + } + + public void clear() { + for (ItemAdapter itemAdapter : childAdapters) { + itemAdapter.clear(); + } + } + + @SuppressWarnings("unchecked") + public void updateItems(int count, int start, List items, Class clazz) { + ItemAdapter adapter = (ItemAdapter) childAdapterMap.get(clazz); + adapter.update(count, start, items); + notifyDataSetChanged(); + } + + public int getMaxCount() { + int count = 0; + for (ItemAdapter itemAdapter : childAdapters) { + if (itemAdapter.getCount() > count) { + count = itemAdapter.getCount(); + } + } + return count; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + ExpandableListContextMenuInfo contextMenuInfo = (ExpandableListContextMenuInfo) menuInfo; + long packedPosition = contextMenuInfo.packedPosition; + if (ExpandableListView.getPackedPositionType(packedPosition) + == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition); + int childPosition = ExpandableListView.getPackedPositionChild(packedPosition); + + AdapterContextMenuInfo adapterContextMenuInfo = new AdapterContextMenuInfo( + contextMenuInfo.targetView, childPosition, contextMenuInfo.id); + + childAdapters[groupPosition].onCreateContextMenu(menu, v, adapterContextMenuInfo); + } + } + + public void onChildClick(int groupPosition, int childPosition) { + childAdapters[groupPosition].onItemSelected(childPosition); + } + + public boolean doItemContext(MenuItem menuItem, int groupPosition, int childPosition) { + return childAdapters[groupPosition].doItemContext(menuItem, childPosition); + } + + @Override + public PlaylistItem getChild(int groupPosition, int childPosition) { + return (PlaylistItem) childAdapters[groupPosition].getItem(childPosition); + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + return childAdapters[groupPosition].getView(childPosition, convertView, parent); + } + + @Override + public int getChildrenCount(int groupPosition) { + return childAdapters[groupPosition].getCount(); + } + + @Override + public Object getGroup(int groupPosition) { + return childAdapters[groupPosition]; + } + + @Override + public int getGroupCount() { + return childAdapters.length; + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + View row = activity.getLayoutInflater().inflate(R.layout.group_item, parent, false); + + TextView label = (TextView) row.findViewById(R.id.label); + label.setText(childAdapters[groupPosition].getHeader()); + + // Build the icon to display next to the text. + // + // Take the normal icon (at 48dp) and scale it to 75% of its + // original size. Then set it as the left-most compound drawable. + + Drawable icon = Squeezer.getContext().getResources().getDrawable(groupIcons[groupPosition]); + int w = icon.getIntrinsicWidth(); + int h = icon.getIntrinsicHeight(); + icon.setBounds(0, 0, (int) Math.ceil(w * 0.75), (int) Math.ceil(h * 0.75)); + + label.setCompoundDrawables(icon, null, null, null); + + return (row); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + +} diff --git a/src/uk/org/ngo/squeezer/SettingsActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/SettingsActivity.java similarity index 57% rename from src/uk/org/ngo/squeezer/SettingsActivity.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/SettingsActivity.java index 902566f7f..39cb78422 100644 --- a/src/uk/org/ngo/squeezer/SettingsActivity.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/SettingsActivity.java @@ -16,11 +16,8 @@ package uk.org.ngo.squeezer; -import uk.org.ngo.squeezer.dialogs.ServerAddressPreference; -import uk.org.ngo.squeezer.service.ISqueezeService; -import uk.org.ngo.squeezer.service.SqueezeService; -import uk.org.ngo.squeezer.util.Scrobble; import android.app.Dialog; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -33,6 +30,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.preference.CheckBoxPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceActivity; @@ -40,46 +38,70 @@ import android.view.View; import android.widget.AdapterView; import android.widget.ListView; +import android.widget.Toast; + +import uk.org.ngo.squeezer.dialog.ServerAddressPreference; +import uk.org.ngo.squeezer.itemlist.action.PlayableItemAction; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.service.SqueezeService; +import uk.org.ngo.squeezer.util.Scrobble; public class SettingsActivity extends PreferenceActivity implements OnPreferenceChangeListener, OnSharedPreferenceChangeListener { - private final String TAG = "SettingsActivity"; + + private final String TAG = "SettingsActivity"; private static final int DIALOG_SCROBBLE_APPS = 0; private ISqueezeService serviceStub = null; + private ServerAddressPreference addrPref; + private IntEditTextPreference fadeInPref; + + private ListPreference onSelectAlbumPref; + + private ListPreference onSelectSongPref; + private final ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { serviceStub = ISqueezeService.Stub.asInterface(service); } + public void onServiceDisconnected(ComponentName name) { serviceStub = null; - }; + } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + bindService(new Intent(this, SqueezeService.class), serviceConnection, + Context.BIND_AUTO_CREATE); + Log.d(TAG, "did bindService; serviceStub = " + serviceStub); + getPreferenceManager().setSharedPreferencesName(Preferences.NAME); addPreferencesFromResource(R.xml.preferences); - addrPref = (ServerAddressPreference) findPreference(Preferences.KEY_SERVERADDR); - addrPref.setOnPreferenceChangeListener(this); - SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); preferences.registerOnSharedPreferenceChangeListener(this); - String currentCliAddr = preferences.getString(Preferences.KEY_SERVERADDR, ""); - updateAddressSummary(currentCliAddr); + addrPref = (ServerAddressPreference) findPreference(Preferences.KEY_SERVERADDR); + addrPref.setOnPreferenceChangeListener(this); + updateAddressSummary(preferences.getString(Preferences.KEY_SERVERADDR, "")); + + fadeInPref = (IntEditTextPreference) findPreference(Preferences.KEY_FADE_IN_SECS); + fadeInPref.setOnPreferenceChangeListener(this); + updateFadeInSecondsSummary(preferences.getInt(Preferences.KEY_FADE_IN_SECS, 0)); - CheckBoxPreference autoConnectPref = (CheckBoxPreference) findPreference(Preferences.KEY_AUTO_CONNECT); + CheckBoxPreference autoConnectPref = (CheckBoxPreference) findPreference( + Preferences.KEY_AUTO_CONNECT); autoConnectPref.setChecked(preferences.getBoolean(Preferences.KEY_AUTO_CONNECT, true)); // Scrobbling - CheckBoxPreference scrobblePref = (CheckBoxPreference) findPreference(Preferences.KEY_SCROBBLE_ENABLED); + CheckBoxPreference scrobblePref = (CheckBoxPreference) findPreference( + Preferences.KEY_SCROBBLE_ENABLED); scrobblePref.setOnPreferenceChangeListener(this); if (!Scrobble.canScrobble()) { @@ -103,47 +125,119 @@ protected void onCreate(Bundle savedInstanceState) { editor.commit(); } } + fillPlayableItemSelectionPreferences(); } - @Override - public void onResume() { - super.onResume(); - bindService(new Intent(this, SqueezeService.class), - serviceConnection, Context.BIND_AUTO_CREATE); - Log.d(TAG, "did bindService; serviceStub = " + serviceStub); + private void fillPlayableItemSelectionPreferences() { + String noneLabel = getString(PlayableItemAction.Type.NONE.labelId); + String addLabel = getString(PlayableItemAction.Type.ADD.labelId); + String playLabel = getString(PlayableItemAction.Type.PLAY.labelId); + String insertLabel = getString(PlayableItemAction.Type.INSERT.labelId); + String browseLabel = getString(PlayableItemAction.Type.BROWSE.labelId); + + onSelectAlbumPref = (ListPreference) findPreference(Preferences.KEY_ON_SELECT_ALBUM_ACTION); + onSelectAlbumPref.setEntryValues(new String[]{ + PlayableItemAction.Type.PLAY.name(), + PlayableItemAction.Type.INSERT.name(), + PlayableItemAction.Type.ADD.name(), + PlayableItemAction.Type.BROWSE.name() + }); + onSelectAlbumPref.setEntries(new String[]{playLabel, insertLabel, + addLabel, browseLabel}); + onSelectAlbumPref.setDefaultValue(PlayableItemAction.Type.BROWSE.name()); + if (onSelectAlbumPref.getValue() == null) { + onSelectAlbumPref.setValue(PlayableItemAction.Type.BROWSE.name()); + } + onSelectAlbumPref.setOnPreferenceChangeListener(this); + updateSelectAlbumSummary(onSelectAlbumPref.getValue()); + + onSelectSongPref = (ListPreference) findPreference(Preferences.KEY_ON_SELECT_SONG_ACTION); + onSelectSongPref.setEntryValues(new String[]{ + PlayableItemAction.Type.NONE.name(), + PlayableItemAction.Type.PLAY.name(), + PlayableItemAction.Type.INSERT.name(), + PlayableItemAction.Type.ADD.name() + }); + onSelectSongPref.setEntries(new String[]{noneLabel, playLabel, + insertLabel, addLabel}); + onSelectSongPref.setDefaultValue(PlayableItemAction.Type.NONE.name()); + if (onSelectSongPref.getValue() == null) { + onSelectSongPref.setValue(PlayableItemAction.Type.NONE.name()); + } + onSelectSongPref.setOnPreferenceChangeListener(this); + updateSelectSongSummary(onSelectSongPref.getValue()); } @Override - public void onPause() { - super.onPause(); + public void onDestroy() { + super.onDestroy(); unbindService(serviceConnection); } - private void updateAddressSummary(String addr) { + private void updateAddressSummary(String addr) { if (addr.length() > 0) { addrPref.setSummary(addr); } else { - addrPref.setSummary(getText(R.string.settings_serveraddr_summary)); + addrPref.setSummary(R.string.settings_serveraddr_summary); + } + } + + private void updateFadeInSecondsSummary(int fadeInSeconds) { + if (fadeInSeconds == 0) { + fadeInPref.setSummary(R.string.disabled); + } else { + fadeInPref.setSummary(fadeInSeconds + " " + getResources() + .getQuantityString(R.plurals.seconds, fadeInSeconds)); } } + private void updateSelectAlbumSummary(String value) { + CharSequence[] entries = onSelectAlbumPref.getEntries(); + int index = onSelectAlbumPref.findIndexOfValue(value); + + onSelectAlbumPref.setSummary(entries[index]); + } + + private void updateSelectSongSummary(String value) { + CharSequence[] entries = onSelectSongPref.getEntries(); + int index = onSelectSongPref.findIndexOfValue(value); + + onSelectSongPref.setSummary(entries[index]); + } + /** - * A preference has been changed by the user, but has not yet been - * persisted. - * + * A preference has been changed by the user, but has not yet been persisted. + * * @param preference * @param newValue + * * @return */ public boolean onPreferenceChange(Preference preference, Object newValue) { - final String key = preference.getKey(); - Log.v(TAG, "preference change for: " + key); - if (Preferences.KEY_SERVERADDR.equals(key)) { - final String ipPort = newValue.toString(); - // TODO: check that it looks valid? - updateAddressSummary(ipPort); - return true; - } + final String key = preference.getKey(); + Log.v(TAG, "preference change for: " + key); + + if (Preferences.KEY_SERVERADDR.equals(key)) { + final String ipPort = newValue.toString(); + // TODO: check that it looks valid? + updateAddressSummary(ipPort); + return true; + } + + if (Preferences.KEY_FADE_IN_SECS.equals(key)) { + updateFadeInSecondsSummary(Util.parseDecimalIntOrZero(newValue.toString())); + return true; + } + + if (Preferences.KEY_ON_SELECT_ALBUM_ACTION.equals(key)) { + updateSelectAlbumSummary(newValue.toString()); + return true; + } + + if (Preferences.KEY_ON_SELECT_SONG_ACTION.equals(key)) { + updateSelectSongSummary(newValue.toString()); + return true; + } // If the user has enabled Scrobbling but we don't think it will work // pop up a dialog with links to Google Play for apps to install. @@ -160,12 +254,12 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { return true; } - return false; - } + return false; + } /** * A preference has been changed by the user and is going to be persisted. - * + * * @param sharedPreferences * @param key */ @@ -209,12 +303,18 @@ protected Dialog onCreateDialog(int id) { ListView appList = (ListView) dialog.findViewById(R.id.scrobble_apps); appList.setAdapter(new IconRowAdapter(this, apps, icons)); + final Context context = dialog.getContext(); appList.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView parent, View view, int position, long id) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("market://details?id=" + urls[position])); - startActivity(intent); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.settings_market_not_found, + Toast.LENGTH_SHORT).show(); + } } }); } diff --git a/src/uk/org/ngo/squeezer/Squeezer.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/Squeezer.java similarity index 96% rename from src/uk/org/ngo/squeezer/Squeezer.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/Squeezer.java index 7cffa2607..6ad9f8590 100644 --- a/src/uk/org/ngo/squeezer/Squeezer.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/Squeezer.java @@ -23,6 +23,7 @@ sharedPreferencesName = Preferences.NAME, formKey = "") public class Squeezer extends Application { + private static Squeezer instance; public Squeezer() { @@ -38,4 +39,5 @@ public void onCreate() { ACRA.init(this); super.onCreate(); } -} \ No newline at end of file +} + diff --git a/src/uk/org/ngo/squeezer/Util.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/Util.java similarity index 65% rename from src/uk/org/ngo/squeezer/Util.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/Util.java index b2e77f325..e62753628 100644 --- a/src/uk/org/ngo/squeezer/Util.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/Util.java @@ -16,6 +16,11 @@ package uk.org.ngo.squeezer; +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -23,57 +28,71 @@ import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; -import android.app.Activity; -import android.view.View; -import android.widget.TextView; - public class Util { - private Util() {} + + private Util() { + } public static String nonNullString(AtomicReference ref) { String string = ref.get(); return string == null ? "" : string; } + public static int getAtomicInteger(AtomicReference ref, int defaultValue) { + Integer integer = ref.get(); + return integer == null ? 0 : integer; + } + + /** * Update target, if it's different from newValue. + * * @param target * @param newValue + * * @return true if target is updated. Otherwise return false. */ - public static boolean atomicReferenceUpdated(AtomicReference target, T newValue) { - T currentValue = target.get(); - if (currentValue == null && newValue == null) - return false; - if (currentValue == null || !currentValue.equals(newValue)) { - target.set(newValue); - return true; - } - return false; + public static boolean atomicReferenceUpdated(AtomicReference target, T newValue) { + T currentValue = target.get(); + if (currentValue == null && newValue == null) { + return false; + } + if (currentValue == null || !currentValue.equals(newValue)) { + target.set(newValue); + return true; + } + return false; } public static int parseDecimalInt(String value, int defaultValue) { - if (value == null) - return defaultValue; + if (value == null) { + return defaultValue; + } int decimalPoint = value.indexOf('.'); - if (decimalPoint != -1) value = value.substring(0, decimalPoint); - if (value.length() == 0) return defaultValue; + if (decimalPoint != -1) { + value = value.substring(0, decimalPoint); + } + if (value.length() == 0) { + return defaultValue; + } try { - int intValue = Integer.parseInt(value); - return intValue; + return Integer.parseInt(value); } catch (NumberFormatException e) { return defaultValue; } } public static int parseDecimalIntOrZero(String value) { - return parseDecimalInt(value, 0); + return parseDecimalInt(value, 0); } private static StringBuilder sFormatBuilder = new StringBuilder(); + private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault()); + private static final Object[] sTimeArgs = new Object[5]; + // TODO(nik): I think this can be removed in favour of Android's built in duration formatter public synchronized static String makeTimeString(long secs) { /* Provide multiple arguments so the format can be changed easily * by modifying the xml. @@ -105,34 +124,32 @@ public static String decode(String string) { } } - /** - * Convenience function for a ListView with entries that are plain - * TextViews. - *

- * - * @param activity - * @param convertView - * @param label The text to show in the list item. - * @return a view inflated from R.layout.list_item, with the - * contents of label assigned to the TextView. - */ - public static View getListItemView(Activity activity, View convertView, String label) { - TextView view; - view = (TextView)(convertView != null && TextView.class.isAssignableFrom(convertView.getClass()) - ? convertView - : activity.getLayoutInflater().inflate(R.layout.list_item, null)); - view.setText(label); - return view; - } - - public static View getSpinnerItemView(Activity activity, View convertView, String label) { + public static View getSpinnerItemView(Activity activity, View convertView, ViewGroup parent, + String label) { TextView view; view = (TextView) (convertView != null && TextView.class.isAssignableFrom(convertView.getClass()) ? convertView - : activity.getLayoutInflater().inflate(R.layout.spinner_item, null)); + : activity.getLayoutInflater() + .inflate(android.R.layout.simple_spinner_dropdown_item, parent, false)); view.setText(label); return view; } + /** + * Count how many of the supplied booleans are true. + * + * @param items Booleans to count + * + * @return Number of arguments which are true + */ + public static int countBooleans(boolean... items) { + int count = 0; + for (boolean item : items) { + if (item) { + count++; + } + } + return count; + } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/VolumePanel.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/VolumePanel.java new file mode 100644 index 000000000..8b580e4f4 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/VolumePanel.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2011 Google Inc. All Rights Reserved. + * + * 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. + */ + +/** + * Implement a custom toast view that's modelled on the one in + * android.view.VolumePanel (but which is not public). + * + */ +package uk.org.ngo.squeezer; + +import android.app.Dialog; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; + +import uk.org.ngo.squeezer.framework.BaseActivity; +import uk.org.ngo.squeezer.service.ISqueezeService; + + +public class VolumePanel extends Handler implements SeekBar.OnSeekBarChangeListener { + + private static final int TIMEOUT_DELAY = 3000; + + private static final int MSG_VOLUME_CHANGED = 0; + + private static final int MSG_FREE_RESOURCES = 1; + + private static final int MSG_TIMEOUT = 2; + + protected BaseActivity mActivity; + + /** + * Dialog displaying the volume panel. + */ + private final Dialog mDialog; + + /** + * View displaying volume sliders. + */ + private final View mView; + + private final TextView mMessage; + + private final TextView mAdditionalMessage; + + private final ImageView mLargeStreamIcon; + + private final SeekBar mSeekbar; + + public VolumePanel(BaseActivity activity) { + mActivity = activity; + + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mView = inflater.inflate(R.layout.volume_adjust, null); + mView.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + resetTimeout(); + return false; + } + }); + + mMessage = (TextView) mView.findViewById(R.id.message); + mAdditionalMessage = (TextView) mView.findViewById(R.id.additional_message); + mSeekbar = (SeekBar) mView.findViewById(R.id.level); + mLargeStreamIcon = (ImageView) mView.findViewById(R.id.ringer_stream_icon); + + mSeekbar.setOnSeekBarChangeListener(this); + + mDialog = new Dialog(mActivity, R.style.VolumePanel) { //android.R.style.Theme_Panel) { + public boolean onTouchEvent(MotionEvent event) { + if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE) { + forceTimeout(); + return true; + } + return false; + } + }; + mDialog.setTitle("Volume Control"); + mDialog.setContentView(mView); + + // Set window properties to match other toasts/dialogs. + Window window = mDialog.getWindow(); + window.setGravity(Gravity.TOP); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.token = null; + lp.width = WindowManager.LayoutParams.WRAP_CONTENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + window.setAttributes(lp); + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); + } + + public void dismiss() { + removeMessages(MSG_TIMEOUT); + if (mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + private void resetTimeout() { + removeMessages(MSG_TIMEOUT); + sendMessageDelayed(obtainMessage(MSG_TIMEOUT), TIMEOUT_DELAY); + } + + private void forceTimeout() { + removeMessages(MSG_TIMEOUT); + sendMessage(obtainMessage(MSG_TIMEOUT)); + } + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + ISqueezeService service = mActivity.getService(); + if (service != null) { + try { + service.adjustVolumeTo(progress); + } catch (RemoteException e) { + } + } + } + } + + public void onStartTrackingTouch(SeekBar seekBar) { + } + + public void onStopTrackingTouch(SeekBar seekBar) { + } + + public void postVolumeChanged(int newVolume, String additionalMessage) { + if (hasMessages(MSG_VOLUME_CHANGED)) { + return; + } + removeMessages(MSG_FREE_RESOURCES); + obtainMessage(MSG_VOLUME_CHANGED, newVolume, 0, additionalMessage).sendToTarget(); + } + + protected void onVolumeChanged(int newVolume, String additionalMessage) { + onShowVolumeChanged(newVolume, additionalMessage); + } + + protected void onShowVolumeChanged(int newVolume, String additionalMessage) { + mSeekbar.setMax(100); + mSeekbar.setProgress(newVolume); + + mMessage.setText( + mActivity.getString(R.string.volume, mActivity.getString(R.string.app_name))); + mAdditionalMessage.setText(additionalMessage); + + mLargeStreamIcon.setImageResource(newVolume == 0 + ? R.drawable.ic_volume_off + : R.drawable.ic_volume); + + if (!mDialog.isShowing() && !mActivity.isFinishing()) { + mDialog.setContentView(mView); + mDialog.show(); + } + + resetTimeout(); + } + + protected void onFreeResources() { + // We'll keep the views, just ditch the cached drawable and hence + // bitmaps + mLargeStreamIcon.setImageDrawable(null); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case MSG_VOLUME_CHANGED: { + onVolumeChanged(msg.arg1, (String) msg.obj); + break; + } + + case MSG_TIMEOUT: { + dismiss(); + break; + } + + case MSG_FREE_RESOURCES: { + onFreeResources(); + break; + } + } + } +} + diff --git a/src/uk/org/ngo/squeezer/dialogs/AboutDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/AboutDialog.java similarity index 77% rename from src/uk/org/ngo/squeezer/dialogs/AboutDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/AboutDialog.java index 02dbe5617..a23bfb4c9 100644 --- a/src/uk/org/ngo/squeezer/dialogs/AboutDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/AboutDialog.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.dialogs; +package uk.org.ngo.squeezer.dialog; -import uk.org.ngo.squeezer.R; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; @@ -29,17 +28,22 @@ import android.view.View; import android.widget.TextView; +import de.cketti.library.changelog.ChangeLog; +import uk.org.ngo.squeezer.R; + public class AboutDialog extends DialogFragment { + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final View view = getActivity().getLayoutInflater().inflate(R.layout.about_dialog, null); final TextView titleText = (TextView) view.findViewById(R.id.about_title); + final TextView versionText = (TextView) view.findViewById(R.id.version_text); PackageManager pm = getActivity().getPackageManager(); PackageInfo info; try { - info = pm.getPackageInfo("uk.org.ngo.squeezer", 0); - titleText.setText(getString(R.string.about_title, info.versionName)); + info = pm.getPackageInfo(getActivity().getPackageName(), 0); + versionText.setText(info.versionName); } catch (NameNotFoundException e) { titleText.setText(getString(R.string.app_name)); } @@ -47,6 +51,13 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { Builder builder = new AlertDialog.Builder(getActivity()); builder.setView(view); builder.setPositiveButton(android.R.string.ok, null); + builder.setNeutralButton("Change Log", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ChangeLog changeLog = new ChangeLog(getActivity()); + changeLog.getFullLogDialog().show(); + } + }); builder.setNegativeButton(R.string.dialog_license, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { new LicenseDialog() @@ -55,4 +66,4 @@ public void onClick(DialogInterface dialog, int which) { }); return builder.create(); } -} \ No newline at end of file +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/AuthenticationDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/AuthenticationDialog.java new file mode 100644 index 000000000..6397b0b47 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/AuthenticationDialog.java @@ -0,0 +1,49 @@ +package uk.org.ngo.squeezer.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.View; +import android.widget.EditText; + +import uk.org.ngo.squeezer.NowPlayingFragment; +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; + +public class AuthenticationDialog extends DialogFragment { + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final SharedPreferences preferences = getActivity() + .getSharedPreferences(Preferences.NAME, Context.MODE_PRIVATE); + + View form = getActivity().getLayoutInflater().inflate(R.layout.authentication_dialog, null); + builder.setView(form); + + final EditText userNameEditText = (EditText) form.findViewById(R.id.username); + userNameEditText.setText(preferences.getString(Preferences.KEY_USERNAME, null)); + + final EditText passwordEditText = (EditText) form.findViewById(R.id.password); + passwordEditText.setText(preferences.getString(Preferences.KEY_PASSWORD, null)); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(Preferences.KEY_USERNAME, userNameEditText.getText().toString()); + editor.putString(Preferences.KEY_PASSWORD, passwordEditText.getText().toString()); + editor.commit(); + + ((NowPlayingFragment) getActivity().getSupportFragmentManager() + .findFragmentById(R.id.now_playing_fragment)).startVisibleConnection(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + return builder.create(); + } +} diff --git a/src/uk/org/ngo/squeezer/dialogs/EnableWifiDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/EnableWifiDialog.java similarity index 51% rename from src/uk/org/ngo/squeezer/dialogs/EnableWifiDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/EnableWifiDialog.java index b8936b08c..18b057184 100644 --- a/src/uk/org/ngo/squeezer/dialogs/EnableWifiDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/EnableWifiDialog.java @@ -1,6 +1,5 @@ -package uk.org.ngo.squeezer.dialogs; +package uk.org.ngo.squeezer.dialog; -import uk.org.ngo.squeezer.R; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -8,21 +7,33 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.util.Log; +import android.widget.Toast; + +import uk.org.ngo.squeezer.R; public class EnableWifiDialog extends DialogFragment { + private static final String TAG = EnableWifiDialog.class.getSimpleName(); + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.wifi_disabled_text); builder.setMessage(R.string.enable_wifi_text); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { - WifiManager wifiManager = (WifiManager) getActivity().getSystemService(Context.WIFI_SERVICE); + WifiManager wifiManager = (WifiManager) getActivity().getSystemService( + Context.WIFI_SERVICE); if (!wifiManager.isWifiEnabled()) { Log.v(getTag(), "Enabling Wifi"); wifiManager.setWifiEnabled(true); + Toast.makeText(getActivity(), R.string.wifi_enabled_text, Toast.LENGTH_LONG) + .show(); } } }); @@ -30,4 +41,19 @@ public void onClick(DialogInterface dialog, int which) { return builder.create(); } -} \ No newline at end of file + public static EnableWifiDialog show(FragmentManager fragmentManager) { + // Remove any currently showing dialog + Fragment prev = fragmentManager.findFragmentByTag(TAG); + if (prev != null) { + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.remove(prev); + fragmentTransaction.commit(); + } + + // Create and show the dialog + EnableWifiDialog dialog = new EnableWifiDialog(); + dialog.show(fragmentManager, TAG); + return dialog; + } + +} diff --git a/src/uk/org/ngo/squeezer/dialogs/LicenseDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/LicenseDialog.java similarity index 94% rename from src/uk/org/ngo/squeezer/dialogs/LicenseDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/LicenseDialog.java index 12bd4c764..835f6333a 100644 --- a/src/uk/org/ngo/squeezer/dialogs/LicenseDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/LicenseDialog.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.dialogs; +package uk.org.ngo.squeezer.dialog; -import uk.org.ngo.squeezer.R; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; @@ -24,7 +23,10 @@ import android.support.v4.app.DialogFragment; import android.text.Html; +import uk.org.ngo.squeezer.R; + public class LicenseDialog extends DialogFragment { + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Builder builder = new AlertDialog.Builder(getActivity()); diff --git a/src/uk/org/ngo/squeezer/dialogs/ServerAddressPreference.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/ServerAddressPreference.java similarity index 79% rename from src/uk/org/ngo/squeezer/dialogs/ServerAddressPreference.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/ServerAddressPreference.java index b4b9505af..8947a63a7 100644 --- a/src/uk/org/ngo/squeezer/dialogs/ServerAddressPreference.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/ServerAddressPreference.java @@ -14,25 +14,14 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.dialogs; - -import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.util.Map.Entry; -import java.util.TreeMap; +package uk.org.ngo.squeezer.dialog; import org.acra.ErrorReporter; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.Squeezer; -import uk.org.ngo.squeezer.util.UIUtils; import android.annotation.TargetApi; import android.app.ProgressDialog; import android.content.Context; +import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; @@ -53,24 +42,45 @@ import android.widget.Spinner; import android.widget.TextView; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Map.Entry; +import java.util.TreeMap; + +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.Squeezer; +import uk.org.ngo.squeezer.util.UIUtils; + /** - * Shows a preference dialog that allows the user to scan the local network - * for servers, choose a server from the results of the scan, or enter the - * name/address of a server directly. + * Shows a preference dialog that allows the user to scan the local network for servers, choose a + * server from the results of the scan, or enter the name/address of a server directly. */ public class ServerAddressPreference extends DialogPreference { + private EditText mServerAddressEditText; - private Button mScanBtn; + private Spinner mServersSpinner; private ScanNetworkTask mScanNetworkTask; - /** Map server names to IP addresses. */ + private EditText userNameEditText; + + private EditText passwordEditText; + + /** + * Map server names to IP addresses. + */ private TreeMap mDiscoveredServers; private ArrayAdapter mServersAdapter; private final Context mContext; + private ProgressDialog mProgressDialog; public ServerAddressPreference(Context context, AttributeSet attrs) { @@ -84,9 +94,14 @@ protected void onBindDialogView(View view) { super.onBindDialogView(view); mServerAddressEditText = (EditText) view.findViewById(R.id.server_address); - mScanBtn = (Button) view.findViewById(R.id.scan_btn); + Button scanBtn = (Button) view.findViewById(R.id.scan_btn); mServersSpinner = (Spinner) view.findViewById(R.id.found_servers); + userNameEditText = (EditText) view.findViewById(R.id.username); + userNameEditText.setText(getSharedPreferences().getString(Preferences.KEY_USERNAME, null)); + passwordEditText = (EditText) view.findViewById(R.id.password); + passwordEditText.setText(getSharedPreferences().getString(Preferences.KEY_PASSWORD, null)); + // If there's no server address configured then set the default text // in the edit box to our IP address, trimmed of the last octet. String serveraddr = getPersistedString(""); @@ -118,7 +133,7 @@ protected void onBindDialogView(View view) { .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo ni = cm.getActiveNetworkInfo(); if (ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI) { - mScanBtn.setOnClickListener(new OnClickListener() { + scanBtn.setOnClickListener(new OnClickListener() { public void onClick(View v) { startNetworkScan(); } @@ -126,7 +141,7 @@ public void onClick(View v) { } else { TextView scan_msg = (TextView) view.findViewById(R.id.scan_msg); scan_msg.setText(mContext.getText(R.string.settings_server_scanning_disabled_msg)); - mScanBtn.setEnabled(false); + scanBtn.setEnabled(false); } } @@ -172,8 +187,9 @@ private void updateProgress(int progress, int secondaryProgress) { void onScanFinished() { mProgressDialog.dismiss(); - if (mScanNetworkTask == null) + if (mScanNetworkTask == null) { return; + } mDiscoveredServers = mScanNetworkTask.getDiscoveredServers(); mScanNetworkTask = null; @@ -209,9 +225,22 @@ protected void onDialogClosed(boolean positiveResult) { } if (positiveResult) { - String addr = mServerAddressEditText.getText().toString(); - persistString(addr); - callChangeListener(addr); + StringBuilder ipPort = new StringBuilder(mServerAddressEditText.getText()); + + // Append the default port if necessary. + if (ipPort.indexOf(":") == -1) { + ipPort.append(":"); + ipPort.append(mContext.getResources().getInteger(R.integer.DefaultPort)); + } + + persistString(ipPort.toString()); + + SharedPreferences.Editor editor = getEditor(); + editor.putString(Preferences.KEY_USERNAME, userNameEditText.getText().toString()); + editor.putString(Preferences.KEY_PASSWORD, passwordEditText.getText().toString()); + editor.commit(); + + callChangeListener(ipPort.toString()); } } @@ -234,6 +263,7 @@ protected Parcelable onSaveInstanceState() { * Inserts the selected address in to the edittext widget. */ public class MyOnItemSelectedListener implements OnItemSelectedListener { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { mServerAddressEditText.setText(mDiscoveredServers.get(parent.getItemAtPosition(pos) @@ -249,21 +279,31 @@ public void onNothingSelected(AdapterView parent) { * Scans the local network for servers. */ static class ScanNetworkTask extends AsyncTask { + private final String TAG = "scanNetworkTask"; private final Context mContext; + private final ServerAddressPreference mPref; - /** Map server names to IP addresses. */ + /** + * Map server names to IP addresses. + */ private final TreeMap mServerMap = new TreeMap(); - /** UDP port to broadcast discovery requests to. */ + /** + * UDP port to broadcast discovery requests to. + */ private final int DISCOVERY_PORT = 3483; - /** Maximum number of discovery attempts. */ + /** + * Maximum number of discovery attempts. + */ public final int MAX_DISCOVERY_ATTEMPTS = 5; - /** Maximum time to wait between discovery attempts (ms). */ + /** + * Maximum time to wait between discovery attempts (ms). + */ private final int DISCOVERY_ATTEMPT_TIMEOUT = 1000; ScanNetworkTask(Context context, ServerAddressPreference pref) { @@ -272,18 +312,16 @@ static class ScanNetworkTask extends AsyncTask { } /** - * Discover Squeezerservers on the local network. - *

- * Do this by sending MAX_DISCOVERY_ATTEMPT UDP broadcasts to port 3483 - * at approximately DISCOVERY_ATTEMPT_TIMEOUT intervals. Squeezeservers - * are supposed to listen for this, and respond with a packet that - * starts 'E' and some information about the server, including its name. - *

- * Map the name to an IP address and store in mDiscoveredServers for - * later use. - *

- * See the Slim::Networking::Discovery module in Squeezeserver for more - * details. + * Discover Squeezeservers on the local network. + *

+ * Do this by sending MAX_DISCOVERY_ATTEMPT UDP broadcasts to port 3483 at approximately + * DISCOVERY_ATTEMPT_TIMEOUT intervals. Squeezeservers are supposed to listen for this, and + * respond with a packet that starts 'E' and some information about the server, including + * its name. + *

+ * Map the name to an IP address and store in mDiscoveredServers for later use. + *

+ * See the Slim::Networking::Discovery module in Squeezeserver for more details. */ @Override protected Void doInBackground(Void... unused) { @@ -343,12 +381,20 @@ protected Void doInBackground(Void... unused) { if (!timedOut) { if (buf[0] == (byte) 'E') { - String serverAddr = responsePacket.getAddress().getHostAddress(); + // There's no mechanism for the server to return the port + // the CLI is listening on, so assume it's the default and + // append it to the address. + StringBuilder ipPort = new StringBuilder( + responsePacket.getAddress().getHostAddress()); + ipPort.append(":"); + ipPort.append( + mContext.getResources().getInteger(R.integer.DefaultPort)); // Blocks of data are TAG/LENGTH/VALUE, where TAG is // a 4 byte string identifying the item, LENGTH is - // the length of the VALUE (e.g., reading \t means the - // value is 9 bytes, and VALUE is the actual value. + // the length of the VALUE (e.g., reading \t means + // the value is 9 bytes, and VALUE is the actual + // value. // Find the 'NAME' block int i = 1; @@ -364,7 +410,7 @@ protected Void doInBackground(Void... unused) { // i now pointing at the length of the NAME value. String name = new String(buf, i + 1, buf[i]); - mServerMap.put(name, serverAddr); + mServerMap.put(name, ipPort.toString()); } } @@ -381,22 +427,24 @@ protected Void doInBackground(Void... unused) { ErrorReporter.getInstance().handleException(e); } - if (socket != null) + if (socket != null) { socket.close(); + } Log.v(TAG, "Scanning complete, unlocking WiFi"); - if (wifiLock != null) + if (wifiLock != null) { wifiLock.release(); + } // For testing that multiple servers are handled correctly. - // mServerMap.put("Dummy 2", "127.0.0.2"); + // mServerMap.put("Dummy", "127.0.0.1"); return null; } /** - * Update the progress bar. The main progress value corresponds to how - * many servers have been discovered, the secondary progress value - * corresponds to how far through the discovery process we are. + * Update the progress bar. The main progress value corresponds to how many servers have + * been discovered, the secondary progress value corresponds to how far through the + * discovery process we are. */ @Override protected void onProgressUpdate(Integer... values) { diff --git a/src/uk/org/ngo/squeezer/dialogs/TipsDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/TipsDialog.java similarity index 96% rename from src/uk/org/ngo/squeezer/dialogs/TipsDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/TipsDialog.java index 35222a885..214813c86 100644 --- a/src/uk/org/ngo/squeezer/dialogs/TipsDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/dialog/TipsDialog.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.dialogs; +package uk.org.ngo.squeezer.dialog; -import uk.org.ngo.squeezer.R; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; @@ -27,7 +26,10 @@ import android.view.KeyEvent; import android.view.View; +import uk.org.ngo.squeezer.R; + public class TipsDialog extends DialogFragment implements OnKeyListener { + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final View view = getActivity().getLayoutInflater().inflate(R.layout.tips_dialog, null); @@ -45,7 +47,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { * Change the volume when the key is depressed. Suppress the keyUp event, * otherwise you get a notification beep as well as the volume changing. * - * TODO: Do this for all the dialogs. + * TODO: Do this for all the dialog. */ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { switch (keyCode) { @@ -56,4 +58,4 @@ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { return false; } -} \ No newline at end of file +} diff --git a/src/uk/org/ngo/squeezer/framework/SqueezerArtworkItem.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ArtworkItem.java similarity index 66% rename from src/uk/org/ngo/squeezer/framework/SqueezerArtworkItem.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ArtworkItem.java index d90a99b27..7031efd1c 100644 --- a/src/uk/org/ngo/squeezer/framework/SqueezerArtworkItem.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ArtworkItem.java @@ -18,13 +18,18 @@ /** - * A SqueezerItem that has associated artwork. - * + * A PlaylistItem that has associated artwork. */ -public abstract class SqueezerArtworkItem extends SqueezerPlaylistItem { +public abstract class ArtworkItem extends PlaylistItem { - private String artwork_track_id; - public String getArtwork_track_id() { return artwork_track_id; } - public void setArtwork_track_id(String artwork_track_id) { this.artwork_track_id = artwork_track_id; } + private String artwork_track_id; + + public String getArtwork_track_id() { + return artwork_track_id; + } + + public void setArtwork_track_id(String artwork_track_id) { + this.artwork_track_id = artwork_track_id; + } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseActivity.java new file mode 100644 index 000000000..2e8fef858 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseActivity.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + +import org.acra.ErrorReporter; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.KeyEvent; +import android.widget.Toast; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.menu.BaseMenuFragment; +import uk.org.ngo.squeezer.menu.MenuFragment; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.service.ServerString; +import uk.org.ngo.squeezer.service.SqueezeService; + +/** + * Common base class for all activities in the squeezer + * + * @author Kurt Aaholst + */ +public abstract class BaseActivity extends ActionBarActivity implements HasUiThread { + + private ISqueezeService service = null; + + private final Handler uiThreadHandler = new Handler() { + }; + + protected abstract void onServiceConnected(); + + protected String getTag() { + return getClass().getSimpleName(); + } + + /** + * @return The squeezeservice, or null if not bound + */ + public ISqueezeService getService() { + return service; + } + + /** + * Use this to post Runnables to work off thread + */ + @Override + public Handler getUIThreadHandler() { + return uiThreadHandler; + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + service = ISqueezeService.Stub.asInterface(binder); + BaseActivity.this.onServiceConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + service = null; + } + }; + + @Override + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ActionBar actionBar = getSupportActionBar(); + + actionBar.setIcon(R.drawable.ic_launcher); + actionBar.setHomeButtonEnabled(true); + bindService(new Intent(this, SqueezeService.class), serviceConnection, + Context.BIND_AUTO_CREATE); + Log.d(getTag(), "did bindService; serviceStub = " + getService()); + + BaseMenuFragment.add(this, MenuFragment.class); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (serviceConnection != null) { + unbindService(serviceConnection); + } + } + + /** + * Block searches, when we are not connected. + */ + @Override + public boolean onSearchRequested() { + if (!isConnected()) { + return false; + } + return super.onSearchRequested(); + } + + /* + * Intercept hardware volume control keys to control Squeezeserver + * volume. + * + * Change the volume when the key is depressed. Suppress the keyUp + * event, otherwise you get a notification beep as well as the volume + * changing. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + changeVolumeBy(+5); + return true; + case KeyEvent.KEYCODE_VOLUME_DOWN: + changeVolumeBy(-5); + return true; + } + + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + return true; + } + + return super.onKeyUp(keyCode, event); + } + + private boolean changeVolumeBy(int delta) { + if (getService() == null) { + return false; + } + Log.v(getTag(), "Adjust volume by: " + delta); + try { + getService().adjustVolumeBy(delta); + return true; + } catch (RemoteException e) { + Log.e(getTag(), "Error from service.adjustVolumeBy: " + e); + } + return false; + } + + // Safe accessors + + public boolean isConnected() { + if (service == null) { + return false; + } + try { + return service.isConnected(); + } catch (RemoteException e) { + Log.e(getTag(), "Service exception in isConnected(): " + e); + } + return false; + } + + public String getIconUrl(String icon) { + if (service == null || icon == null) { + return null; + } + try { + return service.getIconUrl(icon); + } catch (RemoteException e) { + Log.e(getClass().getSimpleName(), "Error requesting icon url '" + icon + "': " + e); + return null; + } + } + + public String getServerString(ServerString stringToken) { + return ServerString.values()[stringToken.ordinal()].getLocalizedString(); + } + + // This section is just an easier way to call squeeze service + + public void play(PlaylistItem item) throws RemoteException { + playlistControl(PlaylistControlCmd.load, item, R.string.ITEM_PLAYING); + } + + public void add(PlaylistItem item) throws RemoteException { + playlistControl(PlaylistControlCmd.add, item, R.string.ITEM_ADDED); + } + + public void insert(PlaylistItem item) throws RemoteException { + playlistControl(PlaylistControlCmd.insert, item, R.string.ITEM_INSERTED); + } + + private void playlistControl(PlaylistControlCmd cmd, PlaylistItem item, int resId) + throws RemoteException { + if (service == null) { + return; + } + + service.playlistControl(cmd.name(), item.getPlaylistTag(), item.getId()); + Toast.makeText(this, getString(resId, item.getName()), Toast.LENGTH_SHORT).show(); + } + + /** + * Attempts to download the supplied song.

This method will silently refuse to download if + * song is null or is remote. + * + * @param song song to download + */ + public void downloadSong(Song song) { + if (song != null && !song.isRemote()) { + downloadSong(song.getId()); + } + } + + /** + * Attempts to download the song given by songId. + * + * @param songId ID of the song to download + */ + public void downloadSong(String songId) { + if (songId == null) { + return; + } + + /* + * Quick-and-dirty version. Use ACTION_VIEW to have something try and + * download the song (probably the browser). + * + * TODO: If running on Gingerbread or greater use the Download Manager + * APIs to have more control over the download. + */ + try { + String url = getService().getSongDownloadUrl(songId); + + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(i); + } catch (RemoteException e) { + ErrorReporter.getInstance().handleException(e); + e.printStackTrace(); + } + } + + private enum PlaylistControlCmd { + load, + add, + insert + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseItemView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseItemView.java new file mode 100644 index 000000000..d79b929e7 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseItemView.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + +import com.google.common.base.Joiner; + +import android.os.Parcelable.Creator; +import android.os.RemoteException; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import java.lang.reflect.Field; +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.util.Reflection; +import uk.org.ngo.squeezer.itemlist.AlbumListActivity; +import uk.org.ngo.squeezer.itemlist.ArtistListActivity; +import uk.org.ngo.squeezer.itemlist.SongListActivity; +import uk.org.ngo.squeezer.util.ImageFetcher; +import uk.org.ngo.squeezer.widget.ListItemImageButton; +import uk.org.ngo.squeezer.widget.SquareImageView; + +/** + * Represents the view hierarchy for a single {@link Item} subclass, suitable for displaying in a + * {@link ItemListActivity}. + *

+ * This class supports views that have a {@link TextView} to display the primary information about + * the {@link Item} and can optionally enable additional views. The layout is defined in {@code + * res/layout/list_item.xml}.

  • A {@link SquareImageView} suitable for displaying icons
  • + *
  • A second, smaller {@link TextView} for additional item information
  • A {@link + * ListItemImageButton} that shows a disclosure triangle for a context menu
The view can + * display an item in one of two states. The primary state is when the data to be inserted in to + * the view is known, and represented by a complete {@link Item} subclass. The loading state is when + * the data type is known, but has not been fetched from the server yet. + *

+ * To customise the view's display create an {@link EnumSet} of {@link ViewParams} and pass it to + * {@link #setViewParams(EnumSet)} or {@link #setLoadingViewParams(EnumSet)} depending on whether + * you want to change the layout of the view in its primary state or the loading state. For example, + * if the primary state should show a context button you may not want to show that button while + * waiting for data to arrive. + *

+ * Override {@link #bindView(View, Item, ImageFetcher)} and {@link #bindView(View, String)} to + * control how data from the item is inserted in to the view. + *

+ * If you need a completely custom view hierarchy then override {@link #getAdapterView(View, + * ViewGroup, EnumSet)} and {@link #getAdapterView(View, ViewGroup, String)}. + * + * @param the Item subclass this view represents. + */ +public abstract class BaseItemView implements ItemView { + + protected static final int BROWSE_ALBUMS = 1; + + private final ItemListActivity mActivity; + + private final LayoutInflater mLayoutInflater; + + private ItemAdapter mAdapter; + + private Class mItemClass; + + private Creator mCreator; + + /** + * Parameters that control which additional views will be enabeld in the item view. + */ + public enum ViewParams { + /** + * Adds a {@link SquareImageView} for displaying album artwork or other iconography. + */ + ICON, + + /** + * Adds a second line for detail information ({@code R.id.text2}). + */ + TWO_LINE, + + /** + * Adds a button (with click handler) to display the context menu. + */ + CONTEXT_BUTTON + } + + /** + * View parameters for a filled-in view. One primary line with context button. + */ + private EnumSet mViewParams = EnumSet.of(ViewParams.CONTEXT_BUTTON); + + /** + * View parameters for a view that is loading data. Primary line only. + */ + private EnumSet mLoadingViewParams = EnumSet.noneOf(ViewParams.class); + + /** + * A ViewHolder for the views that make up a complete list item. + */ + public static class ViewHolder { + + public ImageView icon; + + public TextView text1; + + public TextView text2; + + public ImageButton btnContextMenu; + + public EnumSet viewParams; + } + + /** + * Joins elements together with ' - ', skipping nulls. + */ + protected static final Joiner mJoiner = Joiner.on(" - ").skipNulls(); + + public BaseItemView(ItemListActivity activity) { + this.mActivity = activity; + mLayoutInflater = activity.getLayoutInflater(); + } + + @Override + public ItemListActivity getActivity() { + return mActivity; + } + + public ItemAdapter getAdapter() { + return mAdapter; + } + + public void setAdapter(ItemAdapter adapter) { + mAdapter = adapter; + } + + public LayoutInflater getLayoutInflater() { + return mLayoutInflater; + } + + /** + * Set the view parameters to use for the view when data is loaded. + */ + protected void setViewParams(EnumSet viewParams) { + mViewParams = viewParams; + } + + /** + * Set the view parameters to use for the view while data is being loaded. + */ + protected void setLoadingViewParams(EnumSet viewParams) { + mLoadingViewParams = viewParams; + } + + @Override + @SuppressWarnings("unchecked") + public Class getItemClass() { + if (mItemClass == null) { + mItemClass = (Class) Reflection.getGenericClass(getClass(), ItemView.class, + 0); + if (mItemClass == null) { + throw new RuntimeException("Could not read generic argument for: " + getClass()); + } + } + return mItemClass; + } + + @Override + @SuppressWarnings("unchecked") + public Creator getCreator() { + if (mCreator == null) { + Field field; + try { + field = getItemClass().getField("CREATOR"); + } catch (Exception e) { + throw new RuntimeException(e); + } + try { + mCreator = (Creator) field.get(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return mCreator; + } + + protected String getTag() { + return getClass().getSimpleName(); + } + + /** + * Returns a view suitable for displaying the data of item in a list. Item may not be null. + *

+ * Override this method and {@link #getAdapterView(View, ViewGroup, String)} if your subclass + * uses a different layout. + */ + @Override + public View getAdapterView(View convertView, ViewGroup parent, T item, + ImageFetcher imageFetcher) { + View view = getAdapterView(convertView, parent, mViewParams); + bindView(view, item, imageFetcher); + return view; + } + + /** + * Binds the item's name to {@link ViewHolder#text1}. + *

+ * OVerride this instead of {@link #getAdapterView(View, ViewGroup, Item, ImageFetcher)} if the + * default layouts are sufficient. + * + * @param view The view that contains the {@link ViewHolder} + * @param item The item to be bound + * @param imageFetcher An {@link ImageFetcher} (may be null) + */ + public void bindView(View view, T item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + } + + /** + * Returns a view suitable for displaying the "Loading..." text. + *

+ * Override this method and {@link #getAdapterView(View, ViewGroup, Item, ImageFetcher)} if your + * extension uses a different layout. + */ + @Override + public View getAdapterView(View convertView, ViewGroup parent, String text) { + View view = getAdapterView(convertView, parent, mLoadingViewParams); + bindView(view, text); + return view; + } + + /** + * Binds the text to {@link ViewHolder#text1}. + *

+ * Override this instead of {@link #getAdapterView(View, ViewGroup, String)} if the default + * layout is sufficient. + * + * @param view The view that contains the {@link ViewHolder} + * @param text The text to set in the view. + */ + public void bindView(View view, String text) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(text); + } + + /** + * Creates a view from {@code convertView} and the {@code viewParams} using the default layout + * {@link R.layout#list_item} + * + * @param convertView View to reuse if possible. + * @param parent The {@link ViewGroup} to inherit properties from. + * @param viewParams A set of 0 or more {@link ViewParams} to customise the view. + * + * @return convertView if it can be reused, or a new view + */ + public View getAdapterView(View convertView, ViewGroup parent, EnumSet viewParams) { + return getAdapterView(convertView, parent, viewParams, R.layout.list_item); + } + + /** + * Creates a view from {@code convertView} and the {@code viewParams}. + * + * @param convertView View to reuse if possible. + * @param parent The {@link ViewGroup} to inherit properties from. + * @param viewParams A set of 0 or more {@link ViewParams} to customise the view. + * @param layoutResource The layout resource defining the item view + * + * @return convertView if it can be reused, or a new view + */ + public View getAdapterView(View convertView, ViewGroup parent, EnumSet viewParams, + int layoutResource) { + ViewHolder viewHolder = + (convertView != null && convertView.getTag().getClass() == ViewHolder.class) + ? (ViewHolder) convertView.getTag() + : null; + + if (viewHolder == null) { + convertView = getLayoutInflater().inflate(layoutResource, parent, false); + viewHolder = new ViewHolder(); + viewHolder.text1 = (TextView) convertView.findViewById(R.id.text1); + viewHolder.text2 = (TextView) convertView.findViewById(R.id.text2); + viewHolder.icon = (ImageView) convertView.findViewById(R.id.icon); + viewHolder.btnContextMenu = (ImageButton) convertView.findViewById(R.id.context_menu); + convertView.setTag(viewHolder); + } + + // If the view parameters are different then reset the visibility of child views and hook + // up any standard behaviours. + if (!viewParams.equals(viewHolder.viewParams)) { + viewHolder.icon + .setVisibility(viewParams.contains(ViewParams.ICON) ? View.VISIBLE : View.GONE); + viewHolder.text2.setVisibility( + viewParams.contains(ViewParams.TWO_LINE) ? View.VISIBLE : View.GONE); + + if (viewParams.contains(ViewParams.CONTEXT_BUTTON)) { + viewHolder.btnContextMenu.setVisibility(View.VISIBLE); + viewHolder.btnContextMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + v.showContextMenu(); + } + }); + } else { + viewHolder.btnContextMenu.setVisibility(View.GONE); + } + + viewHolder.viewParams = viewParams; + } + + return convertView; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ItemView.ContextMenuInfo menuInfo) { + menu.setHeaderTitle(menuInfo.item.getName()); + } + + /** + * The default context menu handler handles some common actions. Each action must be set up in + * {@link #setupContextMenu(android.view.ContextMenu, int, Item)} + */ + @Override + public boolean doItemContext(MenuItem menuItem, int index, T selectedItem) + throws RemoteException { + switch (menuItem.getItemId()) { + case R.id.browse_songs: + SongListActivity.show(mActivity, selectedItem); + return true; + + case BROWSE_ALBUMS: + AlbumListActivity.show(mActivity, selectedItem); + return true; + + case R.id.browse_artists: + ArtistListActivity.show(mActivity, selectedItem); + return true; + + case R.id.play_now: + mActivity.play((PlaylistItem) selectedItem); + return true; + + case R.id.add_to_playlist: + mActivity.add((PlaylistItem) selectedItem); + return true; + + case R.id.play_next: + mActivity.insert((PlaylistItem) selectedItem); + return true; + } + return false; + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseListActivity.java new file mode 100644 index 000000000..56cacb5d0 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/BaseListActivity.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + + +import android.os.Bundle; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.AbsListView; +import android.widget.AbsListView.RecyclerListener; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.ProgressBar; + +import java.lang.reflect.Method; +import java.util.List; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.util.RetainFragment; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A generic base class for an activity to list items of a particular SqueezeServer data type. The + * data type is defined by the generic type argument, and must be an extension of {@link Item}. You + * must provide an {@link ItemView} to provide the view logic used by this activity. This is done by + * implementing {@link #createItemView()}. + *

+ * When the activity is first created ({@link #onCreate(Bundle)}), an empty {@link ItemListAdapter} + * is created using the provided {@link ItemView}. See {@link ItemListActivity} for see details of + * ordering and receiving of list items from SqueezeServer, and handling of item selection. + * + * @param Denotes the class of the items this class should list + * + * @author Kurt Aaholst + */ +public abstract class BaseListActivity extends ItemListActivity { + + private static final String TAG = BaseListActivity.class.getName(); + + /** + * Tag for first visible position in mRetainFragment. + */ + private static final String TAG_POSITION = "position"; + + + /** + * Tag for itemAdapter in mRetainFragment. + */ + public static final String TAG_ADAPTER = "adapter"; + + private AbsListView mListView; + + private ItemAdapter itemAdapter; + + /** + * Progress bar (spinning) while items are loading. + */ + private ProgressBar loadingProgress; + + /** + * Fragment to retain information across the activity lifecycle. + */ + private RetainFragment mRetainFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mRetainFragment = RetainFragment.getInstance(TAG, getSupportFragmentManager()); + + setContentView(getContentView()); + mListView = checkNotNull((AbsListView) findViewById(R.id.item_list), + "getContentView() did not return a view containing R.id.item_list"); + + loadingProgress = checkNotNull((ProgressBar) findViewById(R.id.loading_progress), + "getContentView() did not return a view containing R.id.loading_progress"); + + mListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + getItemAdapter().onItemSelected(position); + } + }); + + mListView.setOnScrollListener(new ScrollListener()); + + mListView.setRecyclerListener(new RecyclerListener() { + @Override + public void onMovedToScrapHeap(View view) { + // Release strong reference when a view is recycled + final ImageView imageView = (ImageView) view.findViewById(R.id.icon); + if (imageView != null) { + imageView.setImageBitmap(null); + } + } + }); + + // Delegate context menu creation to the adapter. + mListView.setOnCreateContextMenuListener(getItemAdapter()); + } + + /** + * Returns the ID of a content view to be used by this list activity. + *

+ * The content view must contain a {@link AbsListView} with the id {@literal item_list} and a + * {@link ProgressBar} with the id {@literal loading_progress} in order to be valid. + * + * @return The ID + */ + protected int getContentView() { + return R.layout.item_list; + } + + /** + * @return A new view logic to be used by this activity + */ + abstract protected ItemView createItemView(); + + @Override + public boolean onContextItemSelected(MenuItem menuItem) { + AdapterContextMenuInfo menuInfo = (AdapterContextMenuInfo) menuItem.getMenuInfo(); + + return itemAdapter.doItemContext(menuItem, menuInfo.position); + } + + /** + * Set our adapter on the list view. + *

+ * This can't be done in {@link #onCreate(android.os.Bundle)} because getView might be called + * before the service is connected, so we need to delay it. + *

+ * However when we set the adapter after onCreate the list is scrolled to top, so we retain the + * visible position. + *

+ * Call this method when the service is connected + */ + private void setAdapter() { + // setAdapter is not defined for AbsListView before API level 11, but + // it is for concrete implementations, so we call it by reflection + try { + Method method = mListView.getClass().getMethod("setAdapter", ListAdapter.class); + method.invoke(mListView, getItemAdapter()); + } catch (Exception e) { + Log.e(getTag(), "Error calling 'setAdapter'", e); + } + + Integer position = (Integer) mRetainFragment.get(TAG_POSITION); + if (position != null) { + if (mListView instanceof ListView) { + ((ListView) mListView).setSelectionFromTop(position, 0); + } else { + mListView.setSelection(position); + } + } + } + + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + + maybeOrderVisiblePages(mListView); + setAdapter(); + } + + @Override + public void onResume() { + super.onResume(); + + if (getService() != null) { + maybeOrderVisiblePages(mListView); + setAdapter(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + saveVisiblePosition(); + } + + /** + * Store the first visible position of {@link #mListView}, in the {@link #mRetainFragment}, so + * we can later retrieve it. + * + * @see android.widget.AbsListView#getFirstVisiblePosition() + */ + private void saveVisiblePosition() { + mRetainFragment.put(TAG_POSITION, mListView.getFirstVisiblePosition()); + } + + + /** + * @return The current {@link ItemAdapter}'s {@link ItemView} + */ + public ItemView getItemView() { + return getItemAdapter().getItemView(); + } + + /** + * @return The current {@link ItemAdapter}, creating it if necessary. + */ + public ItemAdapter getItemAdapter() { + if (itemAdapter == null) { + //noinspection unchecked + itemAdapter = (ItemAdapter) mRetainFragment.get(TAG_ADAPTER); + if (itemAdapter == null) { + itemAdapter = createItemListAdapter(createItemView()); + mRetainFragment.put(TAG_ADAPTER, itemAdapter); + } else { + // We have just retained the item adapter, we need to create a new + // item view logic, cause it holds a reference to the old activity + itemAdapter.setItemView(createItemView()); + // Update views with the count from the retained item adapter + itemAdapter.onCountUpdated(); + } + } + + return itemAdapter; + } + + @Override + protected void clearItemAdapter() { + // TODO: This should be removed in favour of showing a progress spinner in the actionbar. + mListView.setVisibility(View.GONE); + loadingProgress.setVisibility(View.VISIBLE); + + getItemAdapter().clear(); + } + + /** + * @return The {@link AbsListView} used by this activity + */ + public AbsListView getListView() { + return mListView; + } + + protected ItemAdapter createItemListAdapter(ItemView itemView) { + return new ItemListAdapter(itemView, getImageFetcher()); + } + + public void onItemsReceived(final int count, final int start, final List items) { + super.onItemsReceived(count, start, items.size()); + + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + mListView.setVisibility(View.VISIBLE); + loadingProgress.setVisibility(View.GONE); + getItemAdapter().update(count, start, items); + } + }); + } + + protected class ScrollListener extends ItemListActivity.ScrollListener { + + ScrollListener() { + super(); + } + + /** + * Pauses cache disk fetches if the user is flinging the list, or if their finger is still + * on the screen. + */ + @Override + public void onScrollStateChanged(AbsListView listView, int scrollState) { + super.onScrollStateChanged(listView, scrollState); + + if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING || + scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + getImageFetcher().setPauseWork(true); + } else { + getImageFetcher().setPauseWork(false); + } + } + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/HasUiThread.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/HasUiThread.java new file mode 100644 index 000000000..ae551fa1b --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/HasUiThread.java @@ -0,0 +1,8 @@ +package uk.org.ngo.squeezer.framework; + +import android.os.Handler; + +public interface HasUiThread { + + Handler getUIThreadHandler(); +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/Item.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/Item.java new file mode 100644 index 000000000..cc8f1ea57 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/Item.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + +import android.os.Parcelable; + +/** + * Base class for SqueezeServer data. Specializations must implement all the necessary boilerplate + * code. This is okay for now, because we only have few data types. + * + * @author Kurt Aaholst + */ +public abstract class Item implements Parcelable { + + private String id; + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + abstract public String getName(); + + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + return (getId() != null ? getId().hashCode() : 0); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (o == null) { + return false; + } + + if (o.getClass() != getClass()) { + // There is no guarantee that SqueezeServer items have globally unique IDs. + return false; + } + + // Both might be empty items. For example a Song initialised + // with an empty token map, because no song is currently playing. + if (getId() == null && ((Item) o).getId() == null) { + return true; + } + + return getId() != null && getId().equals(((Item) o).getId()); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemAdapter.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemAdapter.java new file mode 100644 index 000000000..c5e850c0f --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemAdapter.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.BaseAdapter; + +import java.util.List; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.util.ImageFetcher; + + +/** + * A generic class for an adapter to list items of a particular SqueezeServer data type. The data + * type is defined by the generic type argument, and must be an extension of {@link Item}. + *

+ * If you need an adapter for a {@link BaseListActivity}, then use {@link ItemListAdapter} instead. + *

+ * Normally there is no need to extend this (or {@link ItemListAdapter}), as we delegate all type + * dependent stuff to {@link ItemView}. + * + * @param Denotes the class of the items this class should list + * + * @author Kurt Aaholst + * @see ItemView + */ +public class ItemAdapter extends BaseAdapter implements + OnCreateContextMenuListener { + + private static final String TAG = ItemAdapter.class.getSimpleName(); + + /** + * View logic for this adapter + */ + private ItemView mItemView; + + /** + * List of items, possibly headed with an empty item. + *

+ * As the items are received from SqueezeServer they will be inserted in the list. + */ + private int count; + + private final SparseArray pages = new SparseArray(); + + /** + * This is set if the list shall start with an empty item. + */ + private final boolean mEmptyItem; + + /** + * Text to display before the items are received from SqueezeServer + */ + private final String loadingText; + + /** + * Number of elements to by fetched at a time + */ + private int pageSize; + + /** + * ImageFetcher for thumbnails + */ + private ImageFetcher mImageFetcher; + + public int getPageSize() { + return pageSize; + } + + /** + * Creates a new adapter. Initially the item list is populated with items displaying the + * localized "loading" text. Call {@link #update(int, int, int, List)} as items arrives from + * SqueezeServer. + * + * @param itemView The {@link ItemView} to use with this adapter + * @param emptyItem If set the list of items shall start with an empty item + * @param imageFetcher ImageFetcher to use for loading thumbnails + */ + public ItemAdapter(ItemView itemView, boolean emptyItem, + ImageFetcher imageFetcher) { + mItemView = itemView; + mEmptyItem = emptyItem; + mImageFetcher = imageFetcher; + loadingText = itemView.getActivity().getString(R.string.loading_text); + pageSize = itemView.getActivity().getResources().getInteger(R.integer.PageSize); + pages.clear(); + } + + /** + * Calls {@link #BaseAdapter(ItemView, boolean, ImageFetcher)}, with emptyItem = false + */ + public ItemAdapter(ItemView itemView, ImageFetcher imageFetcher) { + this(itemView, false, imageFetcher); + } + + /** + * Calls {@link #BaseAdapter(ItemView, boolean, ImageFetcher)}, with emptyItem = false + * and a null ImageFetcher. + */ + public ItemAdapter(ItemView itemView) { + this(itemView, false, null); + } + + private int pageNumber(int position) { + return position / pageSize; + } + + /** + * Removes all items from this adapter leaving it empty. + */ + public void clear() { + this.count = (mEmptyItem ? 1 : 0); + pages.clear(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + T item = getItem(position); + if (item != null) { + // XXX: This is ugly -- not all adapters need an ImageFetcher. + // We should really have subclasses of types in the model classes, + // with the hierarchy probably being: + // + // [basic item] -> [item with artwork] -> [artwork is downloaded] + // + // instead of special-casing whether or not mImageFetcher is null + // in getAdapterView(). + return mItemView.getAdapterView(convertView, parent, item, mImageFetcher); + } + + return mItemView.getAdapterView(convertView, parent, + (position == 0 && mEmptyItem ? "" : loadingText)); + } + + public String getQuantityString(int size) { + return mItemView.getQuantityString(size); + } + + public ItemListActivity getActivity() { + return mItemView.getActivity(); + } + + public void onItemSelected(int position) { + T item = getItem(position); + if (item != null && item.getId() != null) { + try { + mItemView.onItemSelected(position, item); + } catch (RemoteException e) { + Log.e(TAG, "Error from default action for '" + item + "': " + e); + } + } + } + + /** + * Creates the context menu for the selected item by calling {@link + * ItemView.onCreateContextMenu} which the subclass should have specialised. + *

+ * Unpacks the {@link ContextMenu.ContextMenuInfo} passed to this method, and creates a {@link + * ItemView.ContextMenuInfo} suitable for passing to subclasses of {@link BaseItemView}. + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenu.ContextMenuInfo menuInfo) { + AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; + final T selectedItem = getItem(adapterMenuInfo.position); + + ItemView.ContextMenuInfo c = new ItemView.ContextMenuInfo( + adapterMenuInfo.position, selectedItem, this, + getActivity().getMenuInflater()); + + if (selectedItem != null && selectedItem.getId() != null) { + mItemView.onCreateContextMenu(menu, v, c); + } + } + + public boolean doItemContext(MenuItem menuItem, int position) { + try { + return mItemView.doItemContext(menuItem, position, getItem(position)); + } catch (RemoteException e) { + Item item = getItem(position); + Log.e(TAG, "Error executing context menu action '" + menuItem.getMenuInfo() + "' for '" + + item + "': " + e); + return false; + } + } + + public ItemView getItemView() { + return mItemView; + } + + public void setItemView(ItemView itemView) { + mItemView = itemView; + } + + @Override + public int getCount() { + return count; + } + + private T[] getPage(int position) { + int pageNumber = pageNumber(position); + T[] page = pages.get(pageNumber); + if (page == null) { + pages.put(pageNumber, page = arrayInstance(pageSize)); + } + return page; + } + + private void setItems(int start, List items) { + T[] page = getPage(start); + int offset = start % pageSize; + for (T item : items) { + if (offset >= pageSize) { + start += offset; + page = getPage(start); + offset = 0; + } + page[offset++] = item; + } + } + + @Override + public T getItem(int position) { + T item = getPage(position)[position % pageSize]; + if (item == null) { + if (mEmptyItem) { + position--; + } + getActivity().maybeOrderPage(pageNumber(position) * pageSize); + } + return item; + } + + public void setItem(int position, T item) { + getPage(position)[position % pageSize] = item; + } + + @Override + public long getItemId(int position) { + return position; + } + + /** + * Generates a string suitable for use as an activity's title. + * + * @return the title. + */ + public String getHeader() { + String item_text = getQuantityString(getCount()); + return getActivity().getString(R.string.browse_items_text, item_text, getCount()); + } + + /** + * Called when the number of items in the list changes. The default implementation is empty. + */ + protected void onCountUpdated() { + } + + /** + * Update the contents of the items in this list. + *

+ * The size of the list of items is automatically adjusted if necessary, to obey the given + * parameters. + * + * @param count Number of items as reported by squeezeserver. + * @param start The start position of items in this update. + * @param items New items to insert in the main list + */ + public void update(int count, int start, List items) { + int offset = (mEmptyItem ? 1 : 0); + count += offset; + start += offset; + if (count == 0 || count != getCount()) { + this.count = count; + onCountUpdated(); + } + setItems(start, items); + + notifyDataSetChanged(); + } + + /** + * @param item + * + * @return The position of the given item in this adapter or 0 if not found + */ + public int findItem(T item) { + for (int pos = 0; pos < getCount(); pos++) { + if (getItem(pos) == null) { + if (item == null) { + return pos; + } + } else if (getItem(pos).equals(item)) { + return pos; + } + } + return 0; + } + + protected T[] arrayInstance(int size) { + return mItemView.getCreator().newArray(size); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemListActivity.java new file mode 100644 index 000000000..647178ef6 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemListActivity.java @@ -0,0 +1,428 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + + +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.menu.BaseMenuFragment; +import uk.org.ngo.squeezer.menu.MenuFragment; +import uk.org.ngo.squeezer.service.SqueezeService; +import uk.org.ngo.squeezer.util.ImageCache; +import uk.org.ngo.squeezer.util.ImageFetcher; +import uk.org.ngo.squeezer.util.RetainFragment; + +/** + * This class defines the common minimum, which any activity browsing the SqueezeServer's database + * must implement. + * + * @author Kurt Aaholst + */ +public abstract class ItemListActivity extends BaseActivity { + + private static final String TAG = ItemListActivity.class.getName(); + + /** + * The list is being actively scrolled by the user + */ + private boolean mListScrolling; + + /** + * Keep track of whether callbacks have been registered + */ + private boolean mRegisteredCallbacks; + + /** + * The number of items per page. + */ + private int mPageSize; + + /** + * The pages that have been requested from the server. + */ + private Set mOrderedPages = new HashSet(); + + /** + * The pages that have been received from the server + */ + private Set mReceivedPages; + + /** + * Tag for mReceivedPages in mRetainFragment. + */ + private static final String TAG_RECEIVED_PAGES = "mReceivedPages"; + + /** + * An ImageFetcher for loading thumbnails. + */ + private ImageFetcher mImageFetcher; + + /** + * Tag for _mImageFetcher in mRetainFragment. + */ + public static final String TAG_IMAGE_FETCHER = "imageFetcher"; + + /** + * ImageCache parameters for the album art. + */ + private ImageCache.ImageCacheParams mImageCacheParams; + + /* Fragment to retain information across orientation changes. */ + private RetainFragment mRetainFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mPageSize = getResources().getInteger(R.integer.PageSize); + + BaseMenuFragment.add(this, MenuFragment.class); + + mRetainFragment = RetainFragment.getInstance(TAG, getSupportFragmentManager()); + + //noinspection unchecked + mReceivedPages = (Set) mRetainFragment.get(TAG_RECEIVED_PAGES); + if (mReceivedPages == null) { + mReceivedPages = new HashSet(); + mRetainFragment.put(TAG_RECEIVED_PAGES, mReceivedPages); + } + } + + @Override + public void onResume() { + super.onResume(); + + getImageFetcher().addImageCache(getSupportFragmentManager(), mImageCacheParams); + + if (getService() != null) { + maybeRegisterCallbacks(); + } + } + + @Override + public void onPause() { + if (mImageFetcher != null) { + mImageFetcher.closeCache(); + } + + if (mRegisteredCallbacks) { + if (getService() != null) { + try { + unregisterCallback(); + } catch (RemoteException e) { + Log.e(getTag(), "Error unregistering list callback: " + e); + } + } + mRegisteredCallbacks = false; + } + + // Any items coming in after callbacks have been unregistered are discarded. + // We cancel any outstanding orders, so items can be reordered after the + // activity resumes. + cancelOrders(); + + super.onPause(); + } + + @Override + protected void onServiceConnected() { + maybeRegisterCallbacks(); + } + + /** + * This is called when the service is first connected, and whenever the activity is resumed. + */ + private void maybeRegisterCallbacks() { + if (!mRegisteredCallbacks) { + try { + registerCallback(); + } catch (RemoteException e) { + Log.e(getTag(), "Error registering list callback: " + e); + } + mRegisteredCallbacks = true; + } + } + + /** + * This is called when the service is connected. + *

+ * You must register a callback for {@link SqueezeService} to call when the ordered items from + * {@link #orderPage(int)} are received from SqueezeServer. This callback must pass these items + * on to {@link ItemListAdapter#update(int, int, List)}. + * + * @throws RemoteException + */ + protected abstract void registerCallback() throws RemoteException; + + /** + * This is called when the service is disconnected. + * + * @throws RemoteException + */ + protected abstract void unregisterCallback() throws RemoteException; + + protected ImageFetcher createImageFetcher() { + // Get an ImageFetcher to scale artwork to the size of the icon view. + Resources resources = getResources(); + int iconSize = (Math.max( + resources.getDimensionPixelSize(R.dimen.album_art_icon_height), + resources.getDimensionPixelSize(R.dimen.album_art_icon_width))); + ImageFetcher imageFetcher = new ImageFetcher(this, iconSize); + imageFetcher.setLoadingImage(R.drawable.icon_pending_artwork); + return imageFetcher; + } + + protected void createImageCacheParams() { + mImageCacheParams = new ImageCache.ImageCacheParams(this, "artwork"); + mImageCacheParams.setMemCacheSizePercent(this, 0.12f); + } + + public ImageFetcher getImageFetcher() { + if (mImageFetcher == null) { + mImageFetcher = (ImageFetcher) mRetainFragment.get(TAG_IMAGE_FETCHER); + if (mImageFetcher == null) { + mImageFetcher = createImageFetcher(); + createImageCacheParams(); + mRetainFragment.put(TAG_IMAGE_FETCHER, mImageFetcher); + } + } + + return mImageFetcher; + } + + + /** + * Implementations must start an asynchronous fetch of items, when this is called. + * + * @param start Position in list to start the fetch. Pass this on to {@link + * uk.org.ngo.squeezer.service.SqueezeService} + * + * @throws RemoteException + */ + protected abstract void orderPage(int start) throws RemoteException; + + /** + * List can clear any information about which items have been received and ordered, by calling + * {@link #clearAndReOrderItems()}. This will call back to this method, which must clear any + * adapters holding items. + */ + protected abstract void clearItemAdapter(); + + /** + * Order a page worth of data, starting at the specified position, if it has not already been + * ordered. + * + * @param pagePosition position in the list to start the fetch. + * + * @return True if the page needed to be ordered (even if the order failed), false otherwise. + */ + public boolean maybeOrderPage(int pagePosition) { + if (!mListScrolling && !mReceivedPages.contains(pagePosition) && !mOrderedPages + .contains(pagePosition)) { + mOrderedPages.add(pagePosition); + try { + orderPage(pagePosition); + } catch (RemoteException e) { + mOrderedPages.remove(pagePosition); + Log.e(getTag(), "Error ordering items (" + pagePosition + "): " + e); + } + return true; + } else { + return false; + } + } + + /** + * Orders pages that correspond to visible rows in the listview. + *

+ * Computes the pages that correspond to the rows that are currently being displayed by the + * listview, and calls {@link #maybeOrderPage(int)} to fetch the page if necessary. + * + * @param listView The listview with visible rows. + */ + public void maybeOrderVisiblePages(AbsListView listView) { + int pos = (listView.getFirstVisiblePosition() / mPageSize) * mPageSize; + int end = listView.getFirstVisiblePosition() + listView.getChildCount(); + + while (pos <= end) { + maybeOrderPage(pos); + pos += mPageSize; + } + } + + /** + * Tracks items that have been received from the server. + *

+ * Subclasses must call this method when receiving data from the server to ensure that + * internal bookkeeping about pages that have/have not been ordered is kept consistent. + * + * @param count The total number of items known by the server. + * @param start The start position of this update. + * @param size The number of items in this update + */ + protected void onItemsReceived(final int count, final int start, int size) { + Log.d(getTag(), "onItemsReceived(" + count + ", " + start + ", " + size + ")"); + + // Add this page of data to mReceivedPages and remove from mOrderedPages. + // Because we might receive a page in chunks, we test for the end of a page, + // before we register the page as being received. + if (((start + size) % mPageSize == 0) || (start + size == count)) { + int pageStart = (start + size == count) ? start : start + size - mPageSize; + mReceivedPages.add(pageStart); + mOrderedPages.remove(pageStart); + } + } + + /** + * Empties the variables that track which pages have been requested, and orders page 0. + */ + public void clearAndReOrderItems() { + mOrderedPages.clear(); + mReceivedPages.clear(); + maybeOrderPage(0); + clearItemAdapter(); + } + + /** + * Removes any outstanding requests from mOrderedPages. + */ + private void cancelOrders() { + if (mRegisteredCallbacks) { + throw new IllegalStateException( + "Cannot call cancelOrders with mRegisteredCallbacks == true"); + } + + mOrderedPages.clear(); + } + + /** + * Tracks scrolling activity. + *

+ * When the list is idle, new pages of data are fetched from the server. + *

+ * Use a TouchListener to work around an Android bug where SCROLL_STATE_IDLE messages are not + * delivered after SCROLL_STATE_TOUCH_SCROLL messages. + */ + protected class ScrollListener implements AbsListView.OnScrollListener { + + private TouchListener mTouchListener = null; + + private boolean mAttachedTouchListener = false; + + private int mPrevScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * Sets up the TouchListener. + *

+ * Subclasses must call this. + */ + public ScrollListener() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR && + Build.VERSION.SDK_INT <= Build.VERSION_CODES.FROYO) { + mTouchListener = new TouchListener(this); + } + } + + @Override + public void onScrollStateChanged(AbsListView listView, int scrollState) { + if (scrollState == mPrevScrollState) { + return; + } + + if (mAttachedTouchListener == false) { + if (mTouchListener != null) { + listView.setOnTouchListener(mTouchListener); + } + mAttachedTouchListener = true; + } + + switch (scrollState) { + case OnScrollListener.SCROLL_STATE_IDLE: + mListScrolling = false; + maybeOrderVisiblePages(listView); + break; + + case OnScrollListener.SCROLL_STATE_FLING: + case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL: + mListScrolling = true; + break; + } + + mPrevScrollState = scrollState; + } + + // Do not use: is not called when the scroll completes, appears to be + // called multiple time during a scroll, including during flinging. + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + } + + /** + * Work around a bug in (at least) API levels 7 and 8. + *

+ * The bug manifests itself like so: after completing a TOUCH_SCROLL the system does not + * deliver a SCROLL_STATE_IDLE message to any attached listeners. + *

+ * In addition, if the user does TOUCH_SCROLL, IDLE, TOUCH_SCROLL you would expect to + * receive three messages. You don't -- you get the first TOUCH_SCROLL, no IDLE message, and + * then the second touch doesn't generate a second TOUCH_SCROLL message. + *

+ * This state clears when the user flings the list. + *

+ * The simplest work around for this app is to track the user's finger, and if the previous + * state was TOUCH_SCROLL then pretend that they finished with a FLING and an IDLE event was + * triggered. This serves to unstick the message pipeline. + */ + protected class TouchListener implements View.OnTouchListener { + + private final OnScrollListener mOnScrollListener; + + public TouchListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + } + + @Override + public boolean onTouch(View view, MotionEvent event) { + final int action = event.getAction(); + boolean mFingerUp = action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_CANCEL; + if (mFingerUp && mPrevScrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + Log.v(TAG, "Sending special scroll state bump"); + mOnScrollListener.onScrollStateChanged((AbsListView) view, + OnScrollListener.SCROLL_STATE_FLING); + mOnScrollListener.onScrollStateChanged((AbsListView) view, + OnScrollListener.SCROLL_STATE_IDLE); + } + return false; + } + } + } +} diff --git a/src/uk/org/ngo/squeezer/framework/SqueezerItemListAdapter.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemListAdapter.java similarity index 63% rename from src/uk/org/ngo/squeezer/framework/SqueezerItemListAdapter.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemListAdapter.java index 965714af4..28043f493 100644 --- a/src/uk/org/ngo/squeezer/framework/SqueezerItemListAdapter.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemListAdapter.java @@ -19,28 +19,27 @@ import uk.org.ngo.squeezer.util.ImageFetcher; /** - * Specialization of {@link SqueezerItemAdapter} to be used in - * {@link SqueezerBaseListActivity}. - *

- * Only difference is that the activity's title is automatically updated to - * reflect the number of items being shown. - * + * Specialization of {@link ItemAdapter} to be used in {@link BaseListActivity}. + *

+ * Only difference is that the activity's title is automatically updated to reflect the number of + * items being shown. + * * @param Denotes the class of the items this class should list + * * @author Kurt Aaholst */ -public class SqueezerItemListAdapter extends SqueezerItemAdapter { +public class ItemListAdapter extends ItemAdapter { /** - * Calls - * {@link SqueezerItemAdapter#SqueezerBaseAdapter(SqueezerItemView, ImageFetcher)} + * Calls {@link ItemAdapter#BaseAdapter(ItemView, ImageFetcher)} */ - public SqueezerItemListAdapter(SqueezerItemView itemView, ImageFetcher imageFetcher) { + public ItemListAdapter(ItemView itemView, ImageFetcher imageFetcher) { super(itemView, imageFetcher); } - @Override - protected void onCountUpdated() { - getActivity().setTitle(getHeader()); - } + @Override + protected void onCountUpdated() { + getActivity().setTitle(getHeader()); + } -} \ No newline at end of file +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemView.java new file mode 100644 index 000000000..ff2cfa4ec --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/ItemView.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + +import android.os.Parcelable.Creator; +import android.os.RemoteException; +import android.view.ContextMenu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import uk.org.ngo.squeezer.util.ImageFetcher; + + +/** + * Defines view logic for a {@link Item} + *

+ * We keep this here because we don't want to pollute the model with view related stuff. + *

+ * Currently this is the only logic class you have to implement for each SqueezeServer data type, so + * it contains a few methods, which are not strictly view related. + *

+ * {@link BaseItemView} implements all the common functionality, an some sensible defaults. + * + * @param Denotes the class of the item this class implements view logic for + * + * @author Kurt Aaholst + */ +public interface ItemView { + + /** + * @return The activity associated with this view logic + */ + ItemListActivity getActivity(); + + /** + * @return {@link Resources#getQuantityString(int, int)} + */ + String getQuantityString(int quantity); + + /** + * Gets a {@link android.view.View} that displays the data at the specified position in the data + * set. See {@link ItemAdapter#getView(int, View, android.view.ViewGroup)} + * + * @param convertView the old view to reuse, per {@link Adapter#getView(int, View, + * android.view.ViewGroup)} + * @param item the item to display. + * @param imageFetcher an {@link ImageFetcher} configured to load image thumbnails. + * + * @return the view to display. + */ + View getAdapterView(View convertView, ViewGroup parent, T item, ImageFetcher imageFetcher); + + /** + * Gets a {@link android.view.View} suitable for displaying the supplied (static) text. See + * {@link ItemAdapter#getView(int, View, android.view.ViewGroup)} + * + * @param convertView The old view to reuse, per {@link android.widget.Adapter#getView(int, + * View, android.view.ViewGroup)} + * @param text text to display + * + * @return the view to display. + */ + View getAdapterView(View convertView, ViewGroup parent, String text); + + /** + * @return The generic argument of the implementation + */ + Class getItemClass(); + + /** + * @return the creator for the current {@link Item} implementation + */ + Creator getCreator(); + + /** + * Implement the action to be taken when an item is selected. + * + * @param index Position in the list of the selected item. + * @param item The selected item. This may be null if + * + * @throws RemoteException + */ + void onItemSelected(int index, T item) throws RemoteException; + + /** + * Creates the context menu, and sets the menu's title to the name of the item that it is the + * context menu. + *

+ * Subclasses with no context menu should override this method and do nothing. + *

+ * Subclasses with a context menu should call this method, then inflate their context menu and + * perform any adjustments to it before returning. + * + * @param menu + * @param v + * @param menuInfo + * + * @see OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, + * android.view.ContextMenu.ContextMenuInfo) + */ + public void onCreateContextMenu(ContextMenu menu, View v, + ItemView.ContextMenuInfo menuInfo); + + /** + * Perform the selected action from the context menu for the selected item. + * + * @param selectedItem The item the context menu was generated for + * @param menuItem The selected menu action + * + * @return True if the action was consumed + * + * @throws RemoteException + * @see {@link Activity#onContextItemSelected(MenuItem)} + */ + public boolean doItemContext(MenuItem menuItem, int index, T selectedItems) + throws RemoteException; + + /** + * Extra menu information provided to the {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, + * View, ContextMenuInfo) } callback when a context menu is brought up for this ItemView. + */ + public static class ContextMenuInfo implements ContextMenu.ContextMenuInfo { + + /** + * The position in the adapter for which the context menu is being displayed. + */ + public int position; + + /** + * The {@link Item} for which the context menu is being displayed. + */ + public Item item; + + /** + * The {@link ItemAdapter} that is bridging the content to the listview. + */ + public ItemAdapter adapter; + + /** + * A {@link android.view.MenuInflater} that can be used to inflate a menu resource. + */ + public MenuInflater menuInflater; + + public ContextMenuInfo(int position, Item item, ItemAdapter adapter, + MenuInflater menuInflater) { + this.position = position; + this.item = item; + this.adapter = adapter; + this.menuInflater = menuInflater; + } + } +} diff --git a/src/uk/org/ngo/squeezer/framework/SqueezerPlaylistItem.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/PlaylistItem.java similarity index 62% rename from src/uk/org/ngo/squeezer/framework/SqueezerPlaylistItem.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/framework/PlaylistItem.java index 28dcca82e..6ba89d5cc 100644 --- a/src/uk/org/ngo/squeezer/framework/SqueezerPlaylistItem.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/PlaylistItem.java @@ -17,20 +17,19 @@ package uk.org.ngo.squeezer.framework; /** - * Items that can be added to Squeezerserver playlists (anything that can be - * passed to the playlistcontrol command) should derive from this - * class and implement {@link #getPlaylistTag()} to provide the correct playlist - * tag. - *

- * See {@link SqueezerBaseActivity#playlistControl}. - * + * Items that can be added to Squeezeserver playlists (anything that can be passed to the + * playlistcontrol command) should derive from this class and implement {@link + * #getPlaylistTag()} to provide the correct playlist tag. + *

+ * See {@link BaseActivity#playlistControl}. + * * @author nik */ -public abstract class SqueezerPlaylistItem extends SqueezerItem { +public abstract class PlaylistItem extends Item { + /** - * Fetches the tag that represents this item in a - * playlistcontrol command. - * + * Fetches the tag that represents this item in a playlistcontrol command. + * * @return the tag, e.g., "album_id". */ abstract public String getPlaylistTag(); diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/PlaylistItemView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/PlaylistItemView.java new file mode 100644 index 000000000..72c845392 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/framework/PlaylistItemView.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.framework; + + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.RemoteException; +import android.util.Log; + +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.itemlist.action.PlayableItemAction; + +/** + * Represents the view hierarchy for a single {@link PlaylistItem} subclass. with a configurable on + * select action. + * + * @param + */ +public abstract class PlaylistItemView extends + BaseItemView implements OnSharedPreferenceChangeListener { + + protected SharedPreferences preferences; + + protected PlayableItemAction onSelectAction; + + public PlaylistItemView(ItemListActivity activity) { + super(activity); + preferences = activity.getSharedPreferences(Preferences.NAME, 0); + preferences.registerOnSharedPreferenceChangeListener(this); + onSelectAction = getOnSelectAction(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + onSelectAction = getOnSelectAction(); + } + + abstract protected PlayableItemAction getOnSelectAction(); + + @Override + public void onItemSelected(int index, T item) throws RemoteException { + Log.d(getTag(), "Executing on select action"); + if (onSelectAction != null) { + onSelectAction.execute(item); + } + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AbstractSongListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AbstractSongListActivity.java new file mode 100644 index 000000000..807831a6d --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AbstractSongListActivity.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.model.Song; + +public abstract class AbstractSongListActivity extends BaseListActivity { + + @Override + protected void registerCallback() throws RemoteException { + getService().registerSongListCallback(songListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterSongListCallback(songListCallback); + } + + private final IServiceSongListCallback songListCallback = new IServiceSongListCallback.Stub() { + public void onSongsReceived(int count, int start, List items) { + onItemsReceived(count, start, items); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumArtView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumArtView.java new file mode 100644 index 000000000..0d181e960 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumArtView.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + + +import android.os.RemoteException; +import android.util.Log; +import android.view.View; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ArtworkItem; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItemView; +import uk.org.ngo.squeezer.service.ISqueezeService; + +/** + * Represents the view hierarchy for a single {@link uk.org.ngo.squeezer.framework.Item} subclass. + * where the item has track artwork associated with it. + * + * @param + */ +public abstract class AlbumArtView extends + PlaylistItemView { + + public AlbumArtView(ItemListActivity activity) { + super(activity); + + setViewParams(EnumSet.of(ViewParams.ICON, ViewParams.TWO_LINE, ViewParams.CONTEXT_BUTTON)); + setLoadingViewParams(EnumSet.of(ViewParams.ICON, ViewParams.TWO_LINE)); + } + + /** + * Binds the label to {@link ViewHolder#text1}. Sets {@link ViewHolder#icon} to the generic + * pending icon, and clears {@link ViewHolder#text2}. + * + * @param view The view that contains the {@link ViewHolder} + * @param text The text to bind to {@link ViewHolder#text1} + */ + @Override + public void bindView(View view, String text) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.icon.setImageResource(R.drawable.icon_pending_artwork); + viewHolder.text1.setText(text); + viewHolder.text2.setText(""); + } + + /** + * Returns the URL to download the specified album artwork, or null if the artwork does not + * exist, or there was a problem with the service. + * + * @param artwork_track_id + * + * @return + */ + protected String getAlbumArtUrl(String artwork_track_id) { + if (artwork_track_id == null) { + return null; + } + + ISqueezeService service = getActivity().getService(); + if (service == null) { + return null; + } + + try { + return service.getAlbumArtUrl(artwork_track_id); + } catch (RemoteException e) { + Log.e(getClass().getSimpleName(), "Error requesting album art url: " + e); + return null; + } + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumGridView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumGridView.java new file mode 100644 index 000000000..79144bc37 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumGridView.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2013 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.view.View; +import android.view.ViewGroup; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemListActivity; + +/** + * Shows a single album with its artwork, and a context menu. + */ +public class AlbumGridView extends AlbumView { + + public AlbumGridView(ItemListActivity activity) { + super(activity); + } + + @Override + public View getAdapterView(View convertView, ViewGroup parent, EnumSet viewParams) { + return getAdapterView(convertView, parent, viewParams, R.layout.grid_item); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumListActivity.java new file mode 100644 index 000000000..472b0973e --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumListActivity.java @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.EnumSet; +import java.util.List; + +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.itemlist.GenreSpinner.GenreSpinnerCallback; +import uk.org.ngo.squeezer.itemlist.YearSpinner.YearSpinnerCallback; +import uk.org.ngo.squeezer.itemlist.dialog.AlbumFilterDialog; +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog; +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog.AlbumListLayout; +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog.AlbumsSortOrder; +import uk.org.ngo.squeezer.menu.BaseMenuFragment; +import uk.org.ngo.squeezer.menu.FilterMenuFragment; +import uk.org.ngo.squeezer.menu.FilterMenuFragment.FilterableListActivity; +import uk.org.ngo.squeezer.menu.ViewMenuItemFragment; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.model.Year; +import uk.org.ngo.squeezer.util.ImageFetcher; + +/** + * Lists albums, optionally filtered to match specific criteria. + */ +public class AlbumListActivity extends BaseListActivity + implements GenreSpinnerCallback, YearSpinnerCallback, + FilterableListActivity, ViewMenuItemFragment.ListActivityWithViewMenu { + + private AlbumsSortOrder sortOrder = null; + + private AlbumListLayout listLayout = null; + + private String searchString = null; + + public String getSearchString() { + return searchString; + } + + public void setSearchString(String searchString) { + this.searchString = searchString; + } + + private Song song; + + public Song getSong() { + return song; + } + + public void setSong(Song song) { + this.song = song; + } + + private Artist artist; + + public Artist getArtist() { + return artist; + } + + public void setArtist(Artist artist) { + this.artist = artist; + } + + private Year year; + + @Override + public Year getYear() { + return year; + } + + @Override + public void setYear(Year year) { + this.year = year; + } + + private Genre genre; + + @Override + public Genre getGenre() { + return genre; + } + + @Override + public void setGenre(Genre genre) { + this.genre = genre; + } + + private GenreSpinner genreSpinner; + + public void setGenreSpinner(Spinner spinner) { + genreSpinner = new GenreSpinner(this, this, spinner); + } + + private YearSpinner yearSpinner; + + public void setYearSpinner(Spinner spinner) { + yearSpinner = new YearSpinner(this, this, spinner); + } + + @Override + public ItemView createItemView() { + return (listLayout == AlbumListLayout.grid) ? new AlbumGridView(this) : new AlbumView(this); + } + + @Override + protected ImageFetcher createImageFetcher() { + // Get an ImageFetcher to scale artwork to the size of the icon view. + Resources resources = getResources(); + int height, width; + if (listLayout == AlbumListLayout.grid) { + height = resources.getDimensionPixelSize(R.dimen.album_art_icon_grid_height); + width = resources.getDimensionPixelSize(R.dimen.album_art_icon_grid_width); + } else { + height = resources.getDimensionPixelSize(R.dimen.album_art_icon_height); + width = resources.getDimensionPixelSize(R.dimen.album_art_icon_width); + } + ImageFetcher imageFetcher = new ImageFetcher(this, Math.max(height, width)); + imageFetcher.setLoadingImage(R.drawable.icon_pending_artwork); + return imageFetcher; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + setListLayout(); + super.onCreate(savedInstanceState); + + BaseMenuFragment.add(this, FilterMenuFragment.class); + BaseMenuFragment.add(this, ViewMenuItemFragment.class); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + if (Artist.class.getName().equals(key)) { + artist = extras.getParcelable(key); + } else if (Year.class.getName().equals(key)) { + year = extras.getParcelable(key); + } else if (Genre.class.getName().equals(key)) { + genre = extras.getParcelable(key); + } else if (Song.class.getName().equals(key)) { + song = extras.getParcelable(key); + } else if (AlbumsSortOrder.class.getName().equals(key)) { + sortOrder = AlbumsSortOrder.valueOf(extras.getString(key)); + } else { + Log.e(getTag(), "Unexpected extra value: " + key + "(" + + extras.get(key).getClass().getName() + ")"); + } + } + } + + TextView header = (TextView) findViewById(R.id.header); + EnumSet details = EnumSet.allOf(AlbumView.Details.class); + if (artist != null) { + details.remove(AlbumView.Details.ARTIST); + header.setText(getString(R.string.albums_by_artist_header, artist.getName())); + header.setVisibility(View.VISIBLE); + } + if (genre != null) { + details.remove(AlbumView.Details.GENRE); + header.setText(getString(R.string.albums_by_genre_header, genre.getName())); + header.setVisibility(View.VISIBLE); + } + if (year != null) { + details.remove(AlbumView.Details.YEAR); + header.setText(getString(R.string.albums_by_year_header, year.getName())); + header.setVisibility(View.VISIBLE); + } + ((AlbumView) getItemView()).setDetails(details); + } + + @Override + protected int getContentView() { + return (listLayout == AlbumListLayout.grid) ? R.layout.item_grid + : R.layout.item_list_albums; + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerAlbumListCallback(albumListCallback); + if (genreSpinner != null) { + genreSpinner.registerCallback(); + } + if (yearSpinner != null) { + yearSpinner.registerCallback(); + } + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterAlbumListCallback(albumListCallback); + if (genreSpinner != null) { + genreSpinner.unregisterCallback(); + } + if (yearSpinner != null) { + yearSpinner.unregisterCallback(); + } + } + + @Override + protected void orderPage(int start) throws RemoteException { + if (sortOrder == null) { + try { + sortOrder = AlbumsSortOrder.valueOf(getService().preferredAlbumSort()); + } catch (IllegalArgumentException e) { + Log.w(getTag(), "Unknown preferred album sort: " + e); + sortOrder = AlbumsSortOrder.album; + } + } + + getService().albums(start, sortOrder.name().replace("__", ""), getSearchString(), artist, + getYear(), getGenre(), song); + } + + public AlbumsSortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(AlbumsSortOrder sortOrder) { + this.sortOrder = sortOrder; + getIntent().putExtra(AlbumsSortOrder.class.getName(), sortOrder.name()); + clearAndReOrderItems(); + } + + public AlbumListLayout getListLayout() { + return listLayout; + } + + /** + * Set the preferred album list layout. + *

+ * If the list layout is not selected, a default one is chosen, based on the current screen + * size, on the assumption that the artwork grid is preferred on larger screens. + */ + private void setListLayout() { + SharedPreferences preferences = getSharedPreferences(Preferences.NAME, 0); + String listLayoutString = preferences.getString(Preferences.KEY_ALBUM_LIST_LAYOUT, null); + if (listLayoutString == null) { + int screenSize = getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK; + listLayout = (screenSize >= Configuration.SCREENLAYOUT_SIZE_LARGE) + ? AlbumListLayout.grid : AlbumListLayout.list; + } else { + listLayout = AlbumListLayout.valueOf(listLayoutString); + } + } + + public void setListLayout(AlbumListLayout listLayout) { + SharedPreferences preferences = getSharedPreferences(Preferences.NAME, 0); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(Preferences.KEY_ALBUM_LIST_LAYOUT, listLayout.name()); + editor.commit(); + + startActivity(getIntent()); + finish(); + } + + @Override + public boolean onSearchRequested() { + showFilterDialog(); + return false; + } + + @Override + public void showFilterDialog() { + new AlbumFilterDialog().show(getSupportFragmentManager(), "AlbumFilterDialog"); + } + + @Override + public void showViewDialog() { + new AlbumViewDialog().show(getSupportFragmentManager(), "AlbumOrderDialog"); + } + + public static void show(Context context, Item... items) { + show(context, null, items); + } + + public static void show(Context context, AlbumsSortOrder sortOrder, Item... items) { + final Intent intent = new Intent(context, AlbumListActivity.class); + if (sortOrder != null) { + intent.putExtra(AlbumsSortOrder.class.getName(), sortOrder.name()); + } + for (Item item : items) { + intent.putExtra(item.getClass().getName(), item); + } + context.startActivity(intent); + } + + private final IServiceAlbumListCallback albumListCallback + = new IServiceAlbumListCallback.Stub() { + @Override + public void onAlbumsReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumView.java new file mode 100644 index 000000000..484d625e1 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/AlbumView.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.view.ContextMenu; +import android.view.View; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.itemlist.action.PlayableItemAction; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.util.ImageFetcher; + +/** + * Shows a single album with its artwork, and a context menu. + */ +public class AlbumView extends AlbumArtView { + + /** + * The details to show in the second line of text. + */ + public enum Details { + /** + * Show the artist name. + */ + ARTIST, + + /** + * Show the year (if known). + */ + YEAR, + + /** + * Show the genre (if known). + */ + GENRE + } + + private EnumSet

mDetails = EnumSet.noneOf(Details.class); + + public AlbumView(ItemListActivity activity) { + super(activity); + } + + public void setDetails(EnumSet
details) { + mDetails = details; + } + + @Override + public void bindView(View view, Album item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + + String text2 = ""; + if (item.getId() != null) { + text2 = mJoiner.join( + mDetails.contains(Details.ARTIST) ? item.getArtist() : null, + mDetails.contains(Details.YEAR) && item.getYear() != 0 ? item.getYear() : null + ); + } + viewHolder.text2.setText(text2); + + String artworkUrl = getAlbumArtUrl(item.getArtwork_track_id()); + if (artworkUrl == null) { + viewHolder.icon.setImageResource(R.drawable.icon_album_noart); + } else { + imageFetcher.loadImage(artworkUrl, viewHolder.icon); + } + } + + @Override + protected PlayableItemAction getOnSelectAction() { + String actionType = preferences.getString(Preferences.KEY_ON_SELECT_ALBUM_ACTION, + PlayableItemAction.Type.BROWSE.name()); + return PlayableItemAction.createAction(getActivity(), actionType); + } + + /** + * Creates the context menu for an album by inflating R.menu.albumcontextmenu. + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + menuInfo.menuInflater.inflate(R.menu.albumcontextmenu, menu); + } + + @Override + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.album, quantity); + } +} diff --git a/src/uk/org/ngo/squeezer/itemlists/SqueezerRadioListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ApplicationListActivity.java similarity index 57% rename from src/uk/org/ngo/squeezer/itemlists/SqueezerRadioListActivity.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ApplicationListActivity.java index 254de8403..ed3780a72 100644 --- a/src/uk/org/ngo/squeezer/itemlists/SqueezerRadioListActivity.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ApplicationListActivity.java @@ -14,31 +14,30 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; +package uk.org.ngo.squeezer.itemlist; - -import uk.org.ngo.squeezer.framework.SqueezerItemView; -import uk.org.ngo.squeezer.model.SqueezerPlugin; import android.content.Context; import android.content.Intent; import android.os.RemoteException; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Plugin; -public class SqueezerRadioListActivity extends SqueezerPluginListActivity{ - @Override - public SqueezerItemView createItemView() { - return new SqueezerRadioView(this); - } +public class ApplicationListActivity extends PluginListActivity { - @Override - protected void orderPage(int start) throws RemoteException { - getService().radios(start); - } + @Override + public ItemView createItemView() { + return new ApplicationView(this); + } + @Override + protected void orderPage(int start) throws RemoteException { + getService().apps(start); + } - public static void show(Context context) { - final Intent intent = new Intent(context, SqueezerRadioListActivity.class); + public static void show(Context context) { + final Intent intent = new Intent(context, ApplicationListActivity.class); context.startActivity(intent); } diff --git a/src/uk/org/ngo/squeezer/itemlists/SqueezerApplicationView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ApplicationView.java similarity index 60% rename from src/uk/org/ngo/squeezer/itemlists/SqueezerApplicationView.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ApplicationView.java index a83a4fb8c..be8b65c57 100644 --- a/src/uk/org/ngo/squeezer/itemlists/SqueezerApplicationView.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ApplicationView.java @@ -14,26 +14,28 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; +package uk.org.ngo.squeezer.itemlist; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.framework.SqueezerBaseListActivity; -import uk.org.ngo.squeezer.model.SqueezerPlugin; import android.view.ContextMenu; import android.view.View; -public class SqueezerApplicationView extends SqueezerPluginView { - public SqueezerApplicationView(SqueezerBaseListActivity activity) { - super(activity); - } +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.model.Plugin; + +public class ApplicationView extends PluginView { - public String getQuantityString(int quantity) { - return getActivity().getResources().getQuantityString(R.plurals.application, quantity); - } + public ApplicationView(BaseListActivity activity) { + super(activity); + } - public void onItemSelected(int index, SqueezerPlugin item) { - //TODO what to do? - } + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.application, quantity); + } + + public void onItemSelected(int index, Plugin item) { + //TODO what to do? + } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ArtistListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ArtistListActivity.java new file mode 100644 index 000000000..1ad69e732 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ArtistListActivity.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.widget.Spinner; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.itemlist.GenreSpinner.GenreSpinnerCallback; +import uk.org.ngo.squeezer.itemlist.dialog.ArtistFilterDialog; +import uk.org.ngo.squeezer.menu.BaseMenuFragment; +import uk.org.ngo.squeezer.menu.FilterMenuFragment; +import uk.org.ngo.squeezer.menu.FilterMenuFragment.FilterableListActivity; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; + +public class ArtistListActivity extends BaseListActivity implements + GenreSpinnerCallback, FilterableListActivity { + + private String searchString = null; + + public String getSearchString() { + return searchString; + } + + public void setSearchString(String searchString) { + this.searchString = searchString; + } + + private Album album; + + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + Genre genre; + + public Genre getGenre() { + return genre; + } + + public void setGenre(Genre genre) { + this.genre = genre; + } + + private GenreSpinner genreSpinner; + + public void setGenreSpinner(Spinner spinner) { + genreSpinner = new GenreSpinner(this, this, spinner); + } + + @Override + public ItemView createItemView() { + return new ArtistView(this); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + BaseMenuFragment.add(this, FilterMenuFragment.class); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + if (Album.class.getName().equals(key)) { + album = extras.getParcelable(key); + } else if (Genre.class.getName().equals(key)) { + genre = extras.getParcelable(key); + } else { + Log.e(getTag(), "Unexpected extra value: " + key + "(" + + extras.get(key).getClass().getName() + ")"); + } + } + } + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerArtistListCallback(artistsListCallback); + if (genreSpinner != null) { + genreSpinner.registerCallback(); + } + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterArtistListCallback(artistsListCallback); + if (genreSpinner != null) { + genreSpinner.unregisterCallback(); + } + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().artists(start, getSearchString(), album, genre); + } + + @Override + public boolean onSearchRequested() { + showFilterDialog(); + return false; + } + + public void showFilterDialog() { + new ArtistFilterDialog().show(getSupportFragmentManager(), "ArtistFilterDialog"); + } + + public static void show(Context context, Item... items) { + final Intent intent = new Intent(context, ArtistListActivity.class); + for (Item item : items) { + intent.putExtra(item.getClass().getName(), item); + } + context.startActivity(intent); + } + + private final IServiceArtistListCallback artistsListCallback + = new IServiceArtistListCallback.Stub() { + public void onArtistsReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ArtistView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ArtistView.java new file mode 100644 index 000000000..64308a93d --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/ArtistView.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.View; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.Artist; + + +public class ArtistView extends BaseItemView { + + public ArtistView(ItemListActivity activity) { + super(activity); + } + + // XXX: Consider making this extend PlaylistItemView and make the action user definable. + public void onItemSelected(int index, Artist item) throws RemoteException { + AlbumListActivity.show(getActivity(), item); + } + + // XXX: Make this a menu resource. + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + menu.add(Menu.NONE, BROWSE_ALBUMS, 0, R.string.BROWSE_ALBUMS); + menu.add(Menu.NONE, R.id.browse_songs, 1, R.string.BROWSE_SONGS); + menu.add(Menu.NONE, R.id.play_now, 2, R.string.PLAY_NOW); + menu.add(Menu.NONE, R.id.add_to_playlist, 3, R.string.ADD_TO_END); + } + + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.artist, quantity); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/CurrentPlaylistActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/CurrentPlaylistActivity.java new file mode 100644 index 000000000..31aa97f0f --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/CurrentPlaylistActivity.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import java.util.EnumSet; +import java.util.List; + +import uk.org.ngo.squeezer.IServiceMusicChangedCallback; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemAdapter; +import uk.org.ngo.squeezer.framework.ItemListAdapter; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistItemMoveDialog; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistSaveDialog; +import uk.org.ngo.squeezer.model.PlayerState; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.util.ImageFetcher; + +import static uk.org.ngo.squeezer.framework.BaseItemView.ViewHolder; + +/** + * Activity that shows the songs in the current playlist. + */ +public class CurrentPlaylistActivity extends BaseListActivity { + + public static void show(Context context) { + final Intent intent = new Intent(context, CurrentPlaylistActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + context.startActivity(intent); + } + + private int currentPlaylistIndex; + + /** + * A list adapter that highlights the view that's currently playing. + */ + private class HighlightingListAdapter extends ItemListAdapter { + + public HighlightingListAdapter(ItemView itemView, + ImageFetcher imageFetcher) { + super(itemView, imageFetcher); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + Object viewTag = view.getTag(); + + // This test because the view tag wont be set until the album is received from the server + if (viewTag != null && viewTag instanceof ViewHolder) { + ViewHolder viewHolder = (ViewHolder) viewTag; + if (position == currentPlaylistIndex) { + viewHolder.text1 + .setTextAppearance(getActivity(), R.style.SqueezerCurrentTextItem); + view.setBackgroundResource(R.drawable.list_item_background_current); + } else { + viewHolder.text1.setTextAppearance(getActivity(), R.style.SqueezerTextItem); + view.setBackgroundResource(R.drawable.list_item_background_normal); + } + } + return view; + } + } + + @Override + protected ItemAdapter createItemListAdapter( + ItemView itemView) { + return new HighlightingListAdapter(itemView, getImageFetcher()); + } + + @Override + public ItemView createItemView() { + SongViewWithArt view = new SongViewWithArt(this) { + /** + * Jumps to whichever song the user chose. + */ + @Override + public void onItemSelected(int index, Song item) throws RemoteException { + getActivity().getService().playlistIndex(index); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + menu.setGroupVisible(R.id.group_playlist, true); + menu.findItem(R.id.add_to_playlist).setVisible(false); + menu.findItem(R.id.play_next).setVisible(false); + + if (menuInfo.position == 0) { + menu.findItem(R.id.playlist_move_up).setVisible(false); + } + + if (menuInfo.position == menuInfo.adapter.getCount() - 1) { + menu.findItem(R.id.playlist_move_down).setVisible(false); + } + } + + @Override + public boolean doItemContext(MenuItem menuItem, int index, Song selectedItem) + throws RemoteException { + switch (menuItem.getItemId()) { + case R.id.play_now: + getService().playlistIndex(index); + return true; + + case R.id.remove_from_playlist: + getService().playlistRemove(index); + clearAndReOrderItems(); + return true; + + case R.id.playlist_move_up: + getService().playlistMove(index, index - 1); + clearAndReOrderItems(); + return true; + + case R.id.playlist_move_down: + getService().playlistMove(index, index + 1); + clearAndReOrderItems(); + return true; + + case R.id.playlist_move: + PlaylistItemMoveDialog.addTo(CurrentPlaylistActivity.this, + index); + return true; + } + + return super.doItemContext(menuItem, index, selectedItem); + } + }; + + view.setDetails(EnumSet.of( + SongView.Details.DURATION, + SongView.Details.ALBUM, + SongView.Details.ARTIST)); + + return view; + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().currentPlaylist(start); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.currentplaylistmenu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_item_playlist_clear: + if (getService() != null) { + try { + getService().playlistClear(); + finish(); + } catch (RemoteException e) { + Log.e(getTag(), "Error trying to clear playlist: " + e); + } + } + return true; + case R.id.menu_item_playlist_save: + PlaylistSaveDialog.addTo(this, getCurrentPlaylist()); + return true; + } + return super.onOptionsItemSelected(item); + } + + private String getCurrentPlaylist() { + if (getService() == null) { + return null; + } + try { + return getService().getCurrentPlaylist(); + } catch (RemoteException e) { + Log.e(getTag(), "Service exception in getCurrentPlaylist(): " + e); + } + return null; + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerCurrentPlaylistCallback(currentPlaylistCallback); + getService().registerSongListCallback(songListCallback); + getService().registerMusicChangedCallback(musicChangedCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterCurrentPlaylistCallback(currentPlaylistCallback); + getService().unregisterSongListCallback(songListCallback); + getService().unregisterMusicChangedCallback(musicChangedCallback); + } + + private final IServiceCurrentPlaylistCallback currentPlaylistCallback + = new IServiceCurrentPlaylistCallback.Stub() { + @Override + public void onAddTracks(PlayerState playerState) { + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + clearAndReOrderItems(); + getItemAdapter().notifyDataSetChanged(); + } + }); + } + + public void onDelete(PlayerState playerState, int index) { + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + // TODO: Investigate feasibility of deleting single items from the adapter. + clearAndReOrderItems(); + getItemAdapter().notifyDataSetChanged(); + } + }); + } + }; + + private final IServiceMusicChangedCallback musicChangedCallback + = new IServiceMusicChangedCallback.Stub() { + @Override + public void onMusicChanged(PlayerState playerState) throws RemoteException { + Log.d(getTag(), "onMusicChanged " + playerState.getCurrentSong()); + currentPlaylistIndex = playerState.getCurrentPlaylistIndex(); + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + getItemAdapter().notifyDataSetChanged(); + } + }); + } + }; + + private final IServiceSongListCallback songListCallback = new IServiceSongListCallback.Stub() { + @Override + public void onSongsReceived(int count, int start, List items) throws RemoteException { + currentPlaylistIndex = getService().getPlayerState().getCurrentPlaylistIndex(); + onItemsReceived(count, start, items); + // Initially position the list at the currently playing song. + // Do it again once it has loaded because the newly displayed items + // may push the current song outside the displayed area. + if (start == 0 || (start <= currentPlaylistIndex && currentPlaylistIndex < start + items + .size())) { + selectCurrentSong(currentPlaylistIndex, start); + } + } + }; + + private void selectCurrentSong(final int currentPlaylistIndex, final int start) { + Log.i(getTag(), "set selection(" + start + "): " + currentPlaylistIndex); + getListView().post(new Runnable() { + @Override + public void run() { + // TODO: this doesn't work if the current playlist is displayed in a grid + ((ListView) getListView()).setSelectionFromTop(currentPlaylistIndex, 0); + } + }); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/FavoriteListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/FavoriteListActivity.java new file mode 100644 index 000000000..03d188e0e --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/FavoriteListActivity.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.app.Activity; +import android.content.Intent; + +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Plugin; +import uk.org.ngo.squeezer.model.PluginItem; + +/** + * List server favourites. + *

+ * A specialisation of {@link PluginItemListActivity} that uses a {@link + * uk.org.ngo.squeezer.itemlist.FavoritesView} for each item. + */ +public class FavoriteListActivity extends PluginItemListActivity { + + @Override + public ItemView createItemView() { + return new FavoritesView(this); + } + + public static void show(Activity activity) { + show(activity, Plugin.FAVORITE); + } + + public static void show(Activity activity, Plugin plugin) { + final Intent intent = new Intent(activity, FavoriteListActivity.class); + intent.putExtra(plugin.getClass().getName(), plugin); + activity.startActivity(intent); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/FavoritesView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/FavoritesView.java new file mode 100644 index 000000000..dfd3b12a6 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/FavoritesView.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.itemlist; + +import uk.org.ngo.squeezer.R; + +/** + * A specialisation of {@link PluginItemView} with a custom {@code getQuantityMethod()} so that the + * activity title is displayed correctly. + */ +public class FavoritesView extends PluginItemView { + + public FavoritesView(PluginItemListActivity activity) { + super(activity); + } + + @Override + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.favorites, quantity); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreListActivity.java new file mode 100644 index 000000000..24290e860 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreListActivity.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Genre; + +public class GenreListActivity extends BaseListActivity { + + @Override + public ItemView createItemView() { + return new GenreView(this); + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerGenreListCallback(genreListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterGenreListCallback(genreListCallback); + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().genres(start, null); + } + + + public static void show(Context context) { + final Intent intent = new Intent(context, GenreListActivity.class); + context.startActivity(intent); + } + + private final IServiceGenreListCallback genreListCallback + = new IServiceGenreListCallback.Stub() { + public void onGenresReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreSpinner.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreSpinner.java new file mode 100644 index 000000000..e16a641a6 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreSpinner.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Spinner; + +import java.util.List; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.ItemAdapter; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public class GenreSpinner { + + private static final String TAG = GenreSpinner.class.getName(); + + GenreSpinnerCallback callback; + + private final ItemListActivity activity; + + private final Spinner spinner; + + public GenreSpinner(GenreSpinnerCallback callback, ItemListActivity activity, Spinner spinner) { + this.callback = callback; + this.activity = activity; + this.spinner = spinner; + registerCallback(); + orderItems(0); + } + + private void orderItems(int start) { + if (callback.getService() != null) { + try { + callback.getService().genres(start, null); + } catch (RemoteException e) { + Log.e(TAG, "Error ordering items: " + e); + } + } + } + + public void registerCallback() { + if (callback.getService() != null) { + try { + callback.getService().registerGenreListCallback(genreListCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error registering callback: " + e); + } + } + } + + public void unregisterCallback() { + if (callback.getService() != null) { + try { + callback.getService().unregisterGenreListCallback(genreListCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error unregistering callback: " + e); + } + } + } + + private final IServiceGenreListCallback genreListCallback + = new IServiceGenreListCallback.Stub() { + private ItemAdapter adapter; + + public void onGenresReceived(final int count, final int start, final List list) + throws RemoteException { + callback.getUIThreadHandler().post(new Runnable() { + public void run() { + if (adapter == null) { + GenreView itemView = new GenreView(activity) { + @Override + public View getAdapterView(View convertView, ViewGroup parent, + Genre item, + ImageFetcher unused) { + return Util.getSpinnerItemView(getActivity(), convertView, parent, + item.getName()); + } + + @Override + public View getAdapterView(View convertView, ViewGroup parent, + String label) { + return Util.getSpinnerItemView(getActivity(), convertView, parent, + label); + } + + }; + adapter = new ItemAdapter(itemView, true, null); + spinner.setAdapter(adapter); + } + adapter.update(count, start, list); + spinner.setSelection(adapter.findItem(callback.getGenre())); + + if (count > start + list.size()) { + if ((start + list.size()) % adapter.getPageSize() == 0) { + orderItems(start + list.size()); + } + } + } + }); + } + + }; + + public interface GenreSpinnerCallback { + + ISqueezeService getService(); + + Handler getUIThreadHandler(); + + Genre getGenre(); + + void setGenre(Genre genre); + } + +} diff --git a/src/uk/org/ngo/squeezer/itemlists/SqueezerYearView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreView.java similarity index 52% rename from src/uk/org/ngo/squeezer/itemlists/SqueezerYearView.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreView.java index 7da5b8b22..4855f294a 100644 --- a/src/uk/org/ngo/squeezer/itemlists/SqueezerYearView.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/GenreView.java @@ -14,39 +14,41 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; +package uk.org.ngo.squeezer.itemlist; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.framework.SqueezerBaseItemView; -import uk.org.ngo.squeezer.framework.SqueezerItemListActivity; -import uk.org.ngo.squeezer.model.SqueezerYear; import android.os.RemoteException; import android.view.ContextMenu; import android.view.Menu; import android.view.View; -public class SqueezerYearView extends SqueezerBaseItemView { +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.Genre; + +public class GenreView extends BaseItemView { - public SqueezerYearView(SqueezerItemListActivity activity) { - super(activity); - } + public GenreView(ItemListActivity activity) { + super(activity); + } - public String getQuantityString(int quantity) { - return getActivity().getResources().getQuantityString(R.plurals.year, quantity); - } + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.genre, quantity); + } - public void onItemSelected(int index, SqueezerYear item) throws RemoteException { - SqueezerAlbumListActivity.show(getActivity(), item); - } + public void onItemSelected(int index, Genre item) throws RemoteException { + AlbumListActivity.show(getActivity(), item); + } // XXX: Make this a menu resource. public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - menu.add(Menu.NONE, R.id.browse_songs, 0, R.string.CONTEXTMENU_BROWSE_SONGS); - menu.add(Menu.NONE, CONTEXTMENU_BROWSE_ALBUMS, 1, R.string.CONTEXTMENU_BROWSE_ALBUMS); - menu.add(Menu.NONE, R.id.play_now, Menu.NONE, R.string.CONTEXTMENU_PLAY_ITEM); - menu.add(Menu.NONE, R.id.add_to_playlist, Menu.NONE, R.string.CONTEXTMENU_ADD_ITEM); - }; + menu.add(Menu.NONE, R.id.browse_songs, 0, R.string.BROWSE_SONGS); + menu.add(Menu.NONE, BROWSE_ALBUMS, 1, R.string.BROWSE_ALBUMS); + menu.add(Menu.NONE, R.id.browse_artists, 2, R.string.BROWSE_ARTISTS); + menu.add(Menu.NONE, R.id.play_now, Menu.NONE, R.string.PLAY_NOW); + menu.add(Menu.NONE, R.id.add_to_playlist, Menu.NONE, R.string.ADD_TO_END); + } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/MusicFolderListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/MusicFolderListActivity.java new file mode 100644 index 000000000..a5f5e665a --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/MusicFolderListActivity.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2012 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 uk.org.ngo.squeezer.itemlist; + +import org.acra.ErrorReporter; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.List; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemAdapter; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.MusicFolderItem; + +/** + * Display a list of Squeezebox music folders. + *

+ * If the extras bundle contains a key that matches MusicFolder.class.getName() + * the value is assumed to be an instance of that class, and that folder will be displayed. + *

+ * Otherwise the root music folder is shown. + *

+ * The activity's content views scrolls in from the right, and disappear to the left, to provide a + * spatial component to navigation. + * + * @author nik + */ +public class MusicFolderListActivity extends BaseListActivity { + + /** + * The folder to view. The root folder if null. + */ + MusicFolderItem mFolder; + + @Override + public ItemView createItemView() { + return new MusicFolderView(this); + } + + /** + * Deliberately use {@link uk.org.ngo.squeezer.framework.ItemAdapter} instead of {@link + * ItemListAdapator} so that the title is not updated out from under us. + */ + @Override + protected ItemAdapter createItemListAdapter( + ItemView itemView) { + return new ItemAdapter(itemView); + } + + /** + * Extract the folder to view (if provided). + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + mFolder = extras.getParcelable(MusicFolderItem.class.getName()); + setTitle(mFolder.getName()); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mFolder != null) { + getMenuInflater().inflate(R.menu.playmenu, menu); + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + try { + switch (item.getItemId()) { + case R.id.play_now: + play(mFolder); + return true; + case R.id.add_to_playlist: + add(mFolder); + return true; + } + } catch (RemoteException e) { + Log.e(getTag(), "Error executing menu action '" + item.getMenuInfo() + "': " + e); + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerMusicFolderListCallback(musicFolderListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterMusicFolderListCallback(musicFolderListCallback); + } + + /** + * Fetch the contents of a folder. Fetches the contents of mFolder if non-null, the + * root folder otherwise. + * + * @param start Where in the list of folders to start fetching. + */ + @Override + protected void orderPage(int start) throws RemoteException { + if (mFolder == null) { + // No specific item, fetch from the beginning. + getService().musicFolders(start, null); + } else { + getService().musicFolders(start, mFolder.getId()); + } + } + + /** + * Show this activity, showing the contents of the root folder. + * + * @param activity + */ + public static void show(Activity activity) { + final Intent intent = new Intent(activity, MusicFolderListActivity.class); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); + } + + /** + * Show this activity, showing the contents of the given folder. + * + * @param activity + * @param folder The folder whose contents will be shown. + */ + public static void show(Activity activity, MusicFolderItem folder) { + final Intent intent = new Intent(activity, MusicFolderListActivity.class); + intent.putExtra(folder.getClass().getName(), folder); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right); + } + + private final IServiceMusicFolderListCallback musicFolderListCallback + = new IServiceMusicFolderListCallback.Stub() { + @Override + public void onMusicFoldersReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items); + } + }; + + /** + * Attempts to download the song given by songId. + *

+ * XXX: Duplicated from AbstractSongListActivity. + * + * @param songId ID of the song to download + */ + @Override + public void downloadSong(String songId) { + try { + String url = getService().getSongDownloadUrl(songId); + + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(i); + } catch (RemoteException e) { + ErrorReporter.getInstance().handleException(e); + e.printStackTrace(); + } + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/MusicFolderView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/MusicFolderView.java new file mode 100644 index 000000000..002acec60 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/MusicFolderView.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2012 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.MusicFolderItem; +import uk.org.ngo.squeezer.util.ImageFetcher; + +/** + * View for one entry in a {@link MusicFolderListActivity}. + *

+ * Shows an entry with an icon indicating the type of the music folder item, and the name of the + * item. + * + * @author nik + */ +public class MusicFolderView extends BaseItemView { + + @SuppressWarnings("unused") + private final static String TAG = "MusicFolderView"; + + public MusicFolderView(ItemListActivity activity) { + super(activity); + + setViewParams(EnumSet.of(ViewParams.ICON, ViewParams.CONTEXT_BUTTON)); + setLoadingViewParams(EnumSet.of(ViewParams.ICON)); + } + + public void bindView(View view, MusicFolderItem item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + + String type = item.getType(); + int icon_resource = R.drawable.ic_unknown; + + if (type.equals("folder")) { + icon_resource = R.drawable.ic_music_folder; + } + if (type.equals("track")) { + icon_resource = R.drawable.ic_songs; + } + if (type.equals("playlist")) { + icon_resource = R.drawable.ic_playlists; + } + + viewHolder.icon.setImageResource(icon_resource); + } + + @Override + public void onItemSelected(int index, MusicFolderItem item) throws RemoteException { + if (item.getType().equals("folder")) { + MusicFolderListActivity.show(getActivity(), item); + } + } + + // XXX: Make this a menu resource. + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + MusicFolderItem item = (MusicFolderItem) menuInfo.item; + if (item.getType().equals("folder")) { + menu.add(Menu.NONE, R.id.browse_songs, Menu.NONE, R.string.BROWSE_SONGS); + } + menu.add(Menu.NONE, R.id.play_now, Menu.NONE, R.string.PLAY_NOW); + menu.add(Menu.NONE, R.id.add_to_playlist, Menu.NONE, R.string.ADD_TO_END); + menu.add(Menu.NONE, R.id.play_next, Menu.NONE, R.string.PLAY_NEXT); + if (item.getType().equals("track")) { + menu.add(Menu.NONE, R.id.download, Menu.NONE, R.string.DOWNLOAD_ITEM); + } + } + + @Override + public boolean doItemContext(MenuItem menuItem, int index, MusicFolderItem selectedItem) + throws RemoteException { + switch (menuItem.getItemId()) { + case R.id.browse_songs: + MusicFolderListActivity.show(getActivity(), selectedItem); + return true; + case R.id.download: + getActivity().downloadSong(selectedItem.getId()); + return true; + } + return super.doItemContext(menuItem, index, selectedItem); + } + + @Override + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.musicfolder, quantity); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlayerListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlayerListActivity.java new file mode 100644 index 000000000..8d04a8b13 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlayerListActivity.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Player; + +public class PlayerListActivity extends BaseListActivity { + + private Player activePlayer; + + public Player getActivePlayer() { + return activePlayer; + } + + @Override + public ItemView createItemView() { + return new PlayerView(this); + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerPlayerListCallback(playerListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterPlayerListCallback(playerListCallback); + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().players(start); + } + + public static void show(Context context) { + final Intent intent = new Intent(context, PlayerListActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + context.startActivity(intent); + } + + private final IServicePlayerListCallback playerListCallback + = new IServicePlayerListCallback.Stub() { + public void onPlayersReceived(int count, int start, List items) + throws RemoteException { + activePlayer = getService().getActivePlayer(); + onItemsReceived(count, start, items); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlayerView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlayerView.java new file mode 100644 index 000000000..69f0dff82 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlayerView.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; +import android.view.View; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.model.Player; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public class PlayerView extends BaseItemView { + + private static final Map modelIcons = initializeModelIcons(); + + private final PlayerListActivity mActivity; + + public PlayerView(PlayerListActivity activity) { + super(activity); + + mActivity = activity; + + setViewParams(EnumSet.of(ViewParams.ICON)); + setLoadingViewParams(EnumSet.of(ViewParams.ICON)); + } + + public void bindView(View view, Player item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + viewHolder.text1.setTextAppearance(mActivity, + item.equals(mActivity.getActivePlayer()) ? R.style.SqueezerCurrentTextItem + : R.style.SqueezerTextItem); + + viewHolder.icon.setImageResource(getModelIcon(item.getModel())); + + view.setBackgroundResource( + item.equals(mActivity.getActivePlayer()) ? R.drawable.list_item_background_current + : R.drawable.list_item_background_normal); + } + + public void onItemSelected(int index, Player item) throws RemoteException { + getActivity().getService().setActivePlayer(item); + getActivity().finish(); + } + + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.player, quantity); + } + + private static Map initializeModelIcons() { + Map modelIcons = new HashMap(); + modelIcons.put("baby", R.drawable.icon_baby); + modelIcons.put("boom", R.drawable.icon_boom); + modelIcons.put("fab4", R.drawable.icon_fab4); + modelIcons.put("receiver", R.drawable.icon_receiver); + modelIcons.put("controller", R.drawable.icon_controller); + modelIcons.put("sb1n2", R.drawable.icon_sb1n2); + modelIcons.put("sb3", R.drawable.icon_sb3); + modelIcons.put("slimp3", R.drawable.icon_slimp3); + modelIcons.put("softsqueeze", R.drawable.icon_softsqueeze); + modelIcons.put("squeezeplay", R.drawable.icon_squeezeplay); + modelIcons.put("transporter", R.drawable.icon_transporter); + return modelIcons; + } + + private static int getModelIcon(String model) { + Integer icon = modelIcons.get(model); + return (icon != null ? icon : R.drawable.icon_blank); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistSongsActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistSongsActivity.java new file mode 100644 index 000000000..636d03775 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistSongsActivity.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistDeleteDialog; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistItemMoveDialog; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistRenameDialog; +import uk.org.ngo.squeezer.model.Playlist; +import uk.org.ngo.squeezer.model.Song; + +public class PlaylistSongsActivity extends AbstractSongListActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + playlist = extras.getParcelable("playlist"); + } + } + + public static void show(Activity context, Playlist playlist) { + final Intent intent = new Intent(context, PlaylistSongsActivity.class); + intent.putExtra("playlist", playlist); + context.startActivityForResult(intent, PlaylistsActivity.PLAYLIST_SONGS_REQUEST_CODE); + } + + private Playlist playlist; + + private String oldName; + + public Playlist getPlaylist() { + return playlist; + } + + public void playlistRename(String newName) { + try { + oldName = playlist.getName(); + getService().playlistsRename(playlist, newName); + playlist.setName(newName); + getIntent().putExtra("playlist", playlist); + setResult(PlaylistsActivity.PLAYLIST_RENAMED); + } catch (RemoteException e) { + Log.e(getTag(), "Error renaming playlist to '" + newName + "': " + e); + } + } + + public void playlistDelete() { + try { + getService().playlistsDelete(getPlaylist()); + setResult(PlaylistsActivity.PLAYLIST_DELETED); + finish(); + } catch (RemoteException e) { + Log.e(getTag(), "Error deleting playlist"); + } + + } + + @Override + public ItemView createItemView() { + return new SongView(this) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + menu.setGroupVisible(R.id.group_playlist, true); + + if (menuInfo.position == 0) { + menu.findItem(R.id.playlist_move_up).setVisible(false); + } + + if (menuInfo.position == menuInfo.adapter.getCount() - 1) { + menu.findItem(R.id.playlist_move_down).setVisible(false); + } + } + + @Override + public boolean doItemContext(MenuItem menuItem, int index, Song selectedItem) + throws RemoteException { + switch (menuItem.getItemId()) { + case R.id.play_now: + play(selectedItem); + return true; + + case R.id.add_to_playlist: + add(selectedItem); + return true; + + case R.id.play_next: + insert(selectedItem); + return true; + + case R.id.remove_from_playlist: + getService().playlistsRemove(playlist, index); + clearAndReOrderItems(); + return true; + + case R.id.playlist_move_up: + getService().playlistsMove(playlist, index, index - 1); + clearAndReOrderItems(); + return true; + + case R.id.playlist_move_down: + getService().playlistsMove(playlist, index, index + 1); + clearAndReOrderItems(); + return true; + + case R.id.playlist_move: + PlaylistItemMoveDialog.addTo(PlaylistSongsActivity.this, + playlist, index); + return true; + } + + return super.doItemContext(menuItem, index, selectedItem); + } + }; + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().playlistSongs(start, playlist); + } + + @Override + protected void registerCallback() throws RemoteException { + super.registerCallback(); + getService().registerPlaylistMaintenanceCallback(playlistMaintenanceCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + super.unregisterCallback(); + getService().unregisterPlaylistMaintenanceCallback(playlistMaintenanceCallback); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.playlistmenu, menu); + getMenuInflater().inflate(R.menu.playmenu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + try { + switch (item.getItemId()) { + case R.id.menu_item_playlists_delete: + new PlaylistDeleteDialog().show(getSupportFragmentManager(), + PlaylistDeleteDialog.class.getName()); + return true; + case R.id.menu_item_playlists_rename: + new PlaylistRenameDialog().show(getSupportFragmentManager(), + PlaylistRenameDialog.class.getName()); + return true; + case R.id.play_now: + play(playlist); + return true; + case R.id.add_to_playlist: + add(playlist); + return true; + } + } catch (RemoteException e) { + Log.e(getTag(), "Error executing menu action '" + item.getMenuInfo() + "': " + e); + } + return super.onOptionsItemSelected(item); + } + + private void showServiceMessage(final String msg) { + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + Toast.makeText(PlaylistSongsActivity.this, msg, Toast.LENGTH_SHORT).show(); + } + }); + } + + private void setResult(String flagName) { + Intent intent = new Intent(); + intent.putExtra(flagName, true); + intent.putExtra(PlaylistsActivity.CURRENT_PLAYLIST, playlist); + setResult(RESULT_OK, intent); + } + + private final IServicePlaylistMaintenanceCallback playlistMaintenanceCallback + = new IServicePlaylistMaintenanceCallback.Stub() { + + @Override + public void onRenameFailed(String msg) throws RemoteException { + playlist.setName(oldName); + getIntent().putExtra("playlist", playlist); + showServiceMessage(msg); + } + + @Override + public void onCreateFailed(String msg) throws RemoteException { + showServiceMessage(msg); + } + + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistView.java new file mode 100644 index 000000000..edf8dc506 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistView.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistsDeleteDialog; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistsRenameDialog; +import uk.org.ngo.squeezer.model.Playlist; + + +public class PlaylistView extends BaseItemView { + + private static final int PLAYLISTS_CONTEXTMENU_DELETE_ITEM = 0; + + private static final int PLAYLISTS_CONTEXTMENU_RENAME_ITEM = 1; + + private final PlaylistsActivity activity; + + public PlaylistView(PlaylistsActivity activity) { + super(activity); + this.activity = activity; + } + + @Override + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.playlist, quantity); + } + + @Override + public void onItemSelected(int index, Playlist item) throws RemoteException { + activity.setCurrentPlaylist(index, item); + PlaylistSongsActivity.show(getActivity(), item); + } + + // XXX: Make this a menu resource. + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + menu.add(Menu.NONE, PLAYLISTS_CONTEXTMENU_DELETE_ITEM, 0, R.string.menu_item_delete); + menu.add(Menu.NONE, PLAYLISTS_CONTEXTMENU_RENAME_ITEM, 1, R.string.menu_item_rename); + menu.add(Menu.NONE, R.id.browse_songs, 2, R.string.BROWSE_SONGS); + menu.add(Menu.NONE, R.id.play_now, 3, R.string.PLAY_NOW); + menu.add(Menu.NONE, R.id.play_next, 3, R.string.PLAY_NEXT); + menu.add(Menu.NONE, R.id.add_to_playlist, 4, R.string.ADD_TO_END); + } + + @Override + public boolean doItemContext(MenuItem menuItem, int index, Playlist selectedItem) + throws RemoteException { + activity.setCurrentPlaylist(index, selectedItem); + switch (menuItem.getItemId()) { + case PLAYLISTS_CONTEXTMENU_DELETE_ITEM: + new PlaylistsDeleteDialog().show(activity.getSupportFragmentManager(), + PlaylistsDeleteDialog.class.getName()); + return true; + case PLAYLISTS_CONTEXTMENU_RENAME_ITEM: + new PlaylistsRenameDialog().show(activity.getSupportFragmentManager(), + PlaylistsRenameDialog.class.getName()); + return true; + case R.id.browse_songs: + PlaylistSongsActivity.show(getActivity(), selectedItem); + return true; + } + return super.doItemContext(menuItem, index, selectedItem); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistsActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistsActivity.java new file mode 100644 index 000000000..90ffaf9f0 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PlaylistsActivity.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import java.util.List; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.itemlist.dialog.PlaylistsNewDialog; +import uk.org.ngo.squeezer.model.Playlist; + +public class PlaylistsActivity extends BaseListActivity { + + public final static int PLAYLIST_SONGS_REQUEST_CODE = 1; + + public static final String PLAYLIST_RENAMED = "playlist_renamed"; + + public static final String PLAYLIST_DELETED = "playlist_deleted"; + + public static final String CURRENT_PLAYLIST = "currentPlaylist"; + + private static final String CURRENT_INDEX = "currentIndex"; + + private int currentIndex = -1; + + private Playlist currentPlaylist; + + private String oldName; + + public Playlist getCurrentPlaylist() { + return currentPlaylist; + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + currentIndex = savedInstanceState.getInt(CURRENT_INDEX); + currentPlaylist = savedInstanceState.getParcelable(CURRENT_PLAYLIST); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putInt(CURRENT_INDEX, currentIndex); + outState.putParcelable(CURRENT_PLAYLIST, currentPlaylist); + super.onSaveInstanceState(outState); + } + + /** + * Set the playlist to be used as context + */ + public void setCurrentPlaylist(int index, Playlist playlist) { + this.currentIndex = index; + this.currentPlaylist = playlist; + } + + /** + * Rename the playlist previously set as context. + */ + public void playlistRename(String newName) { + try { + getService().playlistsRename(currentPlaylist, newName); + oldName = currentPlaylist.getName(); + currentPlaylist.setName(newName); + getItemAdapter().notifyDataSetChanged(); + } catch (RemoteException e) { + Log.e(getTag(), "Error renaming playlist to '" + newName + "': " + e); + } + } + + @Override + public ItemView createItemView() { + return new PlaylistView(this); + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerPlaylistsCallback(playlistsCallback); + getService().registerPlaylistMaintenanceCallback(playlistMaintenanceCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterPlaylistsCallback(playlistsCallback); + getService().unregisterPlaylistMaintenanceCallback(playlistMaintenanceCallback); + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().playlists(start); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d(getTag(), "onActivityResult(" + requestCode + "," + resultCode + ",'" + data + "')"); + if (requestCode == PLAYLIST_SONGS_REQUEST_CODE && resultCode == RESULT_OK) { + if (data.getBooleanExtra(PLAYLIST_RENAMED, false)) { + currentPlaylist = data.getParcelableExtra(CURRENT_PLAYLIST); + getItemAdapter().setItem(currentIndex, currentPlaylist); + getItemAdapter().notifyDataSetChanged(); + } + if (data.getBooleanExtra(PLAYLIST_DELETED, false)) { + clearAndReOrderItems(); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.playlistsmenu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_item_playlists_new: + new PlaylistsNewDialog().show(getSupportFragmentManager(), + PlaylistsNewDialog.class.getName()); + return true; + } + return super.onOptionsItemSelected(item); + } + + public static void show(Context context) { + final Intent intent = new Intent(context, PlaylistsActivity.class); + context.startActivity(intent); + } + + private final IServicePlaylistsCallback playlistsCallback + = new IServicePlaylistsCallback.Stub() { + @Override + public void onPlaylistsReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items); + } + }; + + private void showServiceMessage(final String msg) { + getUIThreadHandler().post(new Runnable() { + @Override + public void run() { + getItemAdapter().notifyDataSetChanged(); + Toast.makeText(PlaylistsActivity.this, msg, Toast.LENGTH_SHORT).show(); + } + }); + } + + private final IServicePlaylistMaintenanceCallback playlistMaintenanceCallback + = new IServicePlaylistMaintenanceCallback.Stub() { + + @Override + public void onRenameFailed(String msg) throws RemoteException { + if (currentIndex != -1) { + currentPlaylist.setName(oldName); + } + showServiceMessage(msg); + } + + @Override + public void onCreateFailed(String msg) throws RemoteException { + showServiceMessage(msg); + } + + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginItemListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginItemListActivity.java new file mode 100644 index 000000000..00aeffbe1 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginItemListActivity.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.widget.EditText; +import android.widget.ImageButton; + +import java.util.List; +import java.util.Map; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Plugin; +import uk.org.ngo.squeezer.model.PluginItem; + +/* + * The activity's content view scrolls in from the right, and disappear to the left, to provide a + * spatial component to navigation. + */ +public class PluginItemListActivity extends BaseListActivity { + + private Plugin plugin; + + private PluginItem parent; + + private String search; + + @Override + public ItemView createItemView() { + return new PluginItemView(this); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + plugin = extras.getParcelable(Plugin.class.getName()); + parent = extras.getParcelable(PluginItem.class.getName()); + findViewById(R.id.search_view).setVisibility( + plugin.isSearchable() ? View.VISIBLE : View.GONE); + + ImageButton searchButton = (ImageButton) findViewById(R.id.search_button); + final EditText searchCriteriaText = (EditText) findViewById(R.id.search_input); + + searchCriteriaText.setOnKeyListener(new OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + if ((event.getAction() == KeyEvent.ACTION_DOWN) + && (keyCode == KeyEvent.KEYCODE_ENTER)) { + clearAndReOrderItems(searchCriteriaText.getText().toString()); + return true; + } + return false; + } + }); + + searchButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (getService() != null) { + clearAndReOrderItems(searchCriteriaText.getText().toString()); + } + } + }); + } + } + + public Plugin getPlugin() { + return plugin; + } + + private void clearAndReOrderItems(String searchString) { + if (getService() != null && !(plugin.isSearchable() && (searchString == null + || searchString.length() == 0))) { + search = searchString; + super.clearAndReOrderItems(); + } + } + + @Override + public void clearAndReOrderItems() { + clearAndReOrderItems(search); + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerPluginItemListCallback(pluginItemListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterPluginItemListCallback(pluginItemListCallback); + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().pluginItems(start, plugin, parent, search); + } + + + public void show(PluginItem pluginItem) { + final Intent intent = new Intent(this, PluginItemListActivity.class); + intent.putExtra(plugin.getClass().getName(), plugin); + intent.putExtra(pluginItem.getClass().getName(), pluginItem); + startActivity(intent); + overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); + } + + public static void show(Activity activity, Plugin plugin) { + final Intent intent = new Intent(activity, PluginItemListActivity.class); + intent.putExtra(plugin.getClass().getName(), plugin); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right); + } + + private final IServicePluginItemListCallback pluginItemListCallback + = new IServicePluginItemListCallback.Stub() { + public void onPluginItemsReceived(int count, int start, + @SuppressWarnings("rawtypes") final Map parameters, List items) + throws RemoteException { + if (parameters.containsKey("title")) { + getUIThreadHandler().post(new Runnable() { + public void run() { + setTitle((String) parameters.get("title")); + } + }); + } + + // Automatically fetch subitems, if this is the only item. + // TODO: Seen an NPE here (before adding the != null) check. Find out + // why count == 1 might be true, but items.get(0) might return null. + if (count == 1 && items.get(0) != null && items.get(0).isHasitems()) { + parent = items.get(0); + getUIThreadHandler().post(new Runnable() { + public void run() { + clearAndReOrderItems(); + } + }); + return; + } + onItemsReceived(count, start, items); + } + }; + + // Shortcuts for operations for plugin items + + public boolean play(PluginItem item) throws RemoteException { + return pluginPlaylistControl(PluginPlaylistControlCmd.play, item); + } + + public boolean load(PluginItem item) throws RemoteException { + return pluginPlaylistControl(PluginPlaylistControlCmd.load, item); + } + + public boolean insert(PluginItem item) throws RemoteException { + return pluginPlaylistControl(PluginPlaylistControlCmd.insert, item); + } + + public boolean add(PluginItem item) throws RemoteException { + return pluginPlaylistControl(PluginPlaylistControlCmd.add, item); + } + + private boolean pluginPlaylistControl(PluginPlaylistControlCmd cmd, PluginItem item) + throws RemoteException { + if (getService() == null) { + return false; + } + getService().pluginPlaylistControl(plugin, cmd.name(), item.getId()); + return true; + } + + private enum PluginPlaylistControlCmd { + play, + load, + add, + insert + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginItemView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginItemView.java new file mode 100644 index 000000000..ca200da4b --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginItemView.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.model.PluginItem; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public class PluginItemView extends BaseItemView { + + private final PluginItemListActivity mActivity; + + public PluginItemView(PluginItemListActivity activity) { + super(activity); + mActivity = activity; + + setViewParams(EnumSet.of(ViewParams.ICON, ViewParams.CONTEXT_BUTTON)); + setLoadingViewParams(EnumSet.of(ViewParams.ICON)); + } + + public void bindView(View view, PluginItem item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + + // Disable the context menu if this item has sub-items. + if (item.isHasitems()) { + viewHolder.btnContextMenu.setVisibility(View.GONE); + } + + // If the item has an image, then fetch and display it + if (item.getImage() != null) { + imageFetcher.loadImage(item.getImage(), viewHolder.icon); + } else { + // Otherwise we will revert to some other icon. This is not an exact approach, more + // like a best effort. + if (item.isHasitems()) { + // If this item has sub-items we use the icon of the parent and if that fails, + // the current plugin. + if (mActivity.getPlugin().getIconResource() != 0) { + viewHolder.icon.setImageResource(mActivity.getPlugin().getIconResource()); + } else { + imageFetcher.loadImage(mActivity.getIconUrl(mActivity.getPlugin().getIcon()), + viewHolder.icon); + } + } else { + // Finally we assume it is an item that can be played. This is consistent with + // onItemSelected and onCreateContextMenu. + viewHolder.icon.setImageResource(R.drawable.ic_songs); + } + } + } + + @Override + public String getQuantityString(int quantity) { + return null; + } + + @Override + public void onItemSelected(int index, PluginItem item) throws RemoteException { + if (item.isHasitems()) { + mActivity.show(item); + } + } + + // XXX: Make this a menu resource. + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (!((PluginItem) menuInfo.item).isHasitems()) { + super.onCreateContextMenu(menu, v, menuInfo); + + menu.add(Menu.NONE, R.id.play_now, Menu.NONE, R.string.PLAY_NOW); + menu.add(Menu.NONE, R.id.add_to_playlist, Menu.NONE, R.string.ADD_TO_END); + menu.add(Menu.NONE, R.id.play_next, Menu.NONE, R.string.PLAY_NEXT); + } + } + + @Override + public boolean doItemContext(MenuItem menuItem, int index, PluginItem selectedItem) + throws RemoteException { + switch (menuItem.getItemId()) { + case R.id.play_now: + if (mActivity.play(selectedItem)) { + Toast.makeText(mActivity, + mActivity.getString(R.string.ITEM_PLAYING, selectedItem.getName()), + Toast.LENGTH_SHORT).show(); + } + return true; + + case R.id.add_to_playlist: + if (mActivity.add(selectedItem)) { + Toast.makeText(mActivity, + mActivity.getString(R.string.ITEM_ADDED, selectedItem.getName()), + Toast.LENGTH_SHORT).show(); + } + return true; + + case R.id.play_next: + if (mActivity.insert(selectedItem)) { + Toast.makeText(mActivity, + mActivity.getString(R.string.ITEM_INSERTED, selectedItem.getName()), + Toast.LENGTH_SHORT).show(); + } + return true; + } + return false; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginListActivity.java new file mode 100644 index 000000000..bad421e86 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginListActivity.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.RemoteException; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.model.Plugin; + +public abstract class PluginListActivity extends BaseListActivity { + + private final IServicePluginListCallback pluginListCallback + = new IServicePluginListCallback.Stub() { + public void onPluginsReceived(int count, int start, List items) + throws RemoteException { + onItemsReceived(count, start, items); + } + }; + + @Override + protected void registerCallback() throws RemoteException { + getService().registerPluginListCallback(pluginListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterPluginListCallback(pluginListCallback); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginView.java new file mode 100644 index 000000000..90c7532db --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/PluginView.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.view.View; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.model.Plugin; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public abstract class PluginView extends BaseItemView { + + public PluginView(BaseListActivity activity) { + super(activity); + + setViewParams(EnumSet.of(ViewParams.ICON)); + } + + @Override + public void bindView(View view, Plugin item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + imageFetcher.loadImage(getActivity().getIconUrl(item.getIcon()), viewHolder.icon); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/RadioListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/RadioListActivity.java new file mode 100644 index 000000000..5394b09dd --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/RadioListActivity.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + + +import android.app.Activity; +import android.content.Intent; +import android.os.RemoteException; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Plugin; + +/* + * Display a list of radio stations. + *

+ * The activity's content view scrolls in from the right, and disappear to the left, to provide a + * spatial component to navigation. + */ +public class RadioListActivity extends PluginListActivity { + + @Override + public ItemView createItemView() { + return new RadioView(this); + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().radios(start); + } + + public static void show(Activity activity) { + final Intent intent = new Intent(activity, RadioListActivity.class); + activity.startActivity(intent); + activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right); + } +} diff --git a/src/uk/org/ngo/squeezer/itemlists/SqueezerRadioView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/RadioView.java similarity index 54% rename from src/uk/org/ngo/squeezer/itemlists/SqueezerRadioView.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/RadioView.java index 6c2c1a026..7ed6928ec 100644 --- a/src/uk/org/ngo/squeezer/itemlists/SqueezerRadioView.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/RadioView.java @@ -14,32 +14,33 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; +package uk.org.ngo.squeezer.itemlist; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.framework.SqueezerBaseListActivity; -import uk.org.ngo.squeezer.framework.SqueezerItemView; -import uk.org.ngo.squeezer.model.SqueezerPlugin; import android.os.RemoteException; import android.view.ContextMenu; import android.view.View; -public class SqueezerRadioView extends SqueezerPluginView { +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Plugin; + +public class RadioView extends PluginView { - public SqueezerRadioView(SqueezerBaseListActivity activity) { - super(activity); - } + public RadioView(BaseListActivity activity) { + super(activity); + } - public String getQuantityString(int quantity) { - return getActivity().getResources().getQuantityString(R.plurals.radio, quantity); - } + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.radio, quantity); + } - public void onItemSelected(int index, SqueezerPlugin item) throws RemoteException { - SqueezerPluginItemListActivity.show(getActivity(), item); - } + public void onItemSelected(int index, Plugin item) throws RemoteException { + PluginItemListActivity.show(getActivity(), item); + } @Override public void onCreateContextMenu(ContextMenu menu, View v, - SqueezerItemView.ContextMenuInfo menuInfo) { + ItemView.ContextMenuInfo menuInfo) { } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongListActivity.java new file mode 100644 index 000000000..1e8beff26 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongListActivity.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.framework.PlaylistItem; +import uk.org.ngo.squeezer.itemlist.GenreSpinner.GenreSpinnerCallback; +import uk.org.ngo.squeezer.itemlist.YearSpinner.YearSpinnerCallback; +import uk.org.ngo.squeezer.itemlist.dialog.SongFilterDialog; +import uk.org.ngo.squeezer.itemlist.dialog.SongOrderDialog; +import uk.org.ngo.squeezer.itemlist.dialog.SongOrderDialog.SongsSortOrder; +import uk.org.ngo.squeezer.menu.BaseMenuFragment; +import uk.org.ngo.squeezer.menu.FilterMenuFragment; +import uk.org.ngo.squeezer.menu.OrderMenuItemFragment; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.model.Year; + +public class SongListActivity extends AbstractSongListActivity + implements GenreSpinnerCallback, YearSpinnerCallback, + FilterMenuFragment.FilterableListActivity, + OrderMenuItemFragment.OrderableListActivity { + + private static final String TAG = SongListActivity.class.getSimpleName(); + + private SongsSortOrder sortOrder = SongsSortOrder.title; + + private String searchString; + + public String getSearchString() { + return searchString; + } + + public void setSearchString(String searchString) { + this.searchString = searchString; + } + + private Album album; + + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + private Artist artist; + + public Artist getArtist() { + return artist; + } + + public void setArtist(Artist artist) { + this.artist = artist; + } + + private Year year; + + @Override + public Year getYear() { + return year; + } + + @Override + public void setYear(Year year) { + this.year = year; + } + + private Genre genre; + + @Override + public Genre getGenre() { + return genre; + } + + @Override + public void setGenre(Genre genre) { + this.genre = genre; + } + + private GenreSpinner genreSpinner; + + public void setGenreSpinner(Spinner spinner) { + genreSpinner = new GenreSpinner(this, this, spinner); + } + + private YearSpinner yearSpinner; + + public void setYearSpinner(Spinner spinner) { + yearSpinner = new YearSpinner(this, this, spinner); + } + + private SongView songViewLogic; + + private MenuItem playButton; + + private MenuItem addButton; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Set the album header. + if (album != null) { + TextView albumView = (TextView) findViewById(R.id.albumname); + TextView artistView = (TextView) findViewById(R.id.artistname); + TextView yearView = (TextView) findViewById(R.id.yearname); + ImageView btnContextMenu = (ImageView) findViewById(R.id.context_menu); + + albumView.setText(album.getName()); + artistView.setText(album.getArtist()); + if (album.getYear() != 0) { + yearView.setText(Integer.toString(album.getYear())); + } + + btnContextMenu.setOnCreateContextMenuListener(this); + + btnContextMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + v.showContextMenu(); + } + }); + } + + if (artist != null) { + TextView header = (TextView) findViewById(R.id.header); + header.setVisibility(View.VISIBLE); + header.setText(getString(R.string.songs_by_header, artist.getName())); + } + + // Adapter has been created (or restored from the fragment) by this point, + // so fetch the itemView that was used. + songViewLogic = (SongView) getItemAdapter().getItemView(); + + BaseMenuFragment.add(this, FilterMenuFragment.class); + BaseMenuFragment.add(this, OrderMenuItemFragment.class); + + songViewLogic.setBrowseByAlbum(album != null); + songViewLogic.setBrowseByArtist(artist != null); + } + + @Override + protected int getContentView() { + Bundle extras = getIntent().getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + if (Album.class.getName().equals(key)) { + album = extras.getParcelable(key); + sortOrder = SongsSortOrder.tracknum; + } else if (Artist.class.getName().equals(key)) { + artist = extras.getParcelable(key); + } else if (Year.class.getName().equals(key)) { + year = extras.getParcelable(key); + } else if (Genre.class.getName().equals(key)) { + genre = extras.getParcelable(key); + } else { + Log.e(getTag(), "Unexpected extra value: " + key + "(" + + extras.get(key).getClass().getName() + ")"); + } + } + } + + if (album != null) { + return R.layout.item_list_album; + } + + return super.getContentView(); + } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + + // Set artwork that requires a service connection. + if (album != null) { + ImageView artwork = (ImageView) findViewById(R.id.album); + + String artworkUrl = ((SongView) getItemView()) + .getAlbumArtUrl(album.getArtwork_track_id()); + + if (artworkUrl == null) { + artwork.setImageResource(R.drawable.icon_album_noart); + } else { + getImageFetcher().loadImage(artworkUrl, artwork); + } + } + } + + public static void show(Context context, Item... items) { + final Intent intent = new Intent(context, SongListActivity.class); + for (Item item : items) { + intent.putExtra(item.getClass().getName(), item); + } + context.startActivity(intent); + } + + + @Override + public ItemView createItemView() { + if (album != null) { + songViewLogic = new SongView(this); + songViewLogic.setDetails(EnumSet.of( + SongView.Details.TRACK_NO, + SongView.Details.DURATION, + SongView.Details.ARTIST_IF_COMPILATION)); + } else if (artist != null) { + songViewLogic = new SongViewWithArt(this); + songViewLogic.setDetails(EnumSet.of( + SongView.Details.DURATION, + SongView.Details.ALBUM, + SongView.Details.YEAR + )); + } else { + songViewLogic = new SongViewWithArt(this); + songViewLogic.setDetails(EnumSet.of( + SongView.Details.ARTIST, + SongView.Details.ALBUM, + SongView.Details.YEAR)); + } + + return songViewLogic; + } + + @Override + protected void registerCallback() throws RemoteException { + super.registerCallback(); + if (genreSpinner != null) { + genreSpinner.registerCallback(); + } + if (yearSpinner != null) { + yearSpinner.registerCallback(); + } + } + + @Override + protected void unregisterCallback() throws RemoteException { + super.unregisterCallback(); + if (genreSpinner != null) { + genreSpinner.unregisterCallback(); + } + if (yearSpinner != null) { + yearSpinner.unregisterCallback(); + } + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().songs(start, sortOrder.name(), searchString, album, artist, year, genre); + + boolean canPlay = (getCurrentPlaylistItem() != null); + if (playButton != null) { + playButton.setVisible(canPlay); + } + + if (addButton != null) { + addButton.setVisible(canPlay); + } + } + + public SongsSortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(SongsSortOrder sortOrder) { + this.sortOrder = sortOrder; + clearAndReOrderItems(); + } + + @Override + public void showFilterDialog() { + new SongFilterDialog().show(getSupportFragmentManager(), "SongFilterDialog"); + } + + @Override + public void showOrderDialog() { + new SongOrderDialog().show(this.getSupportFragmentManager(), "OrderDialog"); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + if (album != null) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.albumcontextmenu, menu); + + // Hide the option to view the album. + MenuItem browse = menu.findItem(R.id.browse_songs); + if (browse != null) { + browse.setVisible(false); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item + .getMenuInfo(); + + // If info is null then this the context menu from the header, not a list item. + if (info == null) { + try { + switch (item.getItemId()) { + case R.id.play_now: + play(album); + return true; + + case R.id.add_to_playlist: + add(album); + return true; + + case R.id.browse_artists: + ArtistListActivity.show(this, album); + return true; + + default: + throw new IllegalStateException("Unknown menu ID."); + } + } catch (RemoteException e) { + Log.e(getTag(), "Error executing menu action '" + item.getTitle() + "': " + e); + } + + } + return super.onContextItemSelected(item); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Only show the play entries from the options menu for albums (the context menu already + // shows them). + if (album == null) { + getMenuInflater().inflate(R.menu.playmenu, menu); + playButton = menu.findItem(R.id.play_now); + addButton = menu.findItem(R.id.add_to_playlist); + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + try { + PlaylistItem playlistItem = getCurrentPlaylistItem(); + switch (item.getItemId()) { + case R.id.play_now: + if (playlistItem != null) { + play(playlistItem); + } + return true; + case R.id.add_to_playlist: + if (playlistItem != null) { + add(playlistItem); + } + return true; + } + } catch (RemoteException e) { + Log.e(getTag(), "Error executing menu action '" + item.getMenuInfo() + "': " + e); + } + return super.onOptionsItemSelected(item); + } + + private PlaylistItem getCurrentPlaylistItem() { + int playlistItems = Util + .countBooleans(album != null, artist != null, genre != null, year != null); + if (playlistItems == 1 && TextUtils.isEmpty(searchString)) { + if (album != null) { + return album; + } + if (artist != null) { + return artist; + } + if (genre != null) { + return genre; + } + if (year != null) { + return year; + } + } + return null; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongView.java new file mode 100644 index 000000000..0ab3f5744 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongView.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + + +import android.os.RemoteException; +import android.util.Log; +import android.view.ContextMenu; +import android.view.View; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItemView; +import uk.org.ngo.squeezer.itemlist.action.PlayableItemAction; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.util.ImageFetcher; + +import static android.text.format.DateUtils.formatElapsedTime; + +/** + * A view that shows a single song with its artwork, and a context menu. + */ +public class SongView extends PlaylistItemView { + + @SuppressWarnings("unused") + private static final String TAG = "SongView"; + + /** + * Which details to show in the second line of text. + */ + public enum Details { + /** + * Show the artist name. Mutually exclusive with ARTIST_IF_COMPILATION. + */ + ARTIST, + + /** + * Show the artist name only if the song is part of a compilation. Mutually exclusive with + * ARTIST. + */ + ARTIST_IF_COMPILATION, + + /** + * Show the album name. + */ + ALBUM, + + /** + * Show the year (if known). + */ + YEAR, + + /** + * Show the genre (if known). + */ + GENRE, + + /** + * Track number. + */ + TRACK_NO, + + /** + * Duration. + */ + DURATION + } + + private EnumSet

mDetails = EnumSet.noneOf(Details.class); + + private boolean browseByAlbum; + + public void setBrowseByAlbum(boolean browseByAlbum) { + this.browseByAlbum = browseByAlbum; + } + + private boolean browseByArtist; + + public void setBrowseByArtist(boolean browseByArtist) { + this.browseByArtist = browseByArtist; + } + + public SongView(ItemListActivity activity) { + super(activity); + + setViewParams(EnumSet.of(ViewParams.TWO_LINE, ViewParams.CONTEXT_BUTTON)); + } + + public void setDetails(EnumSet
details) { + if (details.contains(Details.ARTIST) && details.contains(Details.ARTIST_IF_COMPILATION)) { + throw new IllegalArgumentException( + "ARTIST and ARTIST_IF_COMPILATION are mutually exclusive"); + } + mDetails = details; + } + + @Override + public void bindView(View view, Song item, ImageFetcher imageFetcher) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(item.getName()); + + viewHolder.text2.setText(mJoiner.join( + mDetails.contains(Details.TRACK_NO) ? item.getTracknum() : null, + mDetails.contains(Details.DURATION) ? formatElapsedTime(item.getDuration()) : null, + mDetails.contains(Details.ARTIST) ? item.getArtist() : null, + mDetails.contains(Details.ARTIST_IF_COMPILATION) && item.getCompilation() ? item + .getArtist() : null, + mDetails.contains(Details.ALBUM) ? item.getAlbumName() : null, + mDetails.contains(Details.YEAR) ? item.getYear() : null + )); + } + + /** + * Binds the label to {@link ViewHolder#text1}. Hides the {@link ViewHolder#btnContextMenu} and + * clears {@link ViewHolder#text2}. + * + * @param view The view that contains the {@link ViewHolder} + * @param label The text to bind to {@link ViewHolder#text1} + */ + @Override + public void bindView(View view, String label) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + + viewHolder.text1.setText(label); + viewHolder.text2.setText(""); + } + + /** + * Returns the URL to download the specified album artwork, or null if the artwork does not + * exist, or there was a problem with the service. + * + * @param artwork_track_id + * + * @return + */ + protected String getAlbumArtUrl(String artwork_track_id) { + if (artwork_track_id == null) { + return null; + } + + ISqueezeService service = getActivity().getService(); + if (service == null) { + return null; + } + + try { + return service.getAlbumArtUrl(artwork_track_id); + } catch (RemoteException e) { + Log.e(getClass().getSimpleName(), "Error requesting album art url: " + e); + return null; + } + } + + @Override + protected PlayableItemAction getOnSelectAction() { + String actionType = preferences.getString(Preferences.KEY_ON_SELECT_SONG_ACTION, + PlayableItemAction.Type.NONE.name()); + return PlayableItemAction.createAction(getActivity(), actionType); + } + + /** + * Creates the context menu for a song by inflating R.menu.songcontextmenu. + *

+ * Subclasses that show songs in playlists should call through to this first, then adjust the + * visibility of R.id.group_playlist. + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + menuInfo.menuInflater.inflate(R.menu.songcontextmenu, menu); + + if (((Song) menuInfo.item).getAlbum_id() != null && !browseByAlbum) { + menu.findItem(R.id.view_this_album).setVisible(true); + } + + if (((Song) menuInfo.item).getArtist_id() != null) { + menu.findItem(R.id.view_albums_by_song).setVisible(true); + } + + if (((Song) menuInfo.item).getArtist_id() != null && !browseByArtist) { + menu.findItem(R.id.view_songs_by_artist).setVisible(true); + } + } + + @Override + public boolean doItemContext(android.view.MenuItem menuItem, int index, Song selectedItem) + throws RemoteException { + switch (menuItem.getItemId()) { + case R.id.view_this_album: + SongListActivity.show(getActivity(), selectedItem.getAlbum()); + return true; + + // XXX: Is this actually "view albums by artist"? + case R.id.view_albums_by_song: + AlbumListActivity.show(getActivity(), + new Artist(selectedItem.getArtist_id(), selectedItem.getArtist())); + return true; + + case R.id.view_songs_by_artist: + SongListActivity.show(getActivity(), + new Artist(selectedItem.getArtist_id(), selectedItem.getArtist())); + return true; + + case R.id.download: + getActivity().downloadSong(selectedItem); + return true; + } + + return super.doItemContext(menuItem, index, selectedItem); + } + + @Override + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.song, quantity); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongViewWithArt.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongViewWithArt.java new file mode 100644 index 000000000..c64e4200f --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/SongViewWithArt.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2011 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.itemlist; + + +import android.view.View; + +import java.util.EnumSet; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.util.ImageFetcher; + +/** + * A view that shows a single song with its artwork, and a context menu. + */ +public class SongViewWithArt extends SongView { + + @SuppressWarnings("unused") + private static final String TAG = "SongView"; + + public SongViewWithArt(ItemListActivity activity) { + super(activity); + + setViewParams(EnumSet.of(ViewParams.ICON, ViewParams.TWO_LINE, ViewParams.CONTEXT_BUTTON)); + setLoadingViewParams(EnumSet.of(ViewParams.ICON, ViewParams.TWO_LINE)); + } + + @Override + public void bindView(View view, Song item, ImageFetcher imageFetcher) { + super.bindView(view, item, imageFetcher); + + ViewHolder viewHolder = (ViewHolder) view.getTag(); + String artworkUrl = getAlbumArtUrl(item.getArtwork_track_id()); + if (artworkUrl == null) { + viewHolder.icon.setImageResource( + item.isRemote() ? R.drawable.icon_iradio_noart : R.drawable.icon_album_noart); + } else { + imageFetcher.loadImage(artworkUrl, viewHolder.icon); + } + } + + /** + * Binds the label to {@link ViewHolder#text1}. Sets {@link ViewHolder#icon} to the generic + * pending icon, and clears {@link ViewHolder#text2}. + * + * @param view The view that contains the {@link ViewHolder} + * @param label The text to bind to {@link ViewHolder#text1} + */ + @Override + public void bindView(View view, String label) { + super.bindView(view, label); + + ViewHolder viewHolder = (ViewHolder) view.getTag(); + viewHolder.icon.setImageResource(R.drawable.icon_pending_artwork); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearListActivity.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearListActivity.java new file mode 100644 index 000000000..25238a795 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearListActivity.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; + +import java.util.List; + +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.framework.ItemView; +import uk.org.ngo.squeezer.model.Year; + +public class YearListActivity extends BaseListActivity { + + @Override + public ItemView createItemView() { + return new YearView(this); + } + + @Override + protected void registerCallback() throws RemoteException { + getService().registerYearListCallback(yearListCallback); + } + + @Override + protected void unregisterCallback() throws RemoteException { + getService().unregisterYearListCallback(yearListCallback); + } + + @Override + protected void orderPage(int start) throws RemoteException { + getService().years(start); + } + + + public static void show(Context context) { + final Intent intent = new Intent(context, YearListActivity.class); + context.startActivity(intent); + } + + private final IServiceYearListCallback yearListCallback = new IServiceYearListCallback.Stub() { + public void onYearsReceived(int count, int start, List items) throws RemoteException { + onItemsReceived(count, start, items); + } + }; + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearSpinner.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearSpinner.java new file mode 100644 index 000000000..7e04d8ff5 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearSpinner.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist; + +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Spinner; + +import java.util.List; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.ItemAdapter; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.Year; +import uk.org.ngo.squeezer.service.ISqueezeService; +import uk.org.ngo.squeezer.util.ImageFetcher; + +public class YearSpinner { + + private static final String TAG = YearSpinner.class.getName(); + + YearSpinnerCallback callback; + + private final ItemListActivity activity; + + private final Spinner spinner; + + public YearSpinner(YearSpinnerCallback callback, ItemListActivity activity, Spinner spinner) { + this.callback = callback; + this.activity = activity; + this.spinner = spinner; + registerCallback(); + orderItems(0); + } + + private void orderItems(int start) { + if (callback.getService() != null) { + try { + callback.getService().years(start); + } catch (RemoteException e) { + Log.e(TAG, "Error ordering items: " + e); + } + } + } + + public void registerCallback() { + if (callback.getService() != null) { + try { + callback.getService().registerYearListCallback(yearListCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error registering callback: " + e); + } + } + } + + public void unregisterCallback() { + if (callback.getService() != null) { + try { + callback.getService().unregisterYearListCallback(yearListCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error unregistering callback: " + e); + } + } + } + + private final IServiceYearListCallback yearListCallback = new IServiceYearListCallback.Stub() { + private ItemAdapter adapter; + + public void onYearsReceived(final int count, final int start, final List list) + throws RemoteException { + callback.getUIThreadHandler().post(new Runnable() { + public void run() { + if (adapter == null) { + YearView itemView = new YearView(activity) { + @Override + public View getAdapterView(View convertView, ViewGroup parent, + Year item, + ImageFetcher unused) { + return Util.getSpinnerItemView(getActivity(), convertView, parent, + item.getName()); + } + + @Override + public View getAdapterView(View convertView, ViewGroup parent, + String label) { + return Util.getSpinnerItemView(getActivity(), convertView, parent, + label); + } + }; + adapter = new ItemAdapter(itemView, true, null); + spinner.setAdapter(adapter); + } + adapter.update(count, start, list); + spinner.setSelection(adapter.findItem(callback.getYear())); + + if (count > start + list.size()) { + if ((start + list.size()) % adapter.getPageSize() == 0) { + orderItems(start + list.size()); + } + } + } + }); + } + + }; + + public interface YearSpinnerCallback { + + ISqueezeService getService(); + + Handler getUIThreadHandler(); + + Year getYear(); + + void setYear(Year year); + } + +} diff --git a/src/uk/org/ngo/squeezer/itemlists/SqueezerArtistView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearView.java similarity index 52% rename from src/uk/org/ngo/squeezer/itemlists/SqueezerArtistView.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearView.java index 5d7ad9f5f..21e1911e2 100644 --- a/src/uk/org/ngo/squeezer/itemlists/SqueezerArtistView.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/YearView.java @@ -14,40 +14,40 @@ * limitations under the License. */ -package uk.org.ngo.squeezer.itemlists; +package uk.org.ngo.squeezer.itemlist; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.framework.SqueezerBaseItemView; -import uk.org.ngo.squeezer.framework.SqueezerItemListActivity; -import uk.org.ngo.squeezer.model.SqueezerArtist; import android.os.RemoteException; import android.view.ContextMenu; import android.view.Menu; import android.view.View; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseItemView; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.model.Year; + +public class YearView extends BaseItemView { -public class SqueezerArtistView extends SqueezerBaseItemView { + public YearView(ItemListActivity activity) { + super(activity); + } - public SqueezerArtistView(SqueezerItemListActivity activity) { - super(activity); - } + public String getQuantityString(int quantity) { + return getActivity().getResources().getQuantityString(R.plurals.year, quantity); + } - public void onItemSelected(int index, SqueezerArtist item) throws RemoteException { - SqueezerAlbumListActivity.show(getActivity(), item); - } + public void onItemSelected(int index, Year item) throws RemoteException { + AlbumListActivity.show(getActivity(), item); + } // XXX: Make this a menu resource. + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - menu.add(Menu.NONE, R.id.browse_songs, 0, R.string.CONTEXTMENU_BROWSE_SONGS); - menu.add(Menu.NONE, CONTEXTMENU_BROWSE_ALBUMS, 1, R.string.CONTEXTMENU_BROWSE_ALBUMS); - menu.add(Menu.NONE, R.id.play_now, Menu.NONE, R.string.CONTEXTMENU_PLAY_ITEM); - menu.add(Menu.NONE, R.id.add_to_playlist, Menu.NONE, R.string.CONTEXTMENU_ADD_ITEM); + menu.add(Menu.NONE, R.id.browse_songs, 0, R.string.BROWSE_SONGS); + menu.add(Menu.NONE, BROWSE_ALBUMS, 1, R.string.BROWSE_ALBUMS); + menu.add(Menu.NONE, R.id.play_now, Menu.NONE, R.string.PLAY_NOW); + menu.add(Menu.NONE, R.id.add_to_playlist, Menu.NONE, R.string.ADD_TO_END); } - - public String getQuantityString(int quantity) { - return getActivity().getResources().getQuantityString(R.plurals.artist, quantity); - } - } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/AddAction.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/AddAction.java new file mode 100644 index 000000000..ca46424ab --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/AddAction.java @@ -0,0 +1,20 @@ +package uk.org.ngo.squeezer.itemlist.action; + +import android.os.RemoteException; +import android.util.Log; + +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItem; + +public class AddAction extends PlayableItemAction { + + public AddAction(ItemListActivity activity) { + super(activity); + } + + @Override + public void execute(PlaylistItem item) throws RemoteException { + Log.d(getTag(), "Adding song to playlist"); + activity.add(item); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/BrowseSongsAction.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/BrowseSongsAction.java new file mode 100644 index 000000000..357bc498b --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/BrowseSongsAction.java @@ -0,0 +1,22 @@ +package uk.org.ngo.squeezer.itemlist.action; + +import android.os.RemoteException; +import android.util.Log; + +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItem; +import uk.org.ngo.squeezer.itemlist.SongListActivity; + +public class BrowseSongsAction extends PlayableItemAction { + + public BrowseSongsAction(ItemListActivity activity) { + super(activity); + } + + @Override + public void execute(PlaylistItem item) throws RemoteException { + Log.d(getTag(), "Browsing songs of " + item); + SongListActivity.show(activity, item); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/InsertAction.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/InsertAction.java new file mode 100644 index 000000000..5d923a53e --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/InsertAction.java @@ -0,0 +1,19 @@ +package uk.org.ngo.squeezer.itemlist.action; + +import android.os.RemoteException; + +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItem; + +public class InsertAction extends PlayableItemAction { + + public InsertAction(ItemListActivity activity) { + super(activity); + } + + @Override + public void execute(PlaylistItem item) throws RemoteException { + activity.insert(item); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/PlayAction.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/PlayAction.java new file mode 100644 index 000000000..dec7ab3af --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/PlayAction.java @@ -0,0 +1,21 @@ +package uk.org.ngo.squeezer.itemlist.action; + +import android.os.RemoteException; +import android.util.Log; + +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItem; + +public class PlayAction extends PlayableItemAction { + + public PlayAction(ItemListActivity activity) { + super(activity); + } + + @Override + public void execute(PlaylistItem item) throws RemoteException { + Log.d(getTag(), "Playing song"); + activity.play(item); + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/PlayableItemAction.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/PlayableItemAction.java new file mode 100644 index 000000000..efbf8222d --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/action/PlayableItemAction.java @@ -0,0 +1,75 @@ +package uk.org.ngo.squeezer.itemlist.action; + +import android.os.RemoteException; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.ItemListActivity; +import uk.org.ngo.squeezer.framework.PlaylistItem; + +public abstract class PlayableItemAction { + + public static enum Type { + /** + * Do nothing + */ + NONE(R.string.NO_ACTION), + /** + * PLay song immediately + */ + PLAY(R.string.PLAY_NOW), + /** + * Add the song to the playlist + */ + ADD(R.string.ADD_TO_END), + /** + * Play the song after the current song + */ + INSERT(R.string.PLAY_NEXT), + /** + * Browse contents. + */ + BROWSE(R.string.BROWSE_SONGS); + + public final int labelId; + + private Type(int label) { + this.labelId = label; + } + } + + protected final ItemListActivity activity; + + public PlayableItemAction(ItemListActivity activity) { + super(); + this.activity = activity; + } + + protected String getTag() { + return getClass().getSimpleName(); + } + + public abstract void execute(PlaylistItem item) + throws RemoteException; + + public static PlayableItemAction createAction( + ItemListActivity activity, String actionType) { + if (actionType == null || actionType.equals("")) { + return new PlayAction(activity); + } + + Type type = Type.valueOf(actionType); + switch (type) { + case NONE: + return null; + case BROWSE: + return new BrowseSongsAction(activity); + case ADD: + return new AddAction(activity); + case INSERT: + return new InsertAction(activity); + case PLAY: + default: + return new PlayAction(activity); + } + } +} diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerAlbumFilterDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/AlbumFilterDialog.java similarity index 58% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerAlbumFilterDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/AlbumFilterDialog.java index f3c2c1991..855cc65d4 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerAlbumFilterDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/AlbumFilterDialog.java @@ -1,28 +1,34 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerAlbumListActivity; -import uk.org.ngo.squeezer.model.SqueezerGenre; -import uk.org.ngo.squeezer.model.SqueezerYear; import android.app.Dialog; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Spinner; -public class SqueezerAlbumFilterDialog extends SqueezerBaseFilterDialog { - private SqueezerAlbumListActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.AlbumListActivity; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Year; + +public class AlbumFilterDialog extends BaseFilterDialog { + + private AlbumListActivity activity; + private Spinner genreSpinnerView; + private Spinner yearSpinnerView; + private EditText editText; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerAlbumListActivity) getActivity(); + activity = (AlbumListActivity) getActivity(); editText = (EditText) filterForm.findViewById(R.id.search_string); - editText.setHint(getString(R.string.filter_text_hint, activity.getItemAdapter().getQuantityString(2))); + editText.setHint(getString(R.string.filter_text_hint, + activity.getItemAdapter().getQuantityString(2))); editText.setText(activity.getSearchString()); genreSpinnerView = (Spinner) filterForm.findViewById(R.id.genre_spinner); @@ -31,11 +37,12 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { activity.setYearSpinner(yearSpinnerView); if (activity.getSong() != null) { - ((EditText)filterForm.findViewById(R.id.track)).setText(activity.getSong().getName()); + ((EditText) filterForm.findViewById(R.id.track)).setText(activity.getSong().getName()); filterForm.findViewById(R.id.track_view).setVisibility(View.VISIBLE); } if (activity.getArtist() != null) { - ((EditText)filterForm.findViewById(R.id.artist)).setText(activity.getArtist().getName()); + ((EditText) filterForm.findViewById(R.id.artist)) + .setText(activity.getArtist().getName()); filterForm.findViewById(R.id.artist_view).setVisibility(View.VISIBLE); } @@ -45,9 +52,9 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { @Override protected void filter() { activity.setSearchString(editText.getText().toString()); - activity.setGenre((SqueezerGenre) genreSpinnerView.getSelectedItem()); - activity.setYear((SqueezerYear) yearSpinnerView.getSelectedItem()); - activity.orderItems(); + activity.setGenre((Genre) genreSpinnerView.getSelectedItem()); + activity.setYear((Year) yearSpinnerView.getSelectedItem()); + activity.clearAndReOrderItems(); } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/AlbumViewDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/AlbumViewDialog.java new file mode 100644 index 000000000..d64a88e6c --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/AlbumViewDialog.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.itemlist.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CheckedTextView; +import android.widget.TextView; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.AlbumListActivity; +import uk.org.ngo.squeezer.service.ServerString; + +public class AlbumViewDialog extends DialogFragment { + + private static final int POSITION_SORT_LABEL = AlbumListLayout.values().length; + + private static final int POSITION_SORT_START = POSITION_SORT_LABEL + 1; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlbumListActivity activity = (AlbumListActivity) getActivity(); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getServerString(ServerString.ALBUM_DISPLAY_OPTIONS)); + builder.setAdapter(new BaseAdapter() { + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return (position != POSITION_SORT_LABEL); + } + + @Override + public int getCount() { + return AlbumListLayout.values().length + 1 + AlbumsSortOrder + .values().length; + } + + @Override + public Object getItem(int i) { + return null; + } + + @Override + public long getItemId(int i) { + return i; + } + + + @Override + public View getView(int position, View convertView, + ViewGroup parent) { + if (position < POSITION_SORT_LABEL) { + CheckedTextView textView = (CheckedTextView) activity + .getLayoutInflater() + .inflate(android.R.layout.select_dialog_singlechoice, + parent, false); + AlbumListLayout listLayout = AlbumListLayout + .values()[position]; + textView.setCompoundDrawablesWithIntrinsicBounds( + listLayout.icon, 0, 0, 0); + textView.setText( + activity.getServerString(listLayout.serverString)); + textView.setChecked(listLayout == activity.getListLayout()); + return textView; + } else if (position > POSITION_SORT_LABEL) { + CheckedTextView textView = (CheckedTextView) activity + .getLayoutInflater() + .inflate(android.R.layout.select_dialog_singlechoice, + parent, false); + position -= POSITION_SORT_START; + AlbumsSortOrder sortOrder = AlbumsSortOrder + .values()[position]; + textView.setText( + activity.getServerString(sortOrder.serverString)); + textView.setChecked(sortOrder == activity.getSortOrder()); + return textView; + } + + TextView textView = new TextView(activity, null, + android.R.attr.listSeparatorTextViewStyle); + textView.setText(getString(R.string.choose_sort_order, + activity.getItemAdapter().getQuantityString(2))); + return textView; + } + }, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int position) { + if (position < POSITION_SORT_LABEL) { + activity.setListLayout(AlbumListLayout.values()[position]); + dialog.dismiss(); + } else if (position > POSITION_SORT_LABEL) { + position -= POSITION_SORT_START; + activity.setSortOrder(AlbumsSortOrder.values()[position]); + dialog.dismiss(); + } + } + } + ); + return builder.create(); + } + + /** + * Supported album list layouts. + */ + public enum AlbumListLayout { + grid(R.drawable.ic_action_view_as_grid, ServerString.SWITCH_TO_GALLERY), + list(R.drawable.ic_action_view_as_list, ServerString.SWITCH_TO_EXTENDED_LIST); + + /** + * The icon to use for this layout + */ + private int icon; + + /** + * The text to use for this layout + */ + private ServerString serverString; + + private AlbumListLayout(int icon, ServerString serverString) { + this.serverString = serverString; + this.icon = icon; + } + } + + /** + * Sort order strings supported by the server. + *

+ * Values must correspond with the string expected by the server. Any '__' in the strings will + * be removed. + */ + public enum AlbumsSortOrder { + __new(ServerString.BROWSE_NEW_MUSIC), + album(ServerString.ALBUM), + artflow(ServerString.SORT_ARTISTYEARALBUM), + artistalbum(ServerString.SORT_ARTISTALBUM), + yearalbum(ServerString.SORT_YEARALBUM), + yearartistalbum(ServerString.SORT_YEARARTISTALBUM); + + private ServerString serverString; + + private AlbumsSortOrder(ServerString serverString) { + this.serverString = serverString; + } + } +} diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerArtistFilterDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/ArtistFilterDialog.java similarity index 66% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerArtistFilterDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/ArtistFilterDialog.java index 16c580dc2..f1d11dfbe 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerArtistFilterDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/ArtistFilterDialog.java @@ -1,26 +1,31 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerArtistListActivity; -import uk.org.ngo.squeezer.model.SqueezerGenre; import android.app.Dialog; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Spinner; -public class SqueezerArtistFilterDialog extends SqueezerBaseFilterDialog { - private SqueezerArtistListActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.ArtistListActivity; +import uk.org.ngo.squeezer.model.Genre; + +public class ArtistFilterDialog extends BaseFilterDialog { + + private ArtistListActivity activity; + private EditText editText; + private Spinner genreSpinnerView; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerArtistListActivity) getActivity(); + activity = (ArtistListActivity) getActivity(); editText = (EditText) filterForm.findViewById(R.id.search_string); - editText.setHint(getString(R.string.filter_text_hint, activity.getItemAdapter().getQuantityString(2))); + editText.setHint(getString(R.string.filter_text_hint, + activity.getItemAdapter().getQuantityString(2))); editText.setText(activity.getSearchString()); filterForm.findViewById(R.id.year_view).setVisibility(View.GONE); @@ -38,8 +43,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { @Override protected void filter() { activity.setSearchString(editText.getText().toString()); - activity.setGenre((SqueezerGenre) genreSpinnerView.getSelectedItem()); - activity.orderItems(); + activity.setGenre((Genre) genreSpinnerView.getSelectedItem()); + activity.clearAndReOrderItems(); } } diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerBaseEditTextDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/BaseEditTextDialog.java similarity index 84% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerBaseEditTextDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/BaseEditTextDialog.java index fe9e8a170..780629ca1 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerBaseEditTextDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/BaseEditTextDialog.java @@ -1,6 +1,5 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; @@ -11,8 +10,12 @@ import android.view.View.OnKeyListener; import android.widget.EditText; -public abstract class SqueezerBaseEditTextDialog extends DialogFragment { +import uk.org.ngo.squeezer.R; + +public abstract class BaseEditTextDialog extends DialogFragment { + protected EditText editText; + abstract protected boolean commit(String string); @Override @@ -25,9 +28,11 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { editText.setText(""); editText.setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { - if (commit(editText.getText().toString())) + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode + == KeyEvent.KEYCODE_ENTER)) { + if (commit(editText.getText().toString())) { dismiss(); + } return true; } return false; diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerBaseFilterDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/BaseFilterDialog.java similarity index 89% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerBaseFilterDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/BaseFilterDialog.java index 54d2865d7..4afcce2ec 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerBaseFilterDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/BaseFilterDialog.java @@ -1,6 +1,5 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; @@ -11,8 +10,12 @@ import android.view.View.OnKeyListener; import android.widget.EditText; -public abstract class SqueezerBaseFilterDialog extends DialogFragment { +import uk.org.ngo.squeezer.R; + +public abstract class BaseFilterDialog extends DialogFragment { + protected View filterForm; + protected abstract void filter(); @Override @@ -26,7 +29,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { EditText editText = (EditText) filterForm.findViewById(R.id.search_string); editText.setOnKeyListener(new OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { - if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { + if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode + == KeyEvent.KEYCODE_ENTER)) { filter(); dismiss(); return true; diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistDeleteDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistDeleteDialog.java similarity index 56% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistDeleteDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistDeleteDialog.java index a74843853..1e2857473 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistDeleteDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistDeleteDialog.java @@ -1,33 +1,28 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerPlaylistSongsActivity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; -import android.os.RemoteException; import android.support.v4.app.DialogFragment; -import android.util.Log; -public class SqueezerPlaylistDeleteDialog extends DialogFragment { - private SqueezerPlaylistSongsActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.PlaylistSongsActivity; + +public class PlaylistDeleteDialog extends DialogFragment { + + private PlaylistSongsActivity activity; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - activity = (SqueezerPlaylistSongsActivity) getActivity(); + activity = (PlaylistSongsActivity) getActivity(); builder.setTitle(getString(R.string.delete_title, activity.getPlaylist().getName())); builder.setMessage(R.string.delete__message); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - try { - activity.getService().playlistsDelete(activity.getPlaylist()); - activity.finish(); - } catch (RemoteException e) { - Log.e(getTag(), "Error deleting playlist"); - } + activity.playlistDelete(); } }); builder.setNegativeButton(android.R.string.cancel, null); diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistItemMoveDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistItemMoveDialog.java similarity index 62% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistItemMoveDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistItemMoveDialog.java index 74c25a168..b845a6ae2 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistItemMoveDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistItemMoveDialog.java @@ -1,25 +1,29 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.Util; -import uk.org.ngo.squeezer.framework.SqueezerBaseListActivity; -import uk.org.ngo.squeezer.model.SqueezerPlaylist; import android.app.Dialog; import android.os.Bundle; import android.os.RemoteException; import android.text.InputType; import android.util.Log; -public class SqueezerPlaylistItemMoveDialog extends SqueezerBaseEditTextDialog { - private SqueezerBaseListActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.BaseListActivity; +import uk.org.ngo.squeezer.model.Playlist; + +public class PlaylistItemMoveDialog extends BaseEditTextDialog { + + private BaseListActivity activity; + private int fromIndex; - private SqueezerPlaylist playlist; + + private Playlist playlist; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerBaseListActivity) getActivity(); + activity = (BaseListActivity) getActivity(); Bundle args = getArguments(); fromIndex = args.getInt("fromIndex"); playlist = args.getParcelable("playlist"); @@ -35,29 +39,32 @@ protected boolean commit(String targetString) { int targetIndex = Util.parseDecimalInt(targetString, -1); if (targetIndex > 0 && targetIndex <= activity.getItemAdapter().getCount()) { try { - if (playlist == null) - activity.getService().playlistMove(fromIndex-1, targetIndex-1); - else - activity.getService().playlistsMove(playlist, fromIndex-1, targetIndex-1); - activity.orderItems(); + if (playlist == null) { + activity.getService().playlistMove(fromIndex - 1, targetIndex - 1); + } else { + activity.getService().playlistsMove(playlist, fromIndex - 1, targetIndex - 1); + } + activity.clearAndReOrderItems(); } catch (RemoteException e) { - Log.e(getTag(), "Error moving song from '"+ fromIndex + "' to '" +targetIndex + "': " + e); + Log.e(getTag(), + "Error moving song from '" + fromIndex + "' to '" + targetIndex + "': " + + e); } return true; } return false; } - public static void addTo(SqueezerBaseListActivity activity, int fromIndex) { - SqueezerPlaylistItemMoveDialog dialog = new SqueezerPlaylistItemMoveDialog(); + public static void addTo(BaseListActivity activity, int fromIndex) { + PlaylistItemMoveDialog dialog = new PlaylistItemMoveDialog(); Bundle args = new Bundle(); args.putInt("fromIndex", fromIndex + 1); dialog.setArguments(args); dialog.show(activity.getSupportFragmentManager(), "MoveDialog"); } - public static void addTo(SqueezerBaseListActivity activity, SqueezerPlaylist playlist, int fromIndex) { - SqueezerPlaylistItemMoveDialog dialog = new SqueezerPlaylistItemMoveDialog(); + public static void addTo(BaseListActivity activity, Playlist playlist, int fromIndex) { + PlaylistItemMoveDialog dialog = new PlaylistItemMoveDialog(); Bundle args = new Bundle(); args.putInt("fromIndex", fromIndex + 1); args.putParcelable("playlist", playlist); diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistRenameDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistRenameDialog.java similarity index 68% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistRenameDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistRenameDialog.java index d7b6efb79..bb13d7bbd 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistRenameDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistRenameDialog.java @@ -1,19 +1,21 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerPlaylistSongsActivity; import android.app.Dialog; import android.os.Bundle; import android.text.InputType; -public class SqueezerPlaylistRenameDialog extends SqueezerBaseEditTextDialog { - private SqueezerPlaylistSongsActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.PlaylistSongsActivity; + +public class PlaylistRenameDialog extends BaseEditTextDialog { + + private PlaylistSongsActivity activity; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerPlaylistSongsActivity) getActivity(); + activity = (PlaylistSongsActivity) getActivity(); dialog.setTitle(getString(R.string.rename_title, activity.getPlaylist().getName())); editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); editText.setText(activity.getPlaylist().getName()); diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistSaveDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistSaveDialog.java similarity index 65% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistSaveDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistSaveDialog.java index 089e4dae2..93d8bdea2 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistSaveDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistSaveDialog.java @@ -1,15 +1,17 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.framework.SqueezerBaseActivity; import android.app.Dialog; import android.os.Bundle; import android.os.RemoteException; import android.text.InputType; import android.util.Log; -public class SqueezerPlaylistSaveDialog extends SqueezerBaseEditTextDialog { - private SqueezerBaseActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.framework.BaseActivity; + +public class PlaylistSaveDialog extends BaseEditTextDialog { + + private BaseActivity activity; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { @@ -18,28 +20,29 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle args = getArguments(); String name = args.getString("name"); - activity = (SqueezerBaseActivity) getActivity(); + activity = (BaseActivity) getActivity(); dialog.setTitle(R.string.save_playlist_title); editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); editText.setHint(R.string.save_playlist_hint); - if (name != null && name.length() > 0) + if (name != null && name.length() > 0) { editText.setText(name); + } return dialog; - }; + } @Override protected boolean commit(String name) { try { activity.getService().playlistSave(name); } catch (RemoteException e) { - Log.e(getTag(), "Error saving playlist as '"+ name + "': " + e); + Log.e(getTag(), "Error saving playlist as '" + name + "': " + e); } return true; } - public static void addTo(SqueezerBaseActivity activity, String name) { - SqueezerPlaylistSaveDialog dialog = new SqueezerPlaylistSaveDialog(); + public static void addTo(BaseActivity activity, String name) { + PlaylistSaveDialog dialog = new PlaylistSaveDialog(); Bundle args = new Bundle(); args.putString("name", name); dialog.setArguments(args); diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsDeleteDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsDeleteDialog.java similarity index 77% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsDeleteDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsDeleteDialog.java index 0986dd9aa..2afb99973 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsDeleteDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsDeleteDialog.java @@ -1,7 +1,5 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerPlaylistsActivity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; @@ -10,21 +8,25 @@ import android.support.v4.app.DialogFragment; import android.util.Log; -public class SqueezerPlaylistsDeleteDialog extends DialogFragment { - private SqueezerPlaylistsActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.PlaylistsActivity; + +public class PlaylistsDeleteDialog extends DialogFragment { + + private PlaylistsActivity activity; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - activity = (SqueezerPlaylistsActivity) getActivity(); + activity = (PlaylistsActivity) getActivity(); builder.setTitle(getString(R.string.delete_title, activity.getCurrentPlaylist().getName())); builder.setMessage(R.string.delete__message); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { try { activity.getService().playlistsDelete(activity.getCurrentPlaylist()); - activity.orderItems(); + activity.clearAndReOrderItems(); } catch (RemoteException e) { Log.e(getTag(), "Error deleting playlist"); } diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsNewDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsNewDialog.java similarity index 65% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsNewDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsNewDialog.java index b451438d1..1b6991891 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsNewDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsNewDialog.java @@ -1,21 +1,23 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerPlaylistsActivity; import android.app.Dialog; import android.os.Bundle; import android.os.RemoteException; import android.text.InputType; import android.util.Log; -public class SqueezerPlaylistsNewDialog extends SqueezerBaseEditTextDialog { - private SqueezerPlaylistsActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.PlaylistsActivity; + +public class PlaylistsNewDialog extends BaseEditTextDialog { + + private PlaylistsActivity activity; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerPlaylistsActivity) getActivity(); + activity = (PlaylistsActivity) getActivity(); dialog.setTitle(R.string.new_playlist_title); editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); editText.setHint(R.string.new_playlist_hint); @@ -27,9 +29,9 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { protected boolean commit(String name) { try { activity.getService().playlistsNew(name); - activity.orderItems(); + activity.clearAndReOrderItems(); } catch (RemoteException e) { - Log.e(getTag(), "Error saving playlist as '"+ name + "': " + e); + Log.e(getTag(), "Error saving playlist as '" + name + "': " + e); } return true; } diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsRenameDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsRenameDialog.java similarity index 69% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsRenameDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsRenameDialog.java index 7cb47f783..534319fd6 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerPlaylistsRenameDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/PlaylistsRenameDialog.java @@ -1,19 +1,21 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerPlaylistsActivity; import android.app.Dialog; import android.os.Bundle; import android.text.InputType; -public class SqueezerPlaylistsRenameDialog extends SqueezerBaseEditTextDialog { - private SqueezerPlaylistsActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.PlaylistsActivity; + +public class PlaylistsRenameDialog extends BaseEditTextDialog { + + private PlaylistsActivity activity; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerPlaylistsActivity) getActivity(); + activity = (PlaylistsActivity) getActivity(); dialog.setTitle(getString(R.string.rename_title, activity.getCurrentPlaylist().getName())); editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); editText.setText(activity.getCurrentPlaylist().getName()); diff --git a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerSongFilterDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/SongFilterDialog.java similarity index 62% rename from src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerSongFilterDialog.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/SongFilterDialog.java index 009acf7f0..435fa50b3 100644 --- a/src/uk/org/ngo/squeezer/itemlists/dialogs/SqueezerSongFilterDialog.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/SongFilterDialog.java @@ -1,28 +1,34 @@ -package uk.org.ngo.squeezer.itemlists.dialogs; +package uk.org.ngo.squeezer.itemlist.dialog; -import uk.org.ngo.squeezer.R; -import uk.org.ngo.squeezer.itemlists.SqueezerSongListActivity; -import uk.org.ngo.squeezer.model.SqueezerGenre; -import uk.org.ngo.squeezer.model.SqueezerYear; import android.app.Dialog; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Spinner; -public class SqueezerSongFilterDialog extends SqueezerBaseFilterDialog { - private SqueezerSongListActivity activity; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.SongListActivity; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.Year; + +public class SongFilterDialog extends BaseFilterDialog { + + private SongListActivity activity; + private Spinner genreSpinnerView; + private Spinner yearSpinnerView; + private EditText editText; @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - activity = (SqueezerSongListActivity) getActivity(); + activity = (SongListActivity) getActivity(); editText = (EditText) filterForm.findViewById(R.id.search_string); - editText.setHint(getString(R.string.filter_text_hint, activity.getItemAdapter().getQuantityString(2))); + editText.setHint(getString(R.string.filter_text_hint, + activity.getItemAdapter().getQuantityString(2))); editText.setText(activity.getSearchString()); genreSpinnerView = (Spinner) filterForm.findViewById(R.id.genre_spinner); @@ -31,7 +37,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { activity.setYearSpinner(yearSpinnerView); if (activity.getArtist() != null) { - ((EditText)filterForm.findViewById(R.id.artist)).setText(activity.getArtist().getName()); + ((EditText) filterForm.findViewById(R.id.artist)) + .setText(activity.getArtist().getName()); filterForm.findViewById(R.id.artist_view).setVisibility(View.VISIBLE); } if (activity.getAlbum() != null) { @@ -40,14 +47,14 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { } return dialog; -} + } @Override protected void filter() { activity.setSearchString(editText.getText().toString()); - activity.setGenre((SqueezerGenre) genreSpinnerView.getSelectedItem()); - activity.setYear((SqueezerYear) yearSpinnerView.getSelectedItem()); - activity.orderItems(); + activity.setGenre((Genre) genreSpinnerView.getSelectedItem()); + activity.setYear((Year) yearSpinnerView.getSelectedItem()); + activity.clearAndReOrderItems(); } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/SongOrderDialog.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/SongOrderDialog.java new file mode 100644 index 000000000..1c54ab5c5 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/itemlist/dialog/SongOrderDialog.java @@ -0,0 +1,60 @@ +package uk.org.ngo.squeezer.itemlist.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.itemlist.SongListActivity; + +public class SongOrderDialog extends DialogFragment { + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final SongListActivity activity = (SongListActivity) getActivity(); + + String[] sortOrderStrings = new String[SongsSortOrder.values().length]; + for (SongsSortOrder sortOrder : SongsSortOrder.values()) { + sortOrderStrings[sortOrder.ordinal()] = getString(sortOrder.stringResource); + } + + int checkedItem = activity.getSortOrder().ordinal(); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getString(R.string.choose_sort_order, + activity.getItemAdapter().getQuantityString(2))); + builder.setSingleChoiceItems(sortOrderStrings, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int indexSelected) { + activity.setSortOrder(SongsSortOrder.values()[indexSelected]); + dialog.dismiss(); + } + }); + return builder.create(); + } + + /** + * Sort order strings supported by the server. + *

+ * Values must correspond with the string expected by the server. Any '__' in the strings will + * be removed. + */ + public enum SongsSortOrder { + title(R.string.songs_sort_order_title), + tracknum(R.string.songs_sort_order_tracknum); + // TODO: At least some versions of the server support "albumtrack", + // is that useful? + + /** + * The text to use for this ordering + */ + private int stringResource; + + private SongsSortOrder(int stringResource) { + this.stringResource = stringResource; + } + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/BaseMenuFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/BaseMenuFragment.java new file mode 100644 index 000000000..4c11d3bf9 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/BaseMenuFragment.java @@ -0,0 +1,52 @@ +package uk.org.ngo.squeezer.menu; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; + +/** + * A base class to be extended by fragments which would like to participate in populating the action + * bar, and which are used in an {@link ActionBarActivity} . + *

+ * This class takes care of removing action bar items from the options menu. It also contains a few + * convenience methods to ease using a menu fragment. + * + * @author Kurt Aaholst + */ +public class BaseMenuFragment extends Fragment { + + /** + * Just a little helper, which calls {@link #setHasOptionsMenu(boolean)} with a true argument. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + /** + * Conditionally add a fragment of the given type to the supplied activity. If a fragment of the + * given type has not previously been added to the activity (by this method), then a new + * instance of the fragment is created, and added to the activity by calling {@link + * FragmentTransaction#add(Fragment, String)} + * + * @param activity The activity to add the new fragment to + * @param clazz Type of the fragment to add to the activity + */ + public static void add(FragmentActivity activity, Class clazz) { + FragmentManager fragmentManager = activity.getSupportFragmentManager(); + if (fragmentManager.findFragmentByTag(clazz.getName()) == null) { + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + try { + fragmentTransaction.add(clazz.newInstance(), clazz.getName()); + } catch (Exception e) { + throw new InstantiationException( + "Unable to instantiate fragment " + clazz.getName(), e); + } + fragmentTransaction.commit(); + } + } + +} diff --git a/src/uk/org/ngo/squeezer/menu/SqueezerFilterMenuItemFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/FilterMenuFragment.java similarity index 69% rename from src/uk/org/ngo/squeezer/menu/SqueezerFilterMenuItemFragment.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/menu/FilterMenuFragment.java index ae61ea8e7..daa598d10 100644 --- a/src/uk/org/ngo/squeezer/menu/SqueezerFilterMenuItemFragment.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/FilterMenuFragment.java @@ -1,51 +1,51 @@ package uk.org.ngo.squeezer.menu; -import uk.org.ngo.squeezer.R; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import uk.org.ngo.squeezer.R; + /** * A fragment that implements a "Filter" menu. - *

- * Activities that host this fragment must implement - * {@link #SqueezerFilterableListActivity}. - * + *

+ * Activities that host this fragment must implement {@link #FilterableListActivity}. + * *

- * {@code
  * public void onCreate(Bundle savedInstanceState) {
  *     ...
- *     MenuFragment.add(this, SqueezerFilterMenuItemFragment.class);
+ *     BaseMenuFragment.add(this, FilterMenuFragment.class);
  * }
- * 
+ *
  * public void showFilterDialog() {
  * }
  * 
*/ -public class SqueezerFilterMenuItemFragment extends MenuFragment { - SqueezerFilterableListActivity activity; +public class FilterMenuFragment extends BaseMenuFragment { + + FilterableListActivity activity; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - }; + } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.filtermenuitem, menu); super.onCreateOptionsMenu(menu, inflater); - activity = (SqueezerFilterableListActivity) getActivity(); + activity = (FilterableListActivity) getActivity(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.menu_item_filter: - activity.showFilterDialog(); - return true; + case R.id.menu_item_filter: + activity.showFilterDialog(); + return true; } return super.onOptionsItemSelected(item); } @@ -53,15 +53,15 @@ public boolean onOptionsItemSelected(MenuItem item) { /** * Interface that activities that host this fragment must implement. */ - public interface SqueezerFilterableListActivity { + public interface FilterableListActivity { + /** * Show a dialog allowing the user to specify how to filter the results. */ public void showFilterDialog(); /** - * Ensure that the activity that hosts this fragment derives from - * FragmentActivity. + * Ensure that the activity that hosts this fragment derives from FragmentActivity. */ FragmentManager getSupportFragmentManager(); } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/MenuFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/MenuFragment.java new file mode 100644 index 000000000..ff3595fa4 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/MenuFragment.java @@ -0,0 +1,21 @@ +package uk.org.ngo.squeezer.menu; + +import android.view.MenuItem; + +import uk.org.ngo.squeezer.HomeActivity; + + +public class MenuFragment extends BaseMenuFragment { + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // Application icon clicked. + case android.R.id.home: + HomeActivity.show(getActivity()); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/src/uk/org/ngo/squeezer/menu/SqueezerOrderMenuItemFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/OrderMenuItemFragment.java similarity index 69% rename from src/uk/org/ngo/squeezer/menu/SqueezerOrderMenuItemFragment.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/menu/OrderMenuItemFragment.java index 0d096e5b9..7e5c01767 100644 --- a/src/uk/org/ngo/squeezer/menu/SqueezerOrderMenuItemFragment.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/OrderMenuItemFragment.java @@ -1,38 +1,38 @@ package uk.org.ngo.squeezer.menu; -import uk.org.ngo.squeezer.R; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import uk.org.ngo.squeezer.R; + /** * A fragment that implements a "Sort" menu. - *

- * Activities that host this fragment must implement - * {@link #SqueezerOrderableListActivity}. - * + *

+ * Activities that host this fragment must implement {@link #OrderableListActivity}. + * *

- * {@code
  * public void onCreate(Bundle savedInstanceState) {
  *     ...
- *     MenuFragment.add(this, SqueezerOrderMenuItemFragment.class);
+ *     BaseMenuFragment.add(this, OrderMenuItemFragment.class);
  * }
- * 
+ *
  * public void showOrderDialog() {
  * }
  * 
*/ -public class SqueezerOrderMenuItemFragment extends MenuFragment { - SqueezerOrderableListActivity activity; +public class OrderMenuItemFragment extends BaseMenuFragment { + + OrderableListActivity activity; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - activity = (SqueezerOrderableListActivity) getActivity(); - }; + activity = (OrderableListActivity) getActivity(); + } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @@ -43,9 +43,9 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.menu_item_sort: - activity.showOrderDialog(); - return true; + case R.id.menu_item_sort: + activity.showOrderDialog(); + return true; } return super.onOptionsItemSelected(item); } @@ -53,15 +53,15 @@ public boolean onOptionsItemSelected(MenuItem item) { /** * Interface that activities that host this fragment must implement. */ - public interface SqueezerOrderableListActivity { + public interface OrderableListActivity { + /** * Show a dialog allowing the user to choose the sort order. */ public void showOrderDialog(); /** - * Ensure that the activity that hosts this fragment derives from - * FragmentActivity. + * Ensure that the activity that hosts this fragment derives from FragmentActivity. */ FragmentManager getSupportFragmentManager(); } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/ViewMenuItemFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/ViewMenuItemFragment.java new file mode 100644 index 000000000..9480179f7 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/menu/ViewMenuItemFragment.java @@ -0,0 +1,69 @@ +package uk.org.ngo.squeezer.menu; + +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import uk.org.ngo.squeezer.R; + +/** + * A fragment that implements a "View" menu. + *

+ * Activities that host this fragment must implement {@link ListActivityWithViewMenu}. + *

+ *

+ * {@code
+ * public void onCreate(Bundle savedInstanceState) {
+ *     ...
+ *     BaseMenuFragment.add(this, OrderMenuItemFragment.class);
+ * }
+ *
+ * public void showViewDialog() {
+ * }
+ * 
+ */ +public class ViewMenuItemFragment extends BaseMenuFragment { + + ListActivityWithViewMenu activity; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + activity = (ListActivityWithViewMenu) getActivity(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.viewmenuitem, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_item_view: + activity.showViewDialog(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * Interface that activities that host this fragment must implement. + */ + public interface ListActivityWithViewMenu { + + /** + * Show a dialog allowing the user to choose the sort order. + */ + public void showViewDialog(); + + /** + * Ensure that the activity that hosts this fragment derives from FragmentActivity. + */ + FragmentManager getSupportFragmentManager(); + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Album.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Album.java new file mode 100644 index 000000000..951a35c84 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Album.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.ArtworkItem; + + +public class Album extends ArtworkItem { + + @Override + public String getPlaylistTag() { + return "album_id"; + } + + private String name; + + @Override + public String getName() { + return name; + } + + public Album setName(String name) { + this.name = name; + return this; + } + + private String artist; + + public String getArtist() { + return artist; + } + + public void setArtist(String model) { + this.artist = model; + } + + private int year; + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public Album(String albumId, String album) { + setId(albumId); + setName(album); + } + + public Album(Map record) { + setId(record.containsKey("album_id") ? record.get("album_id") : record.get("id")); + setName(record.get("album")); + setArtist(record.get("artist")); + setYear(Util.parseDecimalIntOrZero(record.get("year"))); + setArtwork_track_id(record.get("artwork_track_id")); + } + + public static final Creator CREATOR = new Creator() { + public Album[] newArray(int size) { + return new Album[size]; + } + + public Album createFromParcel(Parcel source) { + return new Album(source); + } + }; + + private Album(Parcel source) { + setId(source.readString()); + name = source.readString(); + artist = source.readString(); + year = source.readInt(); + setArtwork_track_id(source.readString()); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + dest.writeString(artist); + dest.writeInt(year); + dest.writeString(getArtwork_track_id()); + } + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name + ", artist=" + artist + ", year=" + year; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Artist.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Artist.java new file mode 100644 index 000000000..60e33b225 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Artist.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.framework.PlaylistItem; + + +public class Artist extends PlaylistItem { + + @Override + public String getPlaylistTag() { + return "artist_id"; + } + + private String name; + + @Override + public String getName() { + return name; + } + + public Artist setName(String name) { + this.name = name; + return this; + } + + public Artist(String artistId, String artist) { + setId(artistId); + setName(artist); + } + + public Artist(Map record) { + setId(record.containsKey("contributor_id") ? record.get("contributor_id") + : record.get("id")); + name = record.containsKey("contributor") ? record.get("contributor") : record.get("artist"); + } + + public static final Creator CREATOR = new Creator() { + public Artist[] newArray(int size) { + return new Artist[size]; + } + + public Artist createFromParcel(Parcel source) { + return new Artist(source); + } + }; + + private Artist(Parcel source) { + setId(source.readString()); + name = source.readString(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + } + + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Genre.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Genre.java new file mode 100644 index 000000000..1070813d6 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Genre.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.framework.PlaylistItem; + + +public class Genre extends PlaylistItem { + + @Override + public String getPlaylistTag() { + return "genre_id"; + } + + private String name; + + @Override + public String getName() { + return name; + } + + public Genre setName(String name) { + this.name = name; + return this; + } + + public Genre(Map record) { + setId(record.containsKey("genre_id") ? record.get("genre_id") : record.get("id")); + name = record.get("genre"); + } + + public static final Creator CREATOR = new Creator() { + public Genre[] newArray(int size) { + return new Genre[size]; + } + + public Genre createFromParcel(Parcel source) { + return new Genre(source); + } + }; + + private Genre(Parcel source) { + setId(source.readString()); + name = source.readString(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + } + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name; + } + +} diff --git a/src/uk/org/ngo/squeezer/model/SqueezerMusicFolderItem.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/MusicFolderItem.java similarity index 67% rename from src/uk/org/ngo/squeezer/model/SqueezerMusicFolderItem.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/model/MusicFolderItem.java index c9913536f..f851061fd 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerMusicFolderItem.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/MusicFolderItem.java @@ -16,20 +16,21 @@ package uk.org.ngo.squeezer.model; +import android.os.Parcel; + import java.util.Map; -import uk.org.ngo.squeezer.framework.SqueezerPlaylistItem; -import android.os.Parcel; +import uk.org.ngo.squeezer.framework.PlaylistItem; /** - * Encapsulate a music folder item on the Squeezerserver. - *

- * An item has a name and a type. The name is free text, the type may be one of - * "track", "folder", "playlist", or "unknown". + * Encapsulate a music folder item on the Squeezeserver. + *

+ * An item has a name and a type. The name is free text, the type may be one of "track", "folder", + * "playlist", or "unknown". * * @author nik */ -public class SqueezerMusicFolderItem extends SqueezerPlaylistItem { +public class MusicFolderItem extends PlaylistItem { @Override public String getPlaylistTag() { @@ -56,12 +57,14 @@ public String getName() { return name; } - public SqueezerMusicFolderItem setName(String name) { + public MusicFolderItem setName(String name) { this.name = name; return this; } - /** The folder item's type, "track", "folder", "playlist", "unknown". */ + /** + * The folder item's type, "track", "folder", "playlist", "unknown". + */ // XXX: Should be an enum. private String type; @@ -69,33 +72,33 @@ public String getType() { return type; } - public SqueezerMusicFolderItem setType(String type) { + public MusicFolderItem setType(String type) { this.type = type; return this; } - public SqueezerMusicFolderItem(String musicFolderId, String musicFolder) { + public MusicFolderItem(String musicFolderId, String musicFolder) { setId(musicFolderId); setName(musicFolder); } - public SqueezerMusicFolderItem(Map record) { + public MusicFolderItem(Map record) { setId(record.get("id")); name = record.get("filename"); type = record.get("type"); } - public static final Creator CREATOR = new Creator() { - public SqueezerMusicFolderItem[] newArray(int size) { - return new SqueezerMusicFolderItem[size]; + public static final Creator CREATOR = new Creator() { + public MusicFolderItem[] newArray(int size) { + return new MusicFolderItem[size]; } - public SqueezerMusicFolderItem createFromParcel(Parcel source) { - return new SqueezerMusicFolderItem(source); + public MusicFolderItem createFromParcel(Parcel source) { + return new MusicFolderItem(source); } }; - private SqueezerMusicFolderItem(Parcel source) { + private MusicFolderItem(Parcel source) { setId(source.readString()); name = source.readString(); type = source.readString(); diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Player.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Player.java new file mode 100644 index 000000000..5f1d2eb80 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Player.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.Item; + + +public class Player extends Item { + + private String ip; + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + private String name; + + @Override + public String getName() { + return name; + } + + public Player setName(String name) { + this.name = name; + return this; + } + + private boolean canpoweroff; + + public boolean isCanpoweroff() { + return canpoweroff; + } + + public void setCanpoweroff(boolean canpoweroff) { + this.canpoweroff = canpoweroff; + } + + private String model; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public Player(Map record) { + setId(record.get("playerid")); + ip = record.get("ip"); + name = record.get("name"); + model = record.get("model"); + canpoweroff = Util.parseDecimalIntOrZero(record.get("canpoweroff")) == 1; + } + + public static final Creator CREATOR = new Creator() { + public Player[] newArray(int size) { + return new Player[size]; + } + + public Player createFromParcel(Parcel source) { + return new Player(source); + } + }; + + private Player(Parcel source) { + setId(source.readString()); + ip = source.readString(); + name = source.readString(); + model = source.readString(); + canpoweroff = (source.readByte() == 1); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(ip); + dest.writeString(name); + dest.writeString(model); + dest.writeByte(canpoweroff ? (byte) 1 : (byte) 0); + } + + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name + ", model=" + model + ", canpoweroff=" + + canpoweroff + ", ip=" + ip; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/PlayerState.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/PlayerState.java new file mode 100644 index 000000000..864183016 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/PlayerState.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.model; + + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.service.ServerString; + + +public class PlayerState implements Parcelable { + + public PlayerState() { + } + + public static final Creator CREATOR = new Creator() { + @Override + public PlayerState[] newArray(int size) { + return new PlayerState[size]; + } + + @Override + public PlayerState createFromParcel(Parcel source) { + return new PlayerState(source); + } + }; + + private PlayerState(Parcel source) { + playStatus = PlayStatus.valueOf(source.readString()); + poweredOn = (source.readByte() == 1); + shuffleStatus = ShuffleStatus.valueOf(source.readInt()); + repeatStatus = RepeatStatus.valueOf(source.readInt()); + currentSong = source.readParcelable(null); + currentPlaylistIndex = source.readInt(); + currentTimeSecond = source.readInt(); + currentSongDuration = source.readInt(); + currentVolume = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(playStatus.name()); + dest.writeByte(poweredOn ? (byte) 1 : (byte) 0); + dest.writeInt(shuffleStatus.getId()); + dest.writeInt(repeatStatus.getId()); + dest.writeParcelable(currentSong, 0); + dest.writeInt(currentPlaylistIndex); + dest.writeInt(currentTimeSecond); + dest.writeInt(currentSongDuration); + dest.writeInt(currentVolume); + } + + @Override + public int describeContents() { + return 0; + } + + private boolean poweredOn; + + private PlayStatus playStatus; + + private ShuffleStatus shuffleStatus; + + private RepeatStatus repeatStatus; + + private Song currentSong; + + private String currentPlaylist; + + private int currentPlaylistIndex; + + private int currentTimeSecond; + + private int currentSongDuration; + + private int currentVolume; + + public boolean isPlaying() { + return playStatus == PlayStatus.play; + } + + public PlayStatus getPlayStatus() { + return playStatus; + } + + public void setPlayStatus(PlayStatus state) { + playStatus = state; + } + + public boolean isPoweredOn() { + return poweredOn; + } + + public void setPoweredOn(boolean state) { + poweredOn = state; + } + + public ShuffleStatus getShuffleStatus() { + return shuffleStatus; + } + + public void setShuffleStatus(ShuffleStatus status) { + shuffleStatus = status; + } + + public RepeatStatus getRepeatStatus() { + return repeatStatus; + } + + public void setRepeatStatus(RepeatStatus status) { + repeatStatus = status; + } + + public Song getCurrentSong() { + return currentSong; + } + + public String getCurrentSongName() { + return (currentSong != null) ? currentSong.getName() : ""; + } + + public void setCurrentSong(Song song) { + currentSong = song; + } + + public String getCurrentPlaylist() { + return currentPlaylist; + } + + public int getCurrentPlaylistIndex() { + return currentPlaylistIndex; + } + + public void setCurrentPlaylist(String playlist) { + currentPlaylist = playlist; + } + + public PlayerState setCurrentPlaylistIndex(int value) { + currentPlaylistIndex = value; + return this; + } + + public int getCurrentTimeSecond() { + return currentTimeSecond; + } + + public PlayerState setCurrentTimeSecond(int value) { + currentTimeSecond = value; + return this; + } + + public int getCurrentSongDuration() { + return currentSongDuration; + } + + public PlayerState setCurrentSongDuration(int value) { + currentSongDuration = value; + return this; + } + + public int getCurrentVolume() { + return currentVolume; + } + + public PlayerState setCurrentVolume(int value) { + currentVolume = value; + return this; + } + + public static enum PlayStatus { + play, + pause, + stop + } + + public static enum ShuffleStatus implements EnumWithId { + SHUFFLE_OFF(0, R.drawable.btn_shuffle_off, ServerString.SHUFFLE_OFF), + SHUFFLE_SONG(1, R.drawable.btn_shuffle_song, ServerString.SHUFFLE_ON_SONGS), + SHUFFLE_ALBUM(2, R.drawable.btn_shuffle_album, ServerString.SHUFFLE_ON_ALBUMS); + + private int id; + + private int icon; + + private ServerString text; + + private static EnumIdLookup lookup = new EnumIdLookup( + ShuffleStatus.class); + + private ShuffleStatus(int id, int icon, ServerString text) { + this.id = id; + this.icon = icon; + this.text = text; + } + + @Override + public int getId() { + return id; + } + + public int getIcon() { + return icon; + } + + public ServerString getText() { + return text; + } + + public static ShuffleStatus valueOf(int id) { + return lookup.get(id); + } + } + + public static enum RepeatStatus implements EnumWithId { + REPEAT_OFF(0, R.drawable.btn_repeat_off, ServerString.REPEAT_OFF), + REPEAT_ONE(1, R.drawable.btn_repeat_one, ServerString.REPEAT_ONE), + REPEAT_ALL(2, R.drawable.btn_repeat_all, ServerString.REPEAT_ALL); + + private int id; + + private int icon; + + private ServerString text; + + private static EnumIdLookup lookup = new EnumIdLookup( + RepeatStatus.class); + + private RepeatStatus(int id, int icon, ServerString text) { + this.id = id; + this.icon = icon; + this.text = text; + } + + @Override + public int getId() { + return id; + } + + public int getIcon() { + return icon; + } + + public ServerString getText() { + return text; + } + + public static RepeatStatus valueOf(int id) { + return lookup.get(id); + } + } + + public interface EnumWithId { + + int getId(); + } + + public static class EnumIdLookup & EnumWithId> { + + private SparseArray map = new SparseArray(); + + public EnumIdLookup(Class enumType) { + for (E v : enumType.getEnumConstants()) { + map.put(v.getId(), v); + } + } + + public E get(int num) { + return map.get(num); + } + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Playlist.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Playlist.java new file mode 100644 index 000000000..43971a6a6 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Playlist.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.framework.PlaylistItem; + + +public class Playlist extends PlaylistItem { + + @Override + public String getPlaylistTag() { + return "playlist_id"; + } + + private String name; + + @Override + public String getName() { + return name; + } + + public Playlist setName(String name) { + this.name = name; + return this; + } + + public Playlist(Map record) { + setId(record.containsKey("playlist_id") ? record.get("playlist_id") : record.get("id")); + name = record.get("playlist"); + } + + public static final Creator CREATOR = new Creator() { + public Playlist[] newArray(int size) { + return new Playlist[size]; + } + + public Playlist createFromParcel(Parcel source) { + return new Playlist(source); + } + }; + + private Playlist(Parcel source) { + setId(source.readString()); + name = source.readString(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + } + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Plugin.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Plugin.java new file mode 100644 index 000000000..77826eb0b --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Plugin.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.Item; + + +public class Plugin extends Item { + + public static Plugin FAVORITE = new Plugin("favorites", R.drawable.icon_favorites); + + private String name; + + @Override + public String getName() { + return name; + } + + public Plugin setName(String name) { + this.name = name; + return this; + } + + private String icon; + + /** + * @return Relative URL path to an icon for this radio or music service, for example + * "plugins/Picks/html/images/icon.png" + */ + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + private int iconResource; + + /** + * @return Icon resource for this plugin if it is embedded in the Squeezer app, or null. + */ + public int getIconResource() { + return iconResource; + } + + public void setIconResource(int iconResource) { + this.iconResource = iconResource; + } + + private int weight; + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public boolean isSearchable() { + return "xmlbrowser_search".equals(type); + } + + private Plugin(String cmd, int iconResource) { + setId(cmd); + setIconResource(iconResource); + } + + public Plugin(Map record) { + setId(record.get("cmd")); + name = record.get("name"); + type = record.get("type"); + icon = record.get("icon"); + weight = Util.parseDecimalIntOrZero(record.get("weight")); + } + + public static final Creator CREATOR = new Creator() { + public Plugin[] newArray(int size) { + return new Plugin[size]; + } + + public Plugin createFromParcel(Parcel source) { + return new Plugin(source); + } + }; + + private Plugin(Parcel source) { + setId(source.readString()); + name = source.readString(); + type = source.readString(); + icon = source.readString(); + iconResource = source.readInt(); + weight = source.readInt(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + dest.writeString(type); + dest.writeString(icon); + dest.writeInt(iconResource); + dest.writeInt(weight); + } + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/PluginItem.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/PluginItem.java new file mode 100644 index 000000000..1c84034e5 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/PluginItem.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; + +import java.util.Map; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.Item; + +/** + * Represents a single item in a plugin. + */ +public class PluginItem extends Item { + + private String name; + + @Override + public String getName() { + return name; + } + + public PluginItem setName(String name) { + this.name = name; + return this; + } + + private String description; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * Relative URL to the icon to use for this item. + */ + private String image; + + /** + * @return the absolute URL to the icon to use for this item + */ + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + private boolean hasitems; + + public boolean isHasitems() { + return hasitems; + } + + public void setHasitems(boolean hasitems) { + this.hasitems = hasitems; + } + + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public PluginItem(Map record) { + setId(record.get("id")); + name = record.containsKey("name") ? record.get("name") : record.get("title"); + description = record.get("description"); + type = record.get("type"); + image = record.get("image"); + hasitems = (Util.parseDecimalIntOrZero(record.get("hasitems")) != 0); + } + + public static final Creator CREATOR = new Creator() { + public PluginItem[] newArray(int size) { + return new PluginItem[size]; + } + + public PluginItem createFromParcel(Parcel source) { + return new PluginItem(source); + } + }; + + private PluginItem(Parcel source) { + setId(source.readString()); + name = source.readString(); + description = source.readString(); + type = source.readString(); + image = source.readString(); + hasitems = (source.readInt() != 0); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + dest.writeString(description); + dest.writeString(type); + dest.writeString(image); + dest.writeInt(hasitems ? 1 : 0); + } + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name; + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Song.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Song.java new file mode 100644 index 000000000..eec93dede --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Song.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.model; + +import android.os.Parcel; +import android.os.RemoteException; +import android.util.Log; + +import java.util.Map; + +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.ArtworkItem; +import uk.org.ngo.squeezer.service.ISqueezeService; + +public class Song extends ArtworkItem { + + @Override + public String getPlaylistTag() { + return "track_id"; + } + + private String name; + + @Override + public String getName() { + return name; + } + + public Song setName(String name) { + this.name = name; + return this; + } + + private String artist; + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + private Album album; + + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + private String albumName; + + public String getAlbumName() { + return albumName; + } + + public void setAlbumName(String albumName) { + this.albumName = albumName; + } + + private boolean compilation; + + public boolean getCompilation() { + return compilation; + } + + public void setCompilation(boolean compilation) { + this.compilation = compilation; + } + + private int duration; + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + private int year; + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + private String artist_id; + + public String getArtist_id() { + return artist_id; + } + + public void setArtist_id(String artist_id) { + this.artist_id = artist_id; + } + + private String album_id; + + public String getAlbum_id() { + return album_id; + } + + public void setAlbum_id(String album_id) { + this.album_id = album_id; + } + + private boolean remote; + + public boolean isRemote() { + return remote; + } + + public void setRemote(boolean remote) { + this.remote = remote; + } + + public int tracknum; + + public int getTracknum() { + return tracknum; + } + + public void setTracknum(int tracknum) { + this.tracknum = tracknum; + } + + private String artwork_url; + + public String getArtwork_url() { + return artwork_url; + } + + public void setArtwork_url(String artworkUrl) { + artwork_url = artworkUrl; + } + + public String getArtworkUrl(ISqueezeService service) { + if (getArtwork_track_id() != null) { + try { + if (service == null) { + return null; + } + return service.getAlbumArtUrl(getArtwork_track_id()); + } catch (RemoteException e) { + Log.e(getClass().getSimpleName(), "Error requesting album art url: " + e); + } + } + return getArtwork_url(); + } + + public Song(Map record) { + if (getId() == null) { + setId(record.get("track_id")); + } + if (getId() == null) { + setId(record.get("id")); + } + setName(record.containsKey("track") ? record.get("track") : record.get("title")); + setArtist(record.get("artist")); + setAlbumName(record.get("album")); + setCompilation(Util.parseDecimalIntOrZero(record.get("compilation")) == 1); + setDuration(Util.parseDecimalIntOrZero(record.get("duration"))); + setYear(Util.parseDecimalIntOrZero(record.get("year"))); + setArtist_id(record.get("artist_id")); + setAlbum_id(record.get("album_id")); + setRemote(Util.parseDecimalIntOrZero(record.get("remote")) != 0); + setTracknum(Util.parseDecimalInt(record.get("tracknum"), 1)); + setArtwork_url(record.get("artwork_url")); + + // Work around a (possible) bug in the Squeezeserver. + // + // I've seen tracks where the "coverart" tag comes back positive (1) + // but there's no "artwork_track_id" tag. If that happens, use this + // song's ID as the artwork_track_id. + String artworkTrackId = record.get("artwork_track_id"); + if (artworkTrackId != null) { + setArtwork_track_id(artworkTrackId); + } else { + // If there's no cover art then the server doesn't respond + // "coverart:0" or something useful like that, it just doesn't + // include a response. Hence these shenanigans. + String coverArt = record.get("coverart"); + if (coverArt != null && coverArt.equals("1")) { + setArtwork_track_id(getId()); + } + } + + Album album = new Album(album_id, albumName); + album.setArtist(compilation ? "Various" : artist); + album.setArtwork_track_id(artworkTrackId); + album.setYear(year); + setAlbum(album); + } + + public static final Creator CREATOR = new Creator() { + public Song[] newArray(int size) { + return new Song[size]; + } + + public Song createFromParcel(Parcel source) { + return new Song(source); + } + }; + + private Song(Parcel source) { + setId(source.readString()); + name = source.readString(); + artist = source.readString(); + albumName = source.readString(); + compilation = source.readInt() == 1; + duration = source.readInt(); + year = source.readInt(); + artist_id = source.readString(); + album_id = source.readString(); + setArtwork_track_id(source.readString()); + tracknum = source.readInt(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeString(name); + dest.writeString(artist); + dest.writeString(albumName); + dest.writeInt(compilation ? 1 : 0); + dest.writeInt(duration); + dest.writeInt(year); + dest.writeString(artist_id); + dest.writeString(album_id); + dest.writeString(getArtwork_track_id()); + dest.writeInt(tracknum); + } + + @Override + public String toString() { + return "id=" + getId() + ", name=" + name + ", artist=" + artist + ", year=" + year; + } + +} diff --git a/src/uk/org/ngo/squeezer/model/SqueezerYear.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Year.java similarity index 50% rename from src/uk/org/ngo/squeezer/model/SqueezerYear.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/model/Year.java index 5a69ad11d..0e61b395e 100644 --- a/src/uk/org/ngo/squeezer/model/SqueezerYear.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/model/Year.java @@ -16,48 +16,50 @@ package uk.org.ngo.squeezer.model; +import android.os.Parcel; + import java.util.Map; -import uk.org.ngo.squeezer.framework.SqueezerPlaylistItem; -import android.os.Parcel; +import uk.org.ngo.squeezer.framework.PlaylistItem; -public class SqueezerYear extends SqueezerPlaylistItem { +public class Year extends PlaylistItem { @Override public String getPlaylistTag() { return "year_id"; } - public SqueezerYear(Map record) { - setId(record.get("year")); - } - - public static final Creator CREATOR = new Creator() { - public SqueezerYear[] newArray(int size) { - return new SqueezerYear[size]; - } - - public SqueezerYear createFromParcel(Parcel source) { - return new SqueezerYear(source); - } - }; - - private SqueezerYear(Parcel source) { - setId(source.readString()); - } - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(getId()); - } - - @Override - public String getName() { - return getId(); - } - - @Override - public String toString() { - return "year=" + getId(); - } + public Year(Map record) { + setId(record.get("year")); + } + + public static final Creator CREATOR = new Creator() { + public Year[] newArray(int size) { + return new Year[size]; + } + + public Year createFromParcel(Parcel source) { + return new Year(source); + } + }; + + private Year(Parcel source) { + setId(source.readString()); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + } + + @Override + public String getName() { + return getId(); + } + + @Override + public String toString() { + return "year=" + getId(); + } } diff --git a/src/uk/org/ngo/squeezer/service/SqueezerBaseListHandler.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/BaseListHandler.java similarity index 58% rename from src/uk/org/ngo/squeezer/service/SqueezerBaseListHandler.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/service/BaseListHandler.java index 6e252c51d..7d2dffe2f 100644 --- a/src/uk/org/ngo/squeezer/service/SqueezerBaseListHandler.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/BaseListHandler.java @@ -1,36 +1,44 @@ package uk.org.ngo.squeezer.service; +import android.support.v4.app.Fragment.InstantiationException; + import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import java.util.Map; -import uk.org.ngo.squeezer.ReflectUtil; -import uk.org.ngo.squeezer.framework.SqueezerItem; -import android.support.v4.app.Fragment.InstantiationException; +import uk.org.ngo.squeezer.util.Reflection; +import uk.org.ngo.squeezer.framework.Item; + +abstract class BaseListHandler implements ListHandler { -abstract class SqueezerBaseListHandler implements SqueezerListHandler { protected List items; + @SuppressWarnings("unchecked") - private Class dataType = (Class) ReflectUtil.getGenericClass(this.getClass(), SqueezerListHandler.class, 0); - private Constructor constructor; + private Class dataType = (Class) Reflection + .getGenericClass(this.getClass(), ListHandler.class, 0); + private Constructor constructor; public Class getDataType() { return dataType; } public void clear() { - items = new ArrayList(){private static final long serialVersionUID = 1321113152942485275L;}; + items = new ArrayList() { + private static final long serialVersionUID = 1321113152942485275L; + }; } public void add(Map record) { - if (constructor == null) + if (constructor == null) { try { constructor = dataType.getDeclaredConstructor(Map.class); } catch (Exception e) { - throw new InstantiationException("Unable to create constructor for " + dataType.getName(), e); + throw new InstantiationException( + "Unable to create constructor for " + dataType.getName(), e); } + } try { items.add(constructor.newInstance(record)); } catch (Exception e) { diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/service/CliClient.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/CliClient.java new file mode 100644 index 000000000..74a08f31c --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/CliClient.java @@ -0,0 +1,806 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.service; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.RemoteException; +import android.util.Log; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.framework.Item; +import uk.org.ngo.squeezer.itemlist.IServiceAlbumListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceArtistListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceGenreListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceMusicFolderListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlayerListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlaylistsCallback; +import uk.org.ngo.squeezer.itemlist.IServicePluginItemListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePluginListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceSongListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceYearListCallback; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.MusicFolderItem; +import uk.org.ngo.squeezer.model.Player; +import uk.org.ngo.squeezer.model.Playlist; +import uk.org.ngo.squeezer.model.Plugin; +import uk.org.ngo.squeezer.model.PluginItem; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.model.Year; + +class CliClient { + + private static final String TAG = "CliClient"; + + class ExtendedQueryFormatCmd { + + final boolean playerSpecific; + + final boolean prefixed; + + final String cmd; + + final private Set taggedParameters; + + final private SqueezeParserInfo[] parserInfos; + + private ExtendedQueryFormatCmd(boolean playerSpecific, boolean prefixed, String cmd, + Set taggedParameters, SqueezeParserInfo... parserInfos) { + this.playerSpecific = playerSpecific; + this.prefixed = prefixed; + this.cmd = cmd; + this.taggedParameters = taggedParameters; + this.parserInfos = parserInfos; + } + + private ExtendedQueryFormatCmd(boolean playerSpecific, String cmd, + Set taggedParameters, SqueezeParserInfo... parserInfos) { + this(playerSpecific, false, cmd, taggedParameters, parserInfos); + } + + public ExtendedQueryFormatCmd(String cmd, Set taggedParameters, + String itemDelimiter, ListHandler handler) { + this(false, cmd, taggedParameters, new SqueezeParserInfo(itemDelimiter, handler)); + } + + public ExtendedQueryFormatCmd(String cmd, Set taggedParameters, + ListHandler handler) { + this(false, cmd, taggedParameters, new SqueezeParserInfo(handler)); + } + + } + + final ExtendedQueryFormatCmd[] extQueryFormatCmds = initializeExtQueryFormatCmds(); + + final Map extQueryFormatCmdMap + = initializeExtQueryFormatCmdMap(); + + private ExtendedQueryFormatCmd[] initializeExtQueryFormatCmds() { + List list = new ArrayList(); + + list.add( + new ExtendedQueryFormatCmd( + "players", + new HashSet(Arrays.asList("playerprefs", "charset")), + "playerid", + new BaseListHandler() { + Player defaultPlayer; + + String lastConnectedPlayer; + + @Override + public void clear() { + final SharedPreferences preferences = service + .getSharedPreferences(Preferences.NAME, + Context.MODE_PRIVATE); + lastConnectedPlayer = preferences + .getString(Preferences.KEY_LASTPLAYER, null); + defaultPlayer = null; + Log.v(TAG, "lastConnectedPlayer was: " + lastConnectedPlayer); + super.clear(); + } + + @Override + public void add(Map record) { + Player player = new Player(record); + // Discover the last connected player (if any, otherwise just pick the first one) + if (defaultPlayer == null || player.getId() + .equals(lastConnectedPlayer)) { + defaultPlayer = player; + } + items.add(player); + } + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServicePlayerListCallback callback = service.playerListCallback + .get(); + if (callback != null) { + // If the player list activity is active, pass the discovered players to it + try { + callback.onPlayersReceived(count, + start, items); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + return false; + } + } else if (start + items.size() >= count) { + // Otherwise set the last connected player as the active player + if (defaultPlayer != null) { + service.changeActivePlayer(defaultPlayer); + } + } + return true; + } + } + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "artists", + new HashSet( + Arrays.asList("search", "genre_id", "album_id", "tags", "charset")), + new ArtistListHandler() + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "albums", + new HashSet( + Arrays.asList("search", "genre_id", "artist_id", "track_id", "year", + "compilation", "sort", "tags", "charset")), + new AlbumListHandler() + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "years", + new HashSet(Arrays.asList("charset")), + "year", + new YearListHandler() + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "genres", + new HashSet( + Arrays.asList("search", "artist_id", "album_id", "track_id", "year", + "tags", "charset")), + new GenreListHandler() + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "musicfolder", + new HashSet(Arrays.asList("folder_id", "url", "tags", "charset")), + new MusicFolderListHandler() + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "songs", + new HashSet( + Arrays.asList("genre_id", "artist_id", "album_id", "year", "search", + "tags", "sort", "charset")), + new SongListHandler() + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "playlists", + new HashSet(Arrays.asList("search", "tags", "charset")), + new PlaylistsHandler()) + ); + list.add( + new ExtendedQueryFormatCmd( + "playlists tracks", + new HashSet(Arrays.asList("playlist_id", "tags", "charset")), + "playlist index", + new SongListHandler()) + ); + list.add( + new ExtendedQueryFormatCmd( + false, + "search", + new HashSet(Arrays.asList("term", "charset")), + new SqueezeParserInfo("genres_count", "genre_id", new GenreListHandler()), + new SqueezeParserInfo("albums_count", "album_id", new AlbumListHandler()), + new SqueezeParserInfo("contributors_count", "contributor_id", + new ArtistListHandler()), + new SqueezeParserInfo("tracks_count", "track_id", new SongListHandler()) + ) + ); + list.add( + new ExtendedQueryFormatCmd( + true, + "status", + new HashSet(Arrays.asList("tags", "charset", "subscribe")), + new SqueezeParserInfo("playlist_tracks", "playlist index", + new SongListHandler()) + ) + ); + list.add( + new ExtendedQueryFormatCmd( + "radios", + new HashSet(Arrays.asList("sort", "charset")), + "icon", + new PluginListHandler()) + ); + list.add( + new ExtendedQueryFormatCmd( + "apps", + new HashSet(Arrays.asList("sort", "charset")), + "icon", + new PluginListHandler()) + ); + list.add( + new ExtendedQueryFormatCmd( + true, true, + "items", + new HashSet( + Arrays.asList("item_id", "search", "want_url", "charset")), + new SqueezeParserInfo(new PluginItemListHandler())) + ); + + return list.toArray(new ExtendedQueryFormatCmd[]{}); + } + + private Map initializeExtQueryFormatCmdMap() { + Map map = new HashMap(); + for (ExtendedQueryFormatCmd cmd : extQueryFormatCmds) { + map.put(cmd.cmd, cmd); + } + return map; + } + + private final SqueezeService service; + + private int pageSize; + + CliClient(SqueezeService service) { + this.service = service; + } + + void initialize() { + pageSize = service.getResources().getInteger(R.integer.PageSize); + } + + + // All requests are tagged with a correlation id, which can be used when + // asynchronous responses are received. + private int _correlationid = 0; + + + /** + * Send the supplied commands to the SqueezeboxServer. + *

+ * All data to the server goes through this method + *

+ * Note don't call this from the main (UI) thread. If you are unsure if you are on the + * main thread, then use {@link #sendCommand(String...)} instead. + * + * @param commands List of commands to send + */ + synchronized void sendCommandImmediately(String... commands) { + if (commands.length == 0) { + return; + } + PrintWriter writer = service.connectionState.getSocketWriter(); + if (writer == null) { + return; + } + if (commands.length == 1) { + Log.v(TAG, "SENDING: " + commands[0]); + writer.println(commands[0]); + } else { + // Get it into one packet by deferring flushing... + for (String command : commands) { + Log.v(TAG, "Send: " + command); + writer.print(command + "\n"); + } + writer.flush(); + } + } + + /** + * Send the supplied commands to the SqueezeboxServer. + *

+ * This method takes care to avoid performing network operations on the main thread. Use {@link + * #sendCommandImmediately(String...)} if you are sure you are not on the main thread (eg if + * called from the listening thread). + * + * @param commands List of commands to send + */ + void sendCommand(final String... commands) { + if (service.mainThread != Thread.currentThread()) { + sendCommandImmediately(commands); + } else { + service.executor.execute(new Runnable() { + @Override + public void run() { + sendCommandImmediately(commands); + } + }); + } + } + + /** + * Send the specified command for the active player to the SqueezeboxServer + * + * @param command The command to send + */ + void sendPlayerCommand(final String command) { + if (service.connectionState.getActivePlayer() == null) { + return; + } + sendCommand(Util.encode(service.connectionState.getActivePlayer().getId()) + " " + command); + } + + /** + * Check whether this response is obsolete, i.e. if the correlation id for the current response + * is lower than the last registered minimum. + * + * @param registeredCorrelationId Minimum correlation id + * @param currentCorrelationId The correlation id of the current response + * + * @return True if the response is valid + */ + boolean checkCorrelation(Integer registeredCorrelationId, int currentCorrelationId) { + if (registeredCorrelationId == null) { + return true; + } + return (currentCorrelationId >= registeredCorrelationId); + } + + + // We register when asynchronous fetches are initiated, and when callbacks are unregistered + // because these events will make responses from pending requests obsolete + private final Map cmdCorrelationIds = new HashMap(); + + private final Map, Integer> typeCorrelationIds = new HashMap, Integer>(); + + /** + * Call this when a callback for received SqueezeboxServer items are unregistered.

Responses + * which come in after this time, i.e. which have a correlation id lower than the current, are + * ignored. + * + * @param clazz Type of the callback which are unregistered + */ + void cancelRequests(Class clazz) { + typeCorrelationIds.put(clazz, _correlationid); + } + + /** + * Check whether this response is obsolete. + * + * @param cmd Type of the request + * @param correlationid The correlation id of the current response + * + * @return True if the response is valid + */ + private boolean checkCorrelation(String cmd, int correlationid) { + return checkCorrelation(cmdCorrelationIds.get(cmd), correlationid); + } + + /** + * Check whether this response is obsolete. + * + * @param type Type of the callback to receive the items + * @param correlationid The correlation id of the current response + * + * @return True if the response is valid + */ + private boolean checkCorrelation(Class type, int correlationid) { + return checkCorrelation(typeCorrelationIds.get(type), correlationid); + } + + /** + * Send an asynchronous request to the SqueezeboxServer for the specified items. + *

+ * If start is zero, it means the the list is being reordered, which will make any pending + * replies, for the given items, obsolete. + *

+ * This will always order one item, to learn the number of items from the server. Remaining + * items will then be automatically ordered when the response arrives. See {@link + * #parseSqueezerList(boolean, String, String, List, ListHandler)} for details. + * + * @param playerid Id of the current player or null + * @param cmd Identifies the + * @param start + * @param parameters + */ + private void requestItems(String playerid, String cmd, int start, List parameters) { + if (start == 0) { + cmdCorrelationIds.put(cmd, _correlationid); + } + final StringBuilder sb = new StringBuilder( + cmd + " " + start + " " + (start == 0 ? 1 : pageSize)); + if (playerid != null) { + sb.insert(0, Util.encode(playerid) + " "); + } + if (parameters != null) { + for (String parameter : parameters) { + sb.append(" " + Util.encode(parameter)); + } + } + sendCommand(sb.toString() + " correlationid:" + _correlationid++); + } + + void requestItems(String cmd, int start, List parameters) { + requestItems(null, cmd, start, parameters); + } + + void requestItems(String cmd, int start) { + requestItems(cmd, start, null); + } + + void requestPlayerItems(String cmd, int start, List parameters) { + if (service.connectionState.getActivePlayer() == null) { + return; + } + requestItems(service.connectionState.getActivePlayer().getId(), cmd, start, parameters); + } + + void requestPlayerItems(String cmd, int start) { + requestPlayerItems(cmd, start, null); + } + + /** + * Data for {@link CliClient#parseSqueezerList(boolean, List, SqueezeParserInfo...)} + * + * @author kaa + */ + private static class SqueezeParserInfo { + + private final String item_delimiter; + + private final String count_id; + + private final ListHandler handler; + + /** + * @param countId The label for the tag which contains the total number of results, normally + * "count". + * @param itemDelimiter As defined for each extended query format command in the + * squeezeserver CLI documentation. + * @param handler Callback to receive the parsed data. + */ + public SqueezeParserInfo(String countId, String itemDelimiter, + ListHandler handler) { + count_id = countId; + item_delimiter = itemDelimiter; + this.handler = handler; + } + + public SqueezeParserInfo(String itemDelimiter, + ListHandler handler) { + this("count", itemDelimiter, handler); + } + + public SqueezeParserInfo(ListHandler handler) { + this("id", handler); + } + } + + /** + * Generic method to parse replies for queries in extended query format + *

+ * This is the control center for asynchronous and paging receiving of data from SqueezeServer. + *

+ * Transfer of each data type are started by an asynchronous request by one of the public method + * in this module. This method will forward the data using the supplied {@link ListHandler}, and + * and order the next page if necessary, repeating the current query parameters. + *

+ * Activities should just initiate the request, and supply a callback to receive a page of + * data. + * + * @param playercmd Set this for replies for the current player, to skip the playerid + * @param tokens List of tokens with value or key:value. + * @param parserInfos Data for each list you expect for the current repsonse + */ + void parseSqueezerList(ExtendedQueryFormatCmd cmd, List tokens) { + Log.v(TAG, "Parsing list: " + tokens); + + int ofs = cmd.cmd.split(" ").length + (cmd.playerSpecific ? 1 : 0) + (cmd.prefixed ? 1 : 0); + int actionsCount = 0; + String playerid = (cmd.playerSpecific ? tokens.get(0) + " " : ""); + String prefix = (cmd.prefixed ? tokens.get(cmd.playerSpecific ? 1 : 0) + " " : ""); + int start = Util.parseDecimalIntOrZero(tokens.get(ofs)); + int itemsPerResponse = Util.parseDecimalIntOrZero(tokens.get(ofs + 1)); + + int correlationid = 0; + boolean rescan = false; + Map taggedParameters = new HashMap(); + Map parameters = new HashMap(); + Set countIdSet = new HashSet(); + Map itemDelimeterMap = new HashMap(); + Map counts = new HashMap(); + Map record = null; + + for (SqueezeParserInfo parserInfo : cmd.parserInfos) { + parserInfo.handler.clear(); + countIdSet.add(parserInfo.count_id); + itemDelimeterMap.put(parserInfo.item_delimiter, parserInfo); + } + + SqueezeParserInfo parserInfo = null; + for (int idx = ofs + 2; idx < tokens.size(); idx++) { + String token = tokens.get(idx); + int colonPos = token.indexOf("%3A"); + if (colonPos == -1) { + Log.e(TAG, "Expected colon in list token. '" + token + "'"); + return; + } + String key = Util.decode(token.substring(0, colonPos)); + String value = Util.decode(token.substring(colonPos + 3)); + if (service.debugLogging) { + Log.v(TAG, "key=" + key + ", value: " + value); + } + + if (key.equals("rescan")) { + rescan = (Util.parseDecimalIntOrZero(value) == 1); + } else if (key.equals("correlationid")) { + correlationid = Util.parseDecimalIntOrZero(value); + taggedParameters.put(key, token); + } else if (key.equals("actions")) { + // Apparently squeezer returns some commands which are + // included in the count of the current request + actionsCount++; + } + if (countIdSet.contains(key)) { + counts.put(key, Util.parseDecimalIntOrZero(value)); + } else { + if (itemDelimeterMap.get(key) != null) { + if (record != null) { + parserInfo.handler.add(record); + if (service.debugLogging) { + Log.v(TAG, "record=" + record); + } + } + parserInfo = itemDelimeterMap.get(key); + record = new HashMap(); + } + if (record != null) { + record.put(key, value); + } else if (cmd.taggedParameters.contains(key)) { + taggedParameters.put(key, token); + } else { + parameters.put(key, value); + } + } + } + + if (record != null) { + parserInfo.handler.add(record); + if (service.debugLogging) { + Log.v(TAG, "record=" + record); + } + } + + processLists: + if (checkCorrelation(cmd.cmd, correlationid)) { + int end = start + itemsPerResponse; + int max = 0; + for (SqueezeParserInfo parser : cmd.parserInfos) { + if (checkCorrelation(parser.handler.getDataType(), correlationid)) { + Integer count = counts.get(parser.count_id); + int countValue = (count == null ? 0 : count); + if (count != null || start == 0) { + if (!parser.handler + .processList(rescan, countValue - actionsCount, start, + parameters)) { + break processLists; + } + if (countValue > max) { + max = countValue; + } + } + } + } + if (end % pageSize != 0 && end < max) { + int count = (end + pageSize > max ? max - end : pageSize - itemsPerResponse); + StringBuilder cmdline = new StringBuilder(cmd.cmd + " " + end + " " + count); + for (String parameter : taggedParameters.values()) { + cmdline.append(" " + parameter); + } + sendCommandImmediately(playerid + prefix + cmdline.toString()); + } + } + } + + private class YearListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServiceYearListCallback callback = service.yearListCallback.get(); + if (callback != null) { + try { + callback.onYearsReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class GenreListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServiceGenreListCallback callback = service.genreListCallback.get(); + if (callback != null) { + try { + callback.onGenresReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class ArtistListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServiceArtistListCallback callback = service.artistListCallback.get(); + if (callback != null) { + try { + callback.onArtistsReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class AlbumListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServiceAlbumListCallback callback = service.albumListCallback.get(); + if (callback != null) { + try { + callback.onAlbumsReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class MusicFolderListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServiceMusicFolderListCallback callback = service.musicFolderListCallback.get(); + if (callback != null) { + try { + callback.onMusicFoldersReceived(count, start, + items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + + return false; + } + } + + private class SongListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServiceSongListCallback callback = service.songListCallback.get(); + if (callback != null) { + try { + // TODO use parameters to update the current player state + // service.playerState.update(parameters); + callback.onSongsReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class PlaylistsHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServicePlaylistsCallback callback = service.playlistsCallback.get(); + if (callback != null) { + try { + callback.onPlaylistsReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class PluginListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServicePluginListCallback callback = service.pluginListCallback.get(); + if (callback != null) { + try { + callback.onPluginsReceived(count, start, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + + private class PluginItemListHandler extends BaseListHandler { + + @Override + public boolean processList(boolean rescan, int count, int start, + Map parameters) { + IServicePluginItemListCallback callback = service.pluginItemListCallback.get(); + if (callback != null) { + try { + callback.onPluginItemsReceived(count, start, + parameters, items); + return true; + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } + return false; + } + } + +} diff --git a/src/uk/org/ngo/squeezer/service/SqueezerConnectionState.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ConnectionState.java similarity index 66% rename from src/uk/org/ngo/squeezer/service/SqueezerConnectionState.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/service/ConnectionState.java index 0b8e49558..be6d63b4b 100644 --- a/src/uk/org/ngo/squeezer/service/SqueezerConnectionState.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ConnectionState.java @@ -16,11 +16,17 @@ package uk.org.ngo.squeezer.service; +import android.net.wifi.WifiManager; +import android.os.RemoteException; +import android.util.Log; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; +import java.net.Authenticator; import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -28,16 +34,13 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import uk.org.ngo.squeezer.IServiceCallback; import uk.org.ngo.squeezer.R; import uk.org.ngo.squeezer.Squeezer; -import uk.org.ngo.squeezer.model.SqueezerPlayer; -import android.net.wifi.WifiManager; -import android.os.RemoteException; -import android.util.Log; +import uk.org.ngo.squeezer.model.Player; + +class ConnectionState { -class SqueezerConnectionState { - private static final String TAG = "SqueezeConnectionState"; + private static final String TAG = "ConnectionState"; // Incremented once per new connection and given to the Thread // that's listening on the socket. So if it dies and it's not the @@ -46,25 +49,36 @@ class SqueezerConnectionState { private final AtomicInteger currentConnectionGeneration = new AtomicInteger(0); // Connection state: - private final AtomicReference callback = new AtomicReference(); - private final AtomicBoolean isConnected = new AtomicBoolean(false); + private final AtomicBoolean isConnectInProgress = new AtomicBoolean(false); + + private final AtomicBoolean isConnected = new AtomicBoolean(false); + private final AtomicBoolean mCanMusicfolder = new AtomicBoolean(false); - private final AtomicBoolean canRandomplay = new AtomicBoolean(false); - private final AtomicReference socketRef = new AtomicReference(); - private final AtomicReference socketWriter = new AtomicReference(); - private final AtomicReference activePlayer = new AtomicReference(); - private final AtomicReference defaultPlayer = new AtomicReference(); + + private final AtomicBoolean canRandomplay = new AtomicBoolean(false); + + private final AtomicReference preferredAlbumSort = new AtomicReference("album"); + + private final AtomicReference socketRef = new AtomicReference(); + + private final AtomicReference socketWriter = new AtomicReference(); + + private final AtomicReference activePlayer = new AtomicReference(); + + private final AtomicReference defaultPlayer = new AtomicReference(); // Where we connected (or are connecting) to: private final AtomicReference currentHost = new AtomicReference(); + private final AtomicReference httpPort = new AtomicReference(); + private final AtomicReference cliPort = new AtomicReference(); private WifiManager.WifiLock wifiLock; - void setWifiLock(WifiManager.WifiLock wifiLock) { - this.wifiLock = wifiLock; - } + void setWifiLock(WifiManager.WifiLock wifiLock) { + this.wifiLock = wifiLock; + } void updateWifiLock(boolean state) { // TODO: this might be running in the wrong thread. Is wifiLock thread-safe? @@ -99,115 +113,123 @@ void updateWifiLock(boolean state) { } } - void disconnect() { + void disconnect(SqueezeService service, boolean loginFailed) { + Log.v(TAG, "disconnect" + (loginFailed ? ": authentication failure" : "")); currentConnectionGeneration.incrementAndGet(); Socket socket = socketRef.get(); if (socket != null) { try { socket.close(); - } catch (IOException e) {} + } catch (IOException e) { + } } socketRef.set(null); socketWriter.set(null); isConnected.set(false); - setConnectionState(false, false); + setConnectionState(service, false, false, loginFailed); httpPort.set(null); activePlayer.set(null); } - private void setConnectionState(boolean currentState, boolean postConnect) { + private void setConnectionState(SqueezeService service, boolean currentState, + boolean postConnect, boolean loginFailed) { isConnected.set(currentState); - if (callback.get() == null) { - return; + if (postConnect) { + isConnectInProgress.set(false); } - try { - Log.d(TAG, "pre-call setting callback connection state to: " + currentState); - callback.get().onConnectionChanged(currentState, postConnect); - Log.d(TAG, "post-call setting callback connection state."); - } catch (RemoteException e) { + + int i = service.mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + Log.d(TAG, "pre-call setting callback connection state to: " + currentState); + service.mServiceCallbacks.getBroadcastItem(i) + .onConnectionChanged(currentState, postConnect, loginFailed); + Log.d(TAG, "post-call setting callback connection state."); + + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } } + service.mServiceCallbacks.finishBroadcast(); } - IServiceCallback getCallback() { - return callback.get(); + Player getActivePlayer() { + return activePlayer.get(); } - void setCallback(IServiceCallback callback) { - this.callback.set(callback); + void setActivePlayer(Player player) { + activePlayer.set(player); } - void callbackCompareAndSet(IServiceCallback expect, IServiceCallback update) { - callback.compareAndSet(expect, update); + Player getDefaultPlayer() { + return defaultPlayer.get(); } - SqueezerPlayer getActivePlayer() { - return activePlayer.get(); + void setDefaultPlayer(Player player) { + defaultPlayer.set(player); } - void setActivePlayer(SqueezerPlayer player) { - activePlayer.set(player); + PrintWriter getSocketWriter() { + return socketWriter.get(); } - SqueezerPlayer getDefaultPlayer() { - return defaultPlayer.get(); + void setHttpPort(Integer port) { + httpPort.set(port); + Log.v(TAG, "HTTP port is now: " + port); } - void setDefaultPlayer(SqueezerPlayer player) { - defaultPlayer.set(player); + void setCanMusicfolder(boolean value) { + mCanMusicfolder.set(value); } - PrintWriter getSocketWriter() { - return socketWriter.get(); + boolean canMusicfolder() { + return mCanMusicfolder.get(); } - void setCurrentHost(String host) { - currentHost.set(host); - Log.v(TAG, "HTTP port is now: " + httpPort); + void setCanRandomplay(boolean value) { + canRandomplay.set(value); } - void setCliPort(Integer port) { - cliPort.set(port); - Log.v(TAG, "HTTP port is now: " + httpPort); + boolean canRandomplay() { + return canRandomplay.get(); } - void setHttpPort(Integer port) { - httpPort.set(port); - Log.v(TAG, "HTTP port is now: " + httpPort); + public void setPreferedAlbumSort(String value) { + preferredAlbumSort.set(value); } - void setCanMusicfolder(boolean value) { - mCanMusicfolder.set(value); + public String getPreferredAlbumSort() { + return preferredAlbumSort.get(); } - boolean canMusicfolder() { - return mCanMusicfolder.get(); + boolean isConnected() { + return isConnected.get(); } - void setCanRandomplay(boolean value) { - canRandomplay.set(value); + boolean isConnectInProgress() { + return isConnectInProgress.get(); } - boolean canRandomplay() { - return canRandomplay.get(); - } - - boolean isConnected() { - return isConnected.get(); - } - - void startListeningThread(SqueezeService service) { - Thread listeningThread = new ListeningThread(service, socketRef.get(), currentConnectionGeneration.incrementAndGet()); + void startListeningThread(SqueezeService service) { + Thread listeningThread = new ListeningThread(service, socketRef.get(), + currentConnectionGeneration.incrementAndGet()); listeningThread.start(); - } + } private class ListeningThread extends Thread { - private final SqueezeService service; + + private final SqueezeService service; + private final Socket socket; + private final int generationNumber; + private ListeningThread(SqueezeService service, Socket socket, int generationNumber) { - this.service = service; + this.service = service; this.socket = socket; this.generationNumber = generationNumber; } @@ -237,7 +259,7 @@ public void run() { // else we should notify about it. if (currentConnectionGeneration.get() == generationNumber) { Log.v(TAG, "Server disconnected; exception=" + exception); - service.disconnect(); + service.disconnect(exception == null); } else { // Who cares. Log.v(TAG, "Old generation connection disconnected, as expected."); @@ -249,7 +271,9 @@ public void run() { } } - void startConnect(final SqueezeService service, ScheduledThreadPoolExecutor executor, String hostPort) throws RemoteException { + void startConnect(final SqueezeService service, ScheduledThreadPoolExecutor executor, + String hostPort, final String userName, final String password) throws RemoteException { + Log.v(TAG, "startConnect"); // Common mistakes, based on crash reports... if (hostPort.startsWith("Http://") || hostPort.startsWith("http://")) { hostPort = hostPort.substring(7); @@ -270,28 +294,35 @@ void startConnect(final SqueezeService service, ScheduledThreadPoolExecutor exec // Start the off-thread connect. executor.execute(new Runnable() { + @Override public void run() { service.disconnect(); Socket socket = new Socket(); try { Log.d(TAG, "Connecting to: " + cleanHostPort); + isConnectInProgress.set(true); socket.connect(new InetSocketAddress(host, port), - 4000 /* ms timeout */); + 4000 /* ms timeout */); socketRef.set(socket); Log.d(TAG, "Connected to: " + cleanHostPort); socketWriter.set(new PrintWriter(socket.getOutputStream(), true)); - Log.d(TAG, "writer == " + socketWriter.get()); - setConnectionState(true, true); + setConnectionState(service, true, true, false); Log.d(TAG, "connection state broadcasted true."); - startListeningThread(service); + startListeningThread(service); setDefaultPlayer(null); - service.onCliPortConnectionEstablished(); + service.onCliPortConnectionEstablished(userName, password); + Authenticator.setDefault(new Authenticator() { + @Override + public PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(userName, password.toCharArray()); + } + }); } catch (SocketTimeoutException e) { Log.e(TAG, "Socket timeout connecting to: " + cleanHostPort); - setConnectionState(false, true); + setConnectionState(service, false, true, false); } catch (IOException e) { Log.e(TAG, "IOException connecting to: " + cleanHostPort); - setConnectionState(false, true); + setConnectionState(service, false, true, false); } } @@ -325,12 +356,12 @@ private static int parsePort(String hostPort) { } } - Integer getHttpPort() { - return httpPort.get(); - } + Integer getHttpPort() { + return httpPort.get(); + } - String getCurrentHost() { - return currentHost.get(); - } + String getCurrentHost() { + return currentHost.get(); + } } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ListHandler.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ListHandler.java new file mode 100644 index 000000000..aecd20f68 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ListHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.service; + +import java.util.List; +import java.util.Map; + +import uk.org.ngo.squeezer.framework.Item; + +/** + * Implement this and give it to {@link CliClient#parseSqueezerList(List, ListHandler)} for each + * extended query format command you wish to support.

+ * + * @author Kurt Aaholst + */ +interface ListHandler { + + /** + * @return The type of item this handler can handle + */ + Class getDataType(); + + /** + * Prepare for parsing an extended query format response + */ + void clear(); + + /** + * Called for each item received in the current reply. Just store this internally. + * + * @param record + */ + void add(Map record); + + /** + * Called when the current reply is completely parsed. Pass the information on to your activity + * now. If there are any more data, it is automatically ordered by {@link + * CliClient#parseSqueezerList(List, ListHandler)} + * + * @param rescan Set if SqueezeServer is currently doing a scan of the music library. + * @param count Total number of result for the current query. + * @param max The current configured default maximum list size. + * @param start Offset for the current list in total results. + * + * @return + */ + boolean processList(boolean rescan, int count, int start, Map parameters); +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ServerString.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ServerString.java new file mode 100644 index 000000000..977fe2776 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/ServerString.java @@ -0,0 +1,37 @@ +package uk.org.ngo.squeezer.service; + +public enum ServerString { + ALBUM, + BROWSE_NEW_MUSIC, + SORT_ARTISTYEARALBUM, + SORT_ARTISTALBUM, + SORT_YEARALBUM, + SORT_YEARARTISTALBUM, + REPEAT_OFF, + REPEAT_ONE, + REPEAT_ALL, + SHUFFLE_OFF, + SHUFFLE_ON_SONGS, + SHUFFLE_ON_ALBUMS, + SWITCH_TO_EXTENDED_LIST, + SWITCH_TO_GALLERY, + ALBUM_DISPLAY_OPTIONS; + + private String localizedString; + + /** + * @return The localized string or just the name of the token, if not yet fetched (unlikely). + */ + public String getLocalizedString() { + return localizedString != null ? localizedString : name(); + } + + /** + * Set the localized string for this token, as fetched from the server. + * + * @param localizedString + */ + public void setLocalizedString(String localizedString) { + this.localizedString = localizedString; + } +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/service/SqueezeService.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/SqueezeService.java new file mode 100644 index 000000000..bd84ae792 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/service/SqueezeService.java @@ -0,0 +1,1828 @@ +/* + * Copyright (c) 2009 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.service; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.wifi.WifiManager; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicReference; + +import uk.org.ngo.squeezer.IServiceCallback; +import uk.org.ngo.squeezer.IServiceHandshakeCallback; +import uk.org.ngo.squeezer.IServiceMusicChangedCallback; +import uk.org.ngo.squeezer.IServiceVolumeCallback; +import uk.org.ngo.squeezer.NowPlayingActivity; +import uk.org.ngo.squeezer.Preferences; +import uk.org.ngo.squeezer.R; +import uk.org.ngo.squeezer.Util; +import uk.org.ngo.squeezer.itemlist.IServiceAlbumListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceArtistListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceCurrentPlaylistCallback; +import uk.org.ngo.squeezer.itemlist.IServiceGenreListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceMusicFolderListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlayerListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlaylistMaintenanceCallback; +import uk.org.ngo.squeezer.itemlist.IServicePlaylistsCallback; +import uk.org.ngo.squeezer.itemlist.IServicePluginItemListCallback; +import uk.org.ngo.squeezer.itemlist.IServicePluginListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceSongListCallback; +import uk.org.ngo.squeezer.itemlist.IServiceYearListCallback; +import uk.org.ngo.squeezer.itemlist.dialog.AlbumViewDialog; +import uk.org.ngo.squeezer.itemlist.dialog.SongOrderDialog; +import uk.org.ngo.squeezer.model.Album; +import uk.org.ngo.squeezer.model.Artist; +import uk.org.ngo.squeezer.model.Genre; +import uk.org.ngo.squeezer.model.MusicFolderItem; +import uk.org.ngo.squeezer.model.Player; +import uk.org.ngo.squeezer.model.PlayerState; +import uk.org.ngo.squeezer.model.PlayerState.PlayStatus; +import uk.org.ngo.squeezer.model.PlayerState.RepeatStatus; +import uk.org.ngo.squeezer.model.PlayerState.ShuffleStatus; +import uk.org.ngo.squeezer.model.Playlist; +import uk.org.ngo.squeezer.model.Plugin; +import uk.org.ngo.squeezer.model.PluginItem; +import uk.org.ngo.squeezer.model.Song; +import uk.org.ngo.squeezer.model.Year; +import uk.org.ngo.squeezer.util.Scrobble; + + +public class SqueezeService extends Service { + + private static final String TAG = "SqueezeService"; + + private static final int PLAYBACKSERVICE_STATUS = 1; + + private static final String ALBUMTAGS = "alyj"; + + /** + * Information that will be requested about songs. + *

+ * a: artist name
+ * C: compilation (1 if true, missing otherwise)
+ * d: duration, in seconds
+ * e: album ID
+ * j: coverart (1 if available, missing otherwise)
+ * J: artwork_track_id (if available, missing otherwise)
+ * K: URL to remote artwork
+ * l: album name
+ * s: artist id
+ * t: tracknum, if known
+ * x: 1, if this is a remote track
+ * y: song year + */ + // This should probably be a field in Song. + private static final String SONGTAGS = "aCdejJKlstxy"; + + final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); + + Thread mainThread; + + private boolean mHandshakeComplete = false; + + final RemoteCallbackList mServiceCallbacks + = new RemoteCallbackList(); + + final RemoteCallbackList mCurrentPlaylistCallbacks + = new RemoteCallbackList(); + + final RemoteCallbackList mMusicChangedCallbacks + = new RemoteCallbackList(); + + final RemoteCallbackList mHandshakeCallbacks + = new RemoteCallbackList(); + + final AtomicReference playerListCallback + = new AtomicReference(); + + final AtomicReference albumListCallback + = new AtomicReference(); + + final AtomicReference artistListCallback + = new AtomicReference(); + + final AtomicReference yearListCallback + = new AtomicReference(); + + final AtomicReference genreListCallback + = new AtomicReference(); + + final AtomicReference songListCallback + = new AtomicReference(); + + final AtomicReference playlistsCallback + = new AtomicReference(); + + final AtomicReference playlistMaintenanceCallback + = new AtomicReference(); + + final AtomicReference pluginListCallback + = new AtomicReference(); + + final AtomicReference pluginItemListCallback + = new AtomicReference(); + + final AtomicReference musicFolderListCallback + = new AtomicReference(); + + final RemoteCallbackList mVolumeCallbacks + = new RemoteCallbackList(); + + ConnectionState connectionState = new ConnectionState(); + + PlayerState playerState = new PlayerState(); + + CliClient cli = new CliClient(this); + + /** + * Is scrobbling enabled? + */ + private boolean scrobblingEnabled; + + /** + * Was scrobbling enabled? + */ + private boolean scrobblingPreviouslyEnabled; + + boolean debugLogging; // Enable this if you are debugging something + + boolean mUpdateOngoingNotification; + + int mFadeInSecs; + + @Override + public void onCreate() { + super.onCreate(); + + // Get the main thread + mainThread = Thread.currentThread(); + + // Clear leftover notification in case this service previously got killed while playing + NotificationManager nm = (NotificationManager) getSystemService( + Context.NOTIFICATION_SERVICE); + nm.cancel(PLAYBACKSERVICE_STATUS); + connectionState + .setWifiLock(((WifiManager) getSystemService(Context.WIFI_SERVICE)).createWifiLock( + WifiManager.WIFI_MODE_FULL, "Squeezer_WifiLock")); + + getPreferences(); + + cli.initialize(); + } + + private void getPreferences() { + final SharedPreferences preferences = getSharedPreferences(Preferences.NAME, MODE_PRIVATE); + scrobblingEnabled = preferences.getBoolean(Preferences.KEY_SCROBBLE_ENABLED, false); + mFadeInSecs = preferences.getInt(Preferences.KEY_FADE_IN_SECS, 0); + mUpdateOngoingNotification = preferences + .getBoolean(Preferences.KEY_NOTIFY_OF_CONNECTION, false); + } + + @Override + public IBinder onBind(Intent intent) { + return squeezeService; + } + + @Override + public void onDestroy() { + super.onDestroy(); + disconnect(); + mServiceCallbacks.kill(); + } + + void disconnect() { + disconnect(false); + } + + void disconnect(boolean isServerDisconnect) { + connectionState.disconnect(this, isServerDisconnect && mHandshakeComplete == false); + mHandshakeComplete = false; + clearOngoingNotification(); + playerState = new PlayerState(); + } + + + private interface CmdHandler { + + public void handle(List tokens); + } + + private Map initializeGlobalHandlers() { + Map handlers = new HashMap(); + + for (final CliClient.ExtendedQueryFormatCmd cmd : cli.extQueryFormatCmds) { + if (!(cmd.playerSpecific || cmd.prefixed)) { + handlers.put(cmd.cmd, new CmdHandler() { + @Override + public void handle(List tokens) { + cli.parseSqueezerList(cmd, tokens); + } + }); + } + } + handlers.put("playlists", new CmdHandler() { + @Override + public void handle(List tokens) { + if ("delete".equals(tokens.get(1))) { + ; + } else if ("edit".equals(tokens.get(1))) { + ; + } else if ("new".equals(tokens.get(1))) { + HashMap tokenMap = parseTokens(tokens); + if (tokenMap.get("overwritten_playlist_id") != null) { + IServicePlaylistMaintenanceCallback callback = playlistMaintenanceCallback + .get(); + if (callback != null) { + try { + callback.onCreateFailed(getString(R.string.PLAYLIST_EXISTS_MESSAGE, + tokenMap.get("name"))); + } catch (RemoteException e) { + Log.e(TAG, getString(R.string.PLAYLIST_EXISTS_MESSAGE, + tokenMap.get("name"))); + } + } + } + } else if ("rename".equals(tokens.get(1))) { + HashMap tokenMap = parseTokens(tokens); + if (tokenMap.get("dry_run") != null) { + if (tokenMap.get("overwritten_playlist_id") != null) { + IServicePlaylistMaintenanceCallback callback + = playlistMaintenanceCallback.get(); + if (callback != null) { + try { + callback.onRenameFailed( + getString(R.string.PLAYLIST_EXISTS_MESSAGE, + tokenMap.get("newname"))); + } catch (RemoteException e) { + Log.e(TAG, getString(R.string.PLAYLIST_EXISTS_MESSAGE, + tokenMap.get("newname"))); + } + } + } else { + cli.sendCommandImmediately( + "playlists rename playlist_id:" + tokenMap.get("playlist_id") + + " newname:" + Util.encode(tokenMap.get("newname"))); + } + } + } else if ("tracks".equals(tokens.get(1))) { + cli.parseSqueezerList(cli.extQueryFormatCmdMap.get("playlists tracks"), tokens); + } else { + cli.parseSqueezerList(cli.extQueryFormatCmdMap.get("playlists"), tokens); + } + } + }); + handlers.put("login", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.i(TAG, "Authenticated: " + tokens); + onAuthenticated(); + } + }); + handlers.put("pref", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.i(TAG, "Preference received: " + tokens); + if ("httpport".equals(tokens.get(1)) && tokens.size() >= 3) { + connectionState.setHttpPort(Integer.parseInt(tokens.get(2))); + } + if ("jivealbumsort".equals(tokens.get(1)) && tokens.size() >= 3) { + connectionState.setPreferedAlbumSort(tokens.get(2)); + } + } + }); + handlers.put("can", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.i(TAG, "Capability received: " + tokens); + if ("musicfolder".equals(tokens.get(1)) && tokens.size() >= 3) { + connectionState + .setCanMusicfolder(Util.parseDecimalIntOrZero(tokens.get(2)) == 1); + } + + if ("randomplay".equals(tokens.get(1)) && tokens.size() >= 3) { + connectionState + .setCanRandomplay(Util.parseDecimalIntOrZero(tokens.get(2)) == 1); + } + } + }); + handlers.put("getstring", new CmdHandler() { + @Override + public void handle(List tokens) { + int maxOrdinal = 0; + Map tokenMap = parseTokens(tokens); + for (Entry entry : tokenMap.entrySet()) { + if (entry.getValue() != null) { + ServerString serverString = ServerString.valueOf(entry.getKey()); + serverString.setLocalizedString(entry.getValue()); + if (serverString.ordinal() > maxOrdinal) { + maxOrdinal = serverString.ordinal(); + } + } + } + + // Fetch the next strings until the list is completely translated + if (maxOrdinal < ServerString.values().length - 1) { + cli.sendCommandImmediately( + "getstring " + ServerString.values()[maxOrdinal + 1].name()); + } + } + }); + handlers.put("version", new CmdHandler() { + /** + * Seeing the version result indicates that the + * handshake has completed (see + * {@link onCliPortConnectionEstablished}), call any handshake + * callbacks that have been registered. + */ + @Override + public void handle(List tokens) { + Log.i(TAG, "Version received: " + tokens); + mHandshakeComplete = true; + strings(); + + int i = mHandshakeCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mHandshakeCallbacks.getBroadcastItem(i).onHandshakeCompleted(); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mHandshakeCallbacks.finishBroadcast(); + } + }); + + return handlers; + } + + private Map initializePrefixedHandlers() { + Map handlers = new HashMap(); + + for (final CliClient.ExtendedQueryFormatCmd cmd : cli.extQueryFormatCmds) { + if (cmd.prefixed && !cmd.playerSpecific) { + handlers.put(cmd.cmd, new CmdHandler() { + @Override + public void handle(List tokens) { + cli.parseSqueezerList(cmd, tokens); + } + }); + } + } + + return handlers; + } + + private Map initializePlayerSpecificHandlers() { + Map handlers = new HashMap(); + + for (final CliClient.ExtendedQueryFormatCmd cmd : cli.extQueryFormatCmds) { + if (cmd.playerSpecific && !cmd.prefixed) { + handlers.put(cmd.cmd, new CmdHandler() { + @Override + public void handle(List tokens) { + cli.parseSqueezerList(cmd, tokens); + } + }); + } + } + handlers.put("prefset", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.v(TAG, "Prefset received: " + tokens); + if (tokens.size() > 4 && tokens.get(2).equals("server") && tokens.get(3) + .equals("volume")) { + String newVolume = tokens.get(4); + updatePlayerVolume(Util.parseDecimalIntOrZero(newVolume)); + } + } + }); + handlers.put("play", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.v(TAG, "play registered"); + updatePlayStatus(PlayerState.PlayStatus.play); + } + }); + handlers.put("stop", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.v(TAG, "stop registered"); + updatePlayStatus(PlayerState.PlayStatus.stop); + } + }); + handlers.put("pause", new CmdHandler() { + @Override + public void handle(List tokens) { + Log.v(TAG, "pause registered: " + tokens); + parsePause(tokens.size() >= 3 ? tokens.get(2) : null); + } + }); + handlers.put("status", new CmdHandler() { + @Override + public void handle(List tokens) { + if (tokens.size() >= 3 && "-".equals(tokens.get(2))) { + parseStatusLine(tokens); + } else { + cli.parseSqueezerList(cli.extQueryFormatCmdMap.get("status"), tokens); + } + } + }); + handlers.put("playlist", new CmdHandler() { + @Override + public void handle(List tokens) { + parsePlaylistNotification(tokens); + } + }); + + return handlers; + } + + private Map initializePrefixedPlayerSpecificHandlers() { + Map handlers = new HashMap(); + + for (final CliClient.ExtendedQueryFormatCmd cmd : cli.extQueryFormatCmds) { + if (cmd.playerSpecific && cmd.prefixed) { + handlers.put(cmd.cmd, new CmdHandler() { + @Override + public void handle(List tokens) { + cli.parseSqueezerList(cmd, tokens); + } + }); + } + } + + return handlers; + } + + private final Map globalHandlers = initializeGlobalHandlers(); + + private final Map prefixedHandlers = initializePrefixedHandlers(); + + private final Map playerSpecificHandlers + = initializePlayerSpecificHandlers(); + + private final Map prefixedPlayerSpecificHandlers + = initializePrefixedPlayerSpecificHandlers(); + + void onLineReceived(String serverLine) { + if (debugLogging) { + Log.v(TAG, "LINE: " + serverLine); + } + List tokens = Arrays.asList(serverLine.split(" ")); + if (tokens.size() < 2) { + return; + } + + CmdHandler handler; + if ((handler = globalHandlers.get(tokens.get(0))) != null) { + handler.handle(tokens); + return; + } + if ((handler = prefixedHandlers.get(tokens.get(1))) != null) { + handler.handle(tokens); + return; + } + + // Player-specific commands follow. But we ignore all that aren't for our + // active player. + String activePlayerId = (connectionState.getActivePlayer() != null ? connectionState + .getActivePlayer().getId() : null); + if (activePlayerId == null || activePlayerId.length() == 0 || + !Util.decode(tokens.get(0)).equals(activePlayerId)) { + // Different player that we're not interested in. + // (yet? maybe later.) + return; + } + if ((handler = playerSpecificHandlers.get(tokens.get(1))) != null) { + handler.handle(tokens); + return; + } + if (tokens.size() > 2 + && (handler = prefixedPlayerSpecificHandlers.get(tokens.get(2))) != null) { + handler.handle(tokens); + } + } + + private void updatePlayerVolume(int newVolume) { + playerState.setCurrentVolume(newVolume); + Player player = connectionState.getActivePlayer(); + int i = mVolumeCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mVolumeCallbacks.getBroadcastItem(i).onVolumeChanged(newVolume, player); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mVolumeCallbacks.finishBroadcast(); + } + + private void updateTimes(int secondsIn, int secondsTotal) { + playerState.setCurrentSongDuration(secondsTotal); + if (playerState.getCurrentTimeSecond() != secondsIn) { + playerState.setCurrentTimeSecond(secondsIn); + int i = mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mServiceCallbacks.getBroadcastItem(i) + .onTimeInSongChange(secondsIn, secondsTotal); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mServiceCallbacks.finishBroadcast(); + } + } + + private void parsePlaylistNotification(List tokens) { + Log.v(TAG, "Playlist notification received: " + tokens); + String notification = tokens.get(2); + if ("newsong".equals(notification)) { + // When we don't subscribe to the current players status, we rely + // on playlist notifications and order song details here. + // TODO keep track of subscribe status + cli.sendPlayerCommand("status - 1 tags:" + SONGTAGS); + } else if ("play".equals(notification)) { + updatePlayStatus(PlayerState.PlayStatus.play); + } else if ("stop".equals(notification)) { + updatePlayStatus(PlayerState.PlayStatus.stop); + } else if ("pause".equals(notification)) { + parsePause(tokens.size() >= 4 ? tokens.get(3) : null); + } else if ("addtracks".equals(notification)) { + int i = mCurrentPlaylistCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mCurrentPlaylistCallbacks.getBroadcastItem(i).onAddTracks(playerState); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mCurrentPlaylistCallbacks.finishBroadcast(); + } else if ("delete".equals(notification)) { + int i = mCurrentPlaylistCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mCurrentPlaylistCallbacks.getBroadcastItem(i) + .onDelete(playerState, Integer.parseInt(tokens.get(3))); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mCurrentPlaylistCallbacks.finishBroadcast(); + } + } + + private void parsePause(String explicitPause) { + if ("0".equals(explicitPause)) { + updatePlayStatus(PlayerState.PlayStatus.play); + } else if ("1".equals(explicitPause)) { + updatePlayStatus(PlayerState.PlayStatus.pause); + } + } + + private HashMap parseTokens(List tokens) { + HashMap tokenMap = new HashMap(); + String key, value; + for (String token : tokens) { + if (token == null || token.length() == 0) { + continue; + } + int colonPos = token.indexOf("%3A"); + if (colonPos == -1) { + key = Util.decode(token); + value = null; + } else { + key = Util.decode(token.substring(0, colonPos)); + value = Util.decode(token.substring(colonPos + 3)); + } + tokenMap.put(key, value); + } + return tokenMap; + } + + private void parseStatusLine(List tokens) { + HashMap tokenMap = parseTokens(tokens); + + updatePowerStatus(Util.parseDecimalIntOrZero(tokenMap.get("power")) == 1); + updatePlayStatus(PlayStatus.valueOf(tokenMap.get("mode"))); + updateShuffleStatus(ShuffleStatus + .valueOf(Util.parseDecimalIntOrZero(tokenMap.get("playlist shuffle")))); + updateRepeatStatus( + RepeatStatus.valueOf(Util.parseDecimalIntOrZero(tokenMap.get("playlist repeat")))); + + playerState.setCurrentPlaylist(tokenMap.get("playlist_name")); + playerState.setCurrentPlaylistIndex( + Util.parseDecimalIntOrZero(tokenMap.get("playlist_cur_index"))); + updateCurrentSong(new Song(tokenMap)); + + updateTimes(Util.parseDecimalIntOrZero(tokenMap.get("time")), + Util.parseDecimalIntOrZero(tokenMap.get("duration"))); + } + + void changeActivePlayer(Player newPlayer) { + if (newPlayer == null) { + return; + } + + Log.v(TAG, "Active player now: " + newPlayer); + final String playerId = newPlayer.getId(); + String oldPlayerId = (connectionState.getActivePlayer() != null ? connectionState + .getActivePlayer().getId() : null); + boolean changed = false; + if (oldPlayerId == null || !oldPlayerId.equals(playerId)) { + if (oldPlayerId != null) { + // Unsubscribe from the old player's status. (despite what + // the docs say, multiple subscribes can be active and flood us.) + cli.sendCommand(Util.encode(oldPlayerId) + " status - 1 subscribe:-"); + } + + connectionState.setActivePlayer(newPlayer); + changed = true; + } + + // Start an async fetch of its status. + cli.sendPlayerCommand("status - 1 tags:" + SONGTAGS); + + if (changed) { + updatePlayerSubscriptionState(); + + // NOTE: this involves a write and can block (sqlite lookup via binder call), so + // should be done off-thread, so we can process service requests & send our callback + // as quickly as possible. + executor.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "Saving " + Preferences.KEY_LASTPLAYER + "=" + playerId); + final SharedPreferences preferences = getSharedPreferences(Preferences.NAME, + MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(Preferences.KEY_LASTPLAYER, playerId); + editor.commit(); + } + }); + } + + int i = mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mServiceCallbacks.getBroadcastItem(i).onPlayerChanged(newPlayer); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mServiceCallbacks.finishBroadcast(); + } + + + private void updatePlayerSubscriptionState() { + // Subscribe or unsubscribe to the player's realtime status updates + // depending on whether we have an Activity or some sort of client + // that cares about second-to-second updates. + // + // Note: If scrobbling is turned on then that counts as caring + // about second-to-second updates -- otherwise we miss events from + // buttons on the player, the web interface, and so on + int clients = mServiceCallbacks.beginBroadcast(); + mServiceCallbacks.finishBroadcast(); + if (clients > 0 || scrobblingEnabled) { + cli.sendPlayerCommand("status - 1 subscribe:1 tags:" + SONGTAGS); + } else { + cli.sendPlayerCommand("status - 1 subscribe:-"); + } + } + + /** + * Authenticate on the SqueezeServer. + *

+ * The server does + *

+     * login user wrongpassword
+     * login user ******
+     * (Connection terminted)
+     * 
+ * instead of as documented + *
+     * login user wrongpassword
+     * (Connection terminted)
+     * 
+ * therefore a disconnect when handshake (the next step after authentication) is not completed, + * is considered an authentication failure. + */ + void onCliPortConnectionEstablished(final String userName, final String password) { + cli.sendCommandImmediately("login " + Util.encode(userName) + " " + Util.encode(password)); + } + + /** + * Handshake with the SqueezeServer, learn some of its supported features, and start listening + * for asynchronous updates of server state. + */ + private void onAuthenticated() { + cli.sendCommandImmediately("listen 1", + "players 0 1", // initiate an async player fetch + "can musicfolder ?", // learn music folder browsing support + "can randomplay ?", // learn random play function functionality + "pref httpport ?", // learn the HTTP port (needed for images) + "pref jivealbumsort ?", // learn the preferred album sort order + + // Fetch the version number. This must be the last thing + // fetched, as seeing the result triggers the + // "handshake is complete" logic elsewhere. + "version ?" + ); + } + + /* Start an asynchronous fetch of the squeezeservers localized strings */ + private void strings() { + cli.sendCommandImmediately("getstring " + ServerString.values()[0].name()); + } + + private void updatePlayStatus(PlayStatus state) { + if (playerState.getPlayStatus() != state) { + playerState.setPlayStatus(state); + //TODO when do we want to keep the wiFi lock ? + connectionState.updateWifiLock(playerState.isPlaying()); + updateOngoingNotification(); + + int i = mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mServiceCallbacks.getBroadcastItem(i).onPlayStatusChanged(state.name()); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mServiceCallbacks.finishBroadcast(); + } + } + + private void updateShuffleStatus(ShuffleStatus shuffleStatus) { + if (shuffleStatus != playerState.getShuffleStatus()) { + boolean wasUnknown = playerState.getShuffleStatus() == null; + playerState.setShuffleStatus(shuffleStatus); + int i = mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mServiceCallbacks.getBroadcastItem(i) + .onShuffleStatusChanged(wasUnknown, shuffleStatus.getId()); + } catch (RemoteException e) { + } + } + mServiceCallbacks.finishBroadcast(); + } + } + + private void updateRepeatStatus(RepeatStatus repeatStatus) { + if (repeatStatus != playerState.getRepeatStatus()) { + boolean wasUnknown = playerState.getRepeatStatus() == null; + playerState.setRepeatStatus(repeatStatus); + int i = mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mServiceCallbacks.getBroadcastItem(i) + .onRepeatStatusChanged(wasUnknown, repeatStatus.getId()); + } catch (RemoteException e) { + } + } + mServiceCallbacks.finishBroadcast(); + } + } + + private void updateOngoingNotification() { + boolean playing = playerState.isPlaying(); + String songName = playerState.getCurrentSongName(); + String playerName = connectionState.getActivePlayer() != null ? connectionState + .getActivePlayer().getName() : "squeezer"; + + // Update scrobble state, if either we're currently scrobbling, or we + // were (to catch the case where we started scrobbling a song, and the + // user went in to settings to disable scrobbling). + if (scrobblingEnabled || scrobblingPreviouslyEnabled) { + scrobblingPreviouslyEnabled = scrobblingEnabled; + Song s = playerState.getCurrentSong(); + + if (s != null) { + Log.v(TAG, "Scrobbling, playing is: " + playing); + Intent i = new Intent(); + + if (Scrobble.haveScrobbleDroid()) { + // http://code.google.com/p/scrobbledroid/wiki/DeveloperAPI + i.setAction("net.jjc1138.android.scrobbler.action.MUSIC_STATUS"); + i.putExtra("playing", playing); + i.putExtra("track", songName); + i.putExtra("album", s.getAlbum()); + i.putExtra("artist", s.getArtist()); + i.putExtra("secs", playerState.getCurrentSongDuration()); + i.putExtra("source", "P"); + } else if (Scrobble.haveSls()) { + // http://code.google.com/p/a-simple-lastfm-scrobbler/wiki/Developers + i.setAction("com.adam.aslfms.notify.playstatechanged"); + i.putExtra("state", playing ? 0 : 2); + i.putExtra("app-name", getText(R.string.app_name)); + i.putExtra("app-package", "uk.org.ngo.squeezer"); + i.putExtra("track", songName); + i.putExtra("album", s.getAlbum()); + i.putExtra("artist", s.getArtist()); + i.putExtra("duration", playerState.getCurrentSongDuration()); + i.putExtra("source", "P"); + } + sendBroadcast(i); + } + } + + if (!playing) { + if (!mUpdateOngoingNotification) { + clearOngoingNotification(); + return; + } + } + + NotificationManager nm = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + Notification status = new Notification(); + //status.contentView = views; + Intent showNowPlaying = new Intent(this, NowPlayingActivity.class) + .setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + PendingIntent pIntent = PendingIntent.getActivity(this, 0, showNowPlaying, 0); + if (playing) { + status.setLatestEventInfo(this, + getString(R.string.notification_playing_text, playerName), songName, pIntent); + status.flags |= Notification.FLAG_ONGOING_EVENT; + status.icon = R.drawable.stat_notify_musicplayer; + } else { + status.setLatestEventInfo(this, + getString(R.string.notification_connected_text, playerName), "-", pIntent); + status.flags |= Notification.FLAG_ONGOING_EVENT; + status.icon = R.drawable.ic_launcher; + } + nm.notify(PLAYBACKSERVICE_STATUS, status); + } + + private void updateCurrentSong(Song song) { + Song currentSong = playerState.getCurrentSong(); + if ((song == null ? (currentSong != null) : !song.equals(currentSong))) { + Log.d(TAG, "updateCurrentSong: " + song); + playerState.setCurrentSong(song); + updateOngoingNotification(); + int i = mMusicChangedCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mMusicChangedCallbacks.getBroadcastItem(i).onMusicChanged(playerState); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mMusicChangedCallbacks.finishBroadcast(); + } + } + + private void clearOngoingNotification() { + NotificationManager nm = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(PLAYBACKSERVICE_STATUS); + } + + private void updatePowerStatus(boolean powerStatus) { + if (powerStatus != playerState.isPoweredOn()) { + playerState.setPoweredOn(powerStatus); + int i = mServiceCallbacks.beginBroadcast(); + while (i > 0) { + i--; + try { + mServiceCallbacks.getBroadcastItem(i) + .onPowerStatusChanged(squeezeService.canPowerOn(), + squeezeService.canPowerOff()); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mServiceCallbacks.finishBroadcast(); + } + } + + private final ISqueezeService.Stub squeezeService = new ISqueezeService.Stub() { + + @Override + public void registerCallback(IServiceCallback callback) throws RemoteException { + mServiceCallbacks.register(callback); + updatePlayerSubscriptionState(); + } + + @Override + public void unregisterCallback(IServiceCallback callback) throws RemoteException { + mServiceCallbacks.unregister(callback); + updatePlayerSubscriptionState(); + } + + @Override + public void registerCurrentPlaylistCallback(IServiceCurrentPlaylistCallback callback) + throws RemoteException { + mCurrentPlaylistCallbacks.register(callback); + } + + @Override + public void unregisterCurrentPlaylistCallback(IServiceCurrentPlaylistCallback callback) + throws RemoteException { + mCurrentPlaylistCallbacks.unregister(callback); + } + + @Override + public void registerMusicChangedCallback(IServiceMusicChangedCallback callback) + throws RemoteException { + mMusicChangedCallbacks.register(callback); + } + + @Override + public void unregisterMusicChangedCallback(IServiceMusicChangedCallback callback) + throws RemoteException { + mMusicChangedCallbacks.unregister(callback); + } + + @Override + public void registerHandshakeCallback(IServiceHandshakeCallback callback) + throws RemoteException { + mHandshakeCallbacks.register(callback); + + // Call onHandshakeCompleted() immediately if handshaking is done. + if (mHandshakeComplete) { + callback.onHandshakeCompleted(); + } + } + + @Override + public void unregisterHandshakeCallback(IServiceHandshakeCallback callback) + throws RemoteException { + mHandshakeCallbacks.unregister(callback); + } + + @Override + public void registerVolumeCallback(IServiceVolumeCallback callback) throws RemoteException { + mVolumeCallbacks.register(callback); + } + + @Override + public void unregisterVolumeCallback(IServiceVolumeCallback callback) + throws RemoteException { + mVolumeCallbacks.unregister(callback); + } + + @Override + public void adjustVolumeTo(int newVolume) throws RemoteException { + cli.sendPlayerCommand("mixer volume " + Math.min(100, Math.max(0, newVolume))); + } + + @Override + public void adjustVolumeBy(int delta) throws RemoteException { + if (delta > 0) { + cli.sendPlayerCommand("mixer volume %2B" + delta); + } else if (delta < 0) { + cli.sendPlayerCommand("mixer volume " + delta); + } + } + + @Override + public boolean isConnected() throws RemoteException { + return connectionState.isConnected(); + } + + @Override + public boolean isConnectInProgress() throws RemoteException { + return connectionState.isConnectInProgress(); + } + + @Override + public void startConnect(String hostPort, String userName, String password) + throws RemoteException { + connectionState + .startConnect(SqueezeService.this, executor, hostPort, userName, password); + } + + @Override + public void disconnect() throws RemoteException { + if (!isConnected()) { + return; + } + SqueezeService.this.disconnect(); + } + + @Override + public boolean powerOn() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("power 1"); + return true; + } + + @Override + public boolean powerOff() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("power 0"); + return true; + } + + @Override + public boolean canPowerOn() { + return canPower() && !playerState.isPoweredOn(); + } + + @Override + public boolean canPowerOff() { + return canPower() && playerState.isPoweredOn(); + } + + private boolean canPower() { + Player player = connectionState.getActivePlayer(); + return connectionState.isConnected() && player != null && player.isCanpoweroff(); + } + + /** + * Determines whether the Squeezeserver supports the + * musicfolders command. + * + * @return true if it does, false otherwise. + */ + @Override + public boolean canMusicfolder() { + return connectionState.canMusicfolder(); + } + + @Override + public boolean canRandomplay() { + return connectionState.canRandomplay(); + } + + @Override + public String preferredAlbumSort() { + return connectionState.getPreferredAlbumSort(); + } + + private String fadeInSecs() { + return mFadeInSecs > 0 ? " " + mFadeInSecs : ""; + } + + @Override + public boolean togglePausePlay() throws RemoteException { + if (!isConnected()) { + return false; + } + + PlayerState.PlayStatus playStatus = playerState.getPlayStatus(); + + // May be null (e.g., connected to a server with no connected + // players. TODO: Handle this better, since it's not obvious in the + // UI. + if (playStatus == null) { + return false; + } + + switch (playStatus) { + case play: + // NOTE: we never send ambiguous "pause" toggle commands (without the '1') + // because then we'd get confused when they came back in to us, not being + // able to differentiate ours coming back on the listen channel vs. those + // of those idiots at the dinner party messing around. + cli.sendPlayerCommand("pause 1"); + break; + case stop: + cli.sendPlayerCommand("play" + fadeInSecs()); + break; + case pause: + cli.sendPlayerCommand("pause 0" + fadeInSecs()); + break; + } + return true; + } + + @Override + public boolean play() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("play" + fadeInSecs()); + return true; + } + + @Override + public boolean stop() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("stop"); + return true; + } + + @Override + public boolean nextTrack() throws RemoteException { + if (!isConnected() || !isPlaying()) { + return false; + } + cli.sendPlayerCommand("button jump_fwd"); + return true; + } + + @Override + public boolean previousTrack() throws RemoteException { + if (!isConnected() || !isPlaying()) { + return false; + } + cli.sendPlayerCommand("button jump_rew"); + return true; + } + + @Override + public boolean toggleShuffle() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist shuffle"); + return true; + } + + @Override + public boolean toggleRepeat() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist repeat"); + return true; + } + + @Override + public boolean playlistControl(String cmd, String tag, String itemId) + throws RemoteException { + if (!isConnected()) { + return false; + } + + cli.sendPlayerCommand("playlistcontrol cmd:" + cmd + " " + tag + ":" + itemId); + return true; + } + + @Override + public boolean randomPlay(String type) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("randomplay " + type); + return true; + } + + /** + * Start playing the song in the current playlist at the given index. + * + * @param index the index to jump to + */ + @Override + public boolean playlistIndex(int index) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist index " + index + fadeInSecs()); + return true; + } + + @Override + public boolean playlistRemove(int index) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist delete " + index); + return true; + } + + @Override + public boolean playlistMove(int fromIndex, int toIndex) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist move " + fromIndex + " " + toIndex); + return true; + } + + @Override + public boolean playlistClear() throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist clear"); + return true; + } + + @Override + public boolean playlistSave(String name) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand("playlist save " + Util.encode(name)); + return true; + } + + @Override + public boolean pluginPlaylistControl(Plugin plugin, String cmd, String itemId) + throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendPlayerCommand(plugin.getId() + " playlist " + cmd + " item_id:" + itemId); + return true; + + } + + private boolean isPlaying() throws RemoteException { + return playerState.isPlaying(); + } + + @Override + public void setActivePlayer(Player player) throws RemoteException { + changeActivePlayer(player); + } + + @Override + public Player getActivePlayer() throws RemoteException { + return connectionState.getActivePlayer(); + } + + @Override + public PlayerState getPlayerState() throws RemoteException { + return playerState; + } + + @Override + public String getCurrentPlaylist() { + return playerState.getCurrentPlaylist(); + } + + @Override + public String getAlbumArtUrl(String artworkTrackId) throws RemoteException { + return getAbsoluteUrl(artworkTrackIdUrl(artworkTrackId)); + } + + private String artworkTrackIdUrl(String artworkTrackId) { + return "/music/" + artworkTrackId + "/cover.jpg"; + } + + /** + * Returns a URL to download a song. + * + * @param songId the song ID + * @return The URL (as a string) + */ + @Override + public String getSongDownloadUrl(String songId) throws RemoteException { + return getAbsoluteUrl(songDownloadUrl(songId)); + } + + private String songDownloadUrl(String songId) { + return "/music/" + songId + "/download"; + } + + @Override + public String getIconUrl(String icon) throws RemoteException { + return getAbsoluteUrl('/' + icon); + } + + private String getAbsoluteUrl(String relativeUrl) { + Integer port = connectionState.getHttpPort(); + if (port == null || port == 0) { + return ""; + } + return "http://" + connectionState.getCurrentHost() + ":" + port + relativeUrl; + } + + @Override + public boolean setSecondsElapsed(int seconds) throws RemoteException { + if (!isConnected()) { + return false; + } + if (seconds < 0) { + return false; + } + + cli.sendPlayerCommand("time " + seconds); + + return true; + } + + @Override + public void preferenceChanged(String key) throws RemoteException { + Log.i(TAG, "Preference changed: " + key); + if (Preferences.KEY_NOTIFY_OF_CONNECTION.equals(key)) { + updateOngoingNotification(); + return; + } + + // If the server address changed then disconnect. + if (Preferences.KEY_SERVERADDR.equals(key)) { + disconnect(); + return; + } + + getPreferences(); + } + + /* Start an async fetch of the SqueezeboxServer's players */ + @Override + public boolean players(int start) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestItems("players", start); + return true; + } + + @Override + public void registerPlayerListCallback(IServicePlayerListCallback callback) + throws RemoteException { + Log.v(TAG, "PlayerListCallback attached."); + SqueezeService.this.playerListCallback.set(callback); + } + + @Override + public void unregisterPlayerListCallback(IServicePlayerListCallback callback) + throws RemoteException { + Log.v(TAG, "PlayerListCallback detached."); + SqueezeService.this.playerListCallback.compareAndSet(callback, null); + cli.cancelRequests(Player.class); + } + + /* Start an async fetch of the SqueezeboxServer's albums, which are matching the given parameters */ + @Override + public boolean albums(int start, String sortOrder, String searchString, + Artist artist, Year year, Genre genre, Song song) + throws RemoteException { + if (!isConnected()) { + return false; + } + List parameters = new ArrayList(); + parameters.add("tags:" + ALBUMTAGS); + parameters.add("sort:" + sortOrder); + if (searchString != null && searchString.length() > 0) { + parameters.add("search:" + searchString); + } + if (artist != null) { + parameters.add("artist_id:" + artist.getId()); + } + if (year != null) { + parameters.add("year:" + year.getId()); + } + if (genre != null) { + parameters.add("genre_id:" + genre.getId()); + } + if (song != null) { + parameters.add("track_id:" + song.getId()); + } + cli.requestItems("albums", start, parameters); + return true; + } + + @Override + public void registerAlbumListCallback(IServiceAlbumListCallback callback) + throws RemoteException { + Log.v(TAG, "AlbumListCallback attached."); + SqueezeService.this.albumListCallback.set(callback); + } + + @Override + public void unregisterAlbumListCallback(IServiceAlbumListCallback callback) + throws RemoteException { + Log.v(TAG, "AlbumListCallback detached."); + SqueezeService.this.albumListCallback.compareAndSet(callback, null); + cli.cancelRequests(Album.class); + } + + + /* Start an async fetch of the SqueezeboxServer's artists */ + @Override + public boolean artists(int start, String searchString, Album album, + Genre genre) throws RemoteException { + if (!isConnected()) { + return false; + } + List parameters = new ArrayList(); + if (searchString != null && searchString.length() > 0) { + parameters.add("search:" + searchString); + } + if (album != null) { + parameters.add("album_id:" + album.getId()); + } + if (genre != null) { + parameters.add("genre_id:" + genre.getId()); + } + cli.requestItems("artists", start, parameters); + return true; + } + + @Override + public void registerArtistListCallback(IServiceArtistListCallback callback) + throws RemoteException { + Log.v(TAG, "ArtistListCallback attached."); + SqueezeService.this.artistListCallback.set(callback); + } + + @Override + public void unregisterArtistListCallback(IServiceArtistListCallback callback) + throws RemoteException { + Log.v(TAG, "ArtistListCallback detached."); + SqueezeService.this.artistListCallback.compareAndSet(callback, null); + cli.cancelRequests(Artist.class); + } + + + /* Start an async fetch of the SqueezeboxServer's years */ + @Override + public boolean years(int start) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestItems("years", start); + return true; + } + + @Override + public void registerYearListCallback(IServiceYearListCallback callback) + throws RemoteException { + Log.v(TAG, "YearListCallback attached."); + SqueezeService.this.yearListCallback.set(callback); + } + + @Override + public void unregisterYearListCallback(IServiceYearListCallback callback) + throws RemoteException { + Log.v(TAG, "YearListCallback detached."); + SqueezeService.this.yearListCallback.compareAndSet(callback, null); + cli.cancelRequests(Year.class); + } + + /* Start an async fetch of the SqueezeboxServer's genres */ + @Override + public boolean genres(int start, String searchString) throws RemoteException { + if (!isConnected()) { + return false; + } + List parameters = new ArrayList(); + if (searchString != null && searchString.length() > 0) { + parameters.add("search:" + searchString); + } + cli.requestItems("genres", start, parameters); + return true; + } + + @Override + public void registerGenreListCallback(IServiceGenreListCallback callback) + throws RemoteException { + Log.v(TAG, "GenreListCallback attached."); + SqueezeService.this.genreListCallback.set(callback); + } + + @Override + public void unregisterGenreListCallback(IServiceGenreListCallback callback) + throws RemoteException { + Log.v(TAG, "GenreListCallback detached."); + SqueezeService.this.genreListCallback.compareAndSet(callback, null); + cli.cancelRequests(Genre.class); + } + + /** + * Starts an async fetch of the contents of a SqueezerboxServer's music + * folders in the given folderId. + *

+ * folderId may be null, in which case the contents of the root music + * folder are returned. + *

+ * Results are returned through the callback registered with + * {@link registerMusicFolderListCallback}. + * + * @param start Where in the list of folders to start. + * @param folderId The folder to view. + * @return true if the request was sent, false + * if the service is not connected. + */ + @Override + public boolean musicFolders(int start, String folderId) throws RemoteException { + if (!isConnected()) { + return false; + } + + List parameters = new ArrayList(); + + if (folderId != null) { + parameters.add("folder_id:" + folderId); + } + + cli.requestItems("musicfolder", start, parameters); + return true; + } + + @Override + public void registerMusicFolderListCallback(IServiceMusicFolderListCallback callback) + throws RemoteException { + Log.v(TAG, "MusicFolderListCallback attached."); + SqueezeService.this.musicFolderListCallback.set(callback); + } + + @Override + public void unregisterMusicFolderListCallback(IServiceMusicFolderListCallback callback) + throws RemoteException { + Log.v(TAG, "MusicFolderListCallback detached."); + SqueezeService.this.musicFolderListCallback.compareAndSet(callback, null); + cli.cancelRequests(MusicFolderItem.class); + } + + /* Start an async fetch of the SqueezeboxServer's songs */ + @Override + public boolean songs(int start, String sortOrder, String searchString, Album album, + Artist artist, Year year, Genre genre) throws RemoteException { + if (!isConnected()) { + return false; + } + List parameters = new ArrayList(); + parameters.add("tags:" + SONGTAGS); + parameters.add("sort:" + sortOrder); + if (searchString != null && searchString.length() > 0) { + parameters.add("search:" + searchString); + } + if (album != null) { + parameters.add("album_id:" + album.getId()); + } + if (artist != null) { + parameters.add("artist_id:" + artist.getId()); + } + if (year != null) { + parameters.add("year:" + year.getId()); + } + if (genre != null) { + parameters.add("genre_id:" + genre.getId()); + } + cli.requestItems("songs", start, parameters); + return true; + } + + /* Start an async fetch of the SqueezeboxServer's current playlist */ + @Override + public boolean currentPlaylist(int start) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestPlayerItems("status", start, Arrays.asList("tags:" + SONGTAGS)); + return true; + } + + /* Start an async fetch of the songs of the supplied playlist */ + @Override + public boolean playlistSongs(int start, Playlist playlist) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestItems("playlists tracks", start, + Arrays.asList("playlist_id:" + playlist.getId(), "tags:" + SONGTAGS)); + return true; + } + + @Override + public void registerSongListCallback(IServiceSongListCallback callback) + throws RemoteException { + Log.v(TAG, "SongListCallback attached."); + SqueezeService.this.songListCallback.set(callback); + } + + @Override + public void unregisterSongListCallback(IServiceSongListCallback callback) + throws RemoteException { + Log.v(TAG, "SongListCallback detached."); + SqueezeService.this.songListCallback.compareAndSet(callback, null); + cli.cancelRequests(Song.class); + } + + /* Start an async fetch of the SqueezeboxServer's playlists */ + @Override + public boolean playlists(int start) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestItems("playlists", start); + return true; + } + + @Override + public void registerPlaylistsCallback(IServicePlaylistsCallback callback) + throws RemoteException { + Log.v(TAG, "PlaylistsCallback attached."); + SqueezeService.this.playlistsCallback.set(callback); + } + + @Override + public void unregisterPlaylistsCallback(IServicePlaylistsCallback callback) + throws RemoteException { + Log.v(TAG, "PlaylistsCallback detached."); + SqueezeService.this.playlistsCallback.compareAndSet(callback, null); + cli.cancelRequests(Playlist.class); + } + + + @Override + public void registerPlaylistMaintenanceCallback( + IServicePlaylistMaintenanceCallback callback) throws RemoteException { + Log.v(TAG, "PlaylistMaintenanceCallback attached."); + playlistMaintenanceCallback.set(callback); + } + + @Override + public void unregisterPlaylistMaintenanceCallback( + IServicePlaylistMaintenanceCallback callback) throws RemoteException { + Log.v(TAG, "PlaylistMaintenanceCallback detached."); + playlistMaintenanceCallback.compareAndSet(callback, null); + } + + @Override + public boolean playlistsDelete(Playlist playlist) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendCommand("playlists delete playlist_id:" + playlist.getId()); + return true; + } + + @Override + public boolean playlistsMove(Playlist playlist, int index, int toindex) + throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendCommand("playlists edit cmd:move playlist_id:" + playlist.getId() + + " index:" + index + " toindex:" + toindex); + return true; + } + + @Override + public boolean playlistsNew(String name) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendCommand("playlists new name:" + Util.encode(name)); + return true; + } + + @Override + public boolean playlistsRemove(Playlist playlist, int index) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendCommand("playlists edit cmd:delete playlist_id:" + playlist.getId() + " index:" + + index); + return true; + } + + @Override + public boolean playlistsRename(Playlist playlist, String newname) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.sendCommand( + "playlists rename playlist_id:" + playlist.getId() + " dry_run:1 newname:" + + Util.encode(newname)); + return true; + } + + /* Start an asynchronous search of the SqueezeboxServer's library */ + @Override + public boolean search(int start, String searchString) throws RemoteException { + if (!isConnected()) { + return false; + } + + AlbumViewDialog.AlbumsSortOrder albumSortOrder = AlbumViewDialog.AlbumsSortOrder + .valueOf( + preferredAlbumSort()); + + artists(start, searchString, null, null); + albums(start, albumSortOrder.name().replace("__", ""), searchString, null, null, null, + null); + genres(start, searchString); + songs(start, SongOrderDialog.SongsSortOrder.title.name(), searchString, null, + null, null, null); + + return true; + } + + /* Start an asynchronous fetch of the squeezeservers radio type plugins */ + @Override + public boolean radios(int start) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestItems("radios", start); + return true; + } + + /* Start an asynchronous fetch of the squeezeservers radio application plugins */ + @Override + public boolean apps(int start) throws RemoteException { + if (!isConnected()) { + return false; + } + cli.requestItems("apps", start); + return true; + } + + @Override + public void registerPluginListCallback(IServicePluginListCallback callback) + throws RemoteException { + Log.v(TAG, "PluginListCallback attached."); + SqueezeService.this.pluginListCallback.set(callback); + } + + @Override + public void unregisterPluginListCallback(IServicePluginListCallback callback) + throws RemoteException { + Log.v(TAG, "PluginListCallback detached."); + SqueezeService.this.pluginListCallback.compareAndSet(callback, null); + cli.cancelRequests(Plugin.class); + } + + + /* Start an asynchronous fetch of the squeezeservers items of the given type */ + @Override + public boolean pluginItems(int start, Plugin plugin, PluginItem parent, String search) + throws RemoteException { + if (!isConnected()) { + return false; + } + List parameters = new ArrayList(); + if (parent != null) { + parameters.add("item_id:" + parent.getId()); + } + if (search != null && search.length() > 0) { + parameters.add("search:" + search); + } + cli.requestPlayerItems(plugin.getId() + " items", start, parameters); + return true; + } + + @Override + public void registerPluginItemListCallback(IServicePluginItemListCallback callback) + throws RemoteException { + Log.v(TAG, "SongListCallback attached."); + SqueezeService.this.pluginItemListCallback.set(callback); + } + + @Override + public void unregisterPluginItemListCallback(IServicePluginItemListCallback callback) + throws RemoteException { + Log.v(TAG, "PluginItemListCallback detached."); + SqueezeService.this.pluginItemListCallback.compareAndSet(callback, null); + cli.cancelRequests(PluginItem.class); + } + }; + +} diff --git a/src/uk/org/ngo/squeezer/util/AsyncTask.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/AsyncTask.java similarity index 53% rename from src/uk/org/ngo/squeezer/util/AsyncTask.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/AsyncTask.java index 15b159db5..fa439b4c4 100644 --- a/src/uk/org/ngo/squeezer/util/AsyncTask.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/AsyncTask.java @@ -16,6 +16,11 @@ package uk.org.ngo.squeezer.util; +import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Message; +import android.os.Process; + import java.util.ArrayDeque; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; @@ -32,176 +37,124 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import android.annotation.TargetApi; -import android.os.Handler; -import android.os.Message; -import android.os.Process; - /** - * ************************************* - * Copied from JB release framework: + * ************************************* Copied from JB release framework: * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java - * + *

* so that threading behavior on all OS versions is the same and we can tweak behavior by using * executeOnExecutor() if needed. - * - * There are 3 changes in this copy of AsyncTask: - * -pre-HC a single thread executor is used for serial operation - * (Executors.newSingleThreadExecutor) and is the default - * -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy - * -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added - * ************************************* - * - *

AsyncTask enables proper and easy use of the UI thread. This class allows to - * perform background operations and publish results on the UI thread without - * having to manipulate threads and/or handlers.

- * - *

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} - * and does not constitute a generic threading framework. AsyncTasks should ideally be - * used for short operations (a few seconds at the most.) If you need to keep threads - * running for long periods of time, it is highly recommended you use the various APIs - * provided by the java.util.concurrent pacakge such as {@link Executor}, - * {@link ThreadPoolExecutor} and {@link FutureTask}.

- * - *

An asynchronous task is defined by a computation that runs on a background thread and - * whose result is published on the UI thread. An asynchronous task is defined by 3 generic - * types, called Params, Progress and Result, - * and 4 steps, called onPreExecute, doInBackground, - * onProgressUpdate and onPostExecute.

- * - *
- *

Developer Guides

- *

For more information about using tasks and threads, read the - * Processes and - * Threads developer guide.

- *
- * - *

Usage

- *

AsyncTask must be subclassed to be used. The subclass will override at least - * one method ({@link #doInBackground}), and most often will override a - * second one ({@link #onPostExecute}.)

- * - *

Here is an example of subclassing:

- *
- * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
- *     protected Long doInBackground(URL... urls) {
- *         int count = urls.length;
- *         long totalSize = 0;
- *         for (int i = 0; i < count; i++) {
- *             totalSize += Downloader.downloadFile(urls[i]);
- *             publishProgress((int) ((i / (float) count) * 100));
- *             // Escape early if cancel() is called
- *             if (isCancelled()) break;
- *         }
- *         return totalSize;
- *     }
- *
- *     protected void onProgressUpdate(Integer... progress) {
- *         setProgressPercent(progress[0]);
- *     }
- *
- *     protected void onPostExecute(Long result) {
- *         showDialog("Downloaded " + result + " bytes");
- *     }
- * }
- * 
- * - *

Once created, a task is executed very simply:

- *
- * new DownloadFilesTask().execute(url1, url2, url3);
- * 
- * - *

AsyncTask's generic types

- *

The three types used by an asynchronous task are the following:

- *
    - *
  1. Params, the type of the parameters sent to the task upon - * execution.
  2. - *
  3. Progress, the type of the progress units published during - * the background computation.
  4. - *
  5. Result, the type of the result of the background - * computation.
  6. - *
- *

Not all types are always used by an asynchronous task. To mark a type as unused, - * simply use the type {@link Void}:

- *
- * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
+ * 

+ * There are 3 changes in this copy of AsyncTask: -pre-HC a single thread executor is used for + * serial operation (Executors.newSingleThreadExecutor) and is the default -the default + * THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy -a new fixed thread pool called + * DUAL_THREAD_EXECUTOR was added ************************************* + *

+ *

AsyncTask enables proper and easy use of the UI thread. This class allows to perform + * background operations and publish results on the UI thread without having to manipulate threads + * and/or handlers.

+ *

+ *

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} and does + * not constitute a generic threading framework. AsyncTasks should ideally be used for short + * operations (a few seconds at the most.) If you need to keep threads running for long periods of + * time, it is highly recommended you use the various APIs provided by the + * java.util.concurrent pacakge such as {@link Executor}, {@link ThreadPoolExecutor} + * and {@link FutureTask}.

+ *

+ *

An asynchronous task is defined by a computation that runs on a background thread and whose + * result is published on the UI thread. An asynchronous task is defined by 3 generic types, called + * Params, Progress and Result, and 4 steps, called + * onPreExecute, doInBackground, onProgressUpdate and + * onPostExecute.

+ *

+ *

Developer Guides

For more information about using + * tasks and threads, read the Processes + * and Threads developer guide.

+ *

+ *

Usage

AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a second one ({@link + * #onPostExecute}.)

+ *

+ *

Here is an example of subclassing:

 private class
+ * DownloadFilesTask extends AsyncTask<URL, Integer, Long> { protected Long
+ * doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i <
+ * count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float)
+ * count) * 100)); // Escape early if cancel() is called if (isCancelled()) break; } return
+ * totalSize; }
+ * 

+ * protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); } + *

+ * protected void onPostExecute(Long result) { showDialog("Downloaded " + result + " bytes"); } } *

- * - *

The 4 steps

- *

When an asynchronous task is executed, the task goes through 4 steps:

- *
    - *
  1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task - * is executed. This step is normally used to setup the task, for instance by - * showing a progress bar in the user interface.
  2. - *
  3. {@link #doInBackground}, invoked on the background thread - * immediately after {@link #onPreExecute()} finishes executing. This step is used - * to perform background computation that can take a long time. The parameters - * of the asynchronous task are passed to this step. The result of the computation must - * be returned by this step and will be passed back to the last step. This step - * can also use {@link #publishProgress} to publish one or more units - * of progress. These values are published on the UI thread, in the - * {@link #onProgressUpdate} step.
  4. - *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a - * call to {@link #publishProgress}. The timing of the execution is - * undefined. This method is used to display any form of progress in the user - * interface while the background computation is still executing. For instance, - * it can be used to animate a progress bar or show logs in a text field.
  6. - *
  7. {@link #onPostExecute}, invoked on the UI thread after the background - * computation finishes. The result of the background computation is passed to - * this step as a parameter.
  8. + *

    + *

    Once created, a task is executed very simply:

     new
    + * DownloadFilesTask().execute(url1, url2, url3); 
    + *

    + *

    AsyncTask's generic types

    The three types used by an asynchronous task are the + * following:

    1. Params, the type of the parameters sent to the task upon + * execution.
    2. Progress, the type of the progress units published during the + * background computation.
    3. Result, the type of the result of the background + * computation.

    Not all types are always used by an asynchronous task. To mark a type + * as unused, simply use the type {@link Void}:

     private class MyTask extends
    + * AsyncTask<Void, Void, Void> { ... } 
    + *

    + *

    The 4 steps

    When an asynchronous task is executed, the task goes through 4 steps:

    + *
    1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task is + * executed. This step is normally used to setup the task, for instance by showing a progress bar in + * the user interface.
    2. {@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used to perform + * background computation that can take a long time. The parameters of the asynchronous task are + * passed to this step. The result of the computation must be returned by this step and will be + * passed back to the last step. This step can also use {@link #publishProgress} to publish one or + * more units of progress. These values are published on the UI thread, in the {@link + * #onProgressUpdate} step.
    3. {@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is undefined. This method is used + * to display any form of progress in the user interface while the background computation is still + * executing. For instance, it can be used to animate a progress bar or show logs in a text + * field.
    4. {@link #onPostExecute}, invoked on the UI thread after the background computation + * finishes. The result of the background computation is passed to this step as a parameter.
    5. *
    - * - *

    Cancelling a task

    - *

    A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking - * this method will cause subsequent calls to {@link #isCancelled()} to return true. - * After invoking this method, {@link #onCancelled(Object)}, instead of - * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} - * returns. To ensure that a task is cancelled as quickly as possible, you should always - * check the return value of {@link #isCancelled()} periodically from - * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

    - * - *

    Threading rules

    - *

    There are a few threading rules that must be followed for this class to - * work properly:

    - *
      - *
    • The AsyncTask class must be loaded on the UI thread. This is done - * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
    • - *
    • The task instance must be created on the UI thread.
    • - *
    • {@link #execute} must be invoked on the UI thread.
    • - *
    • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, - * {@link #doInBackground}, {@link #onProgressUpdate} manually.
    • - *
    • The task can be executed only once (an exception will be thrown if - * a second execution is attempted.)
    • - *
    - * - *

    Memory observability

    - *

    AsyncTask guarantees that all callback calls are synchronized in such a way that the following - * operations are safe without explicit synchronizations.

    - *
      - *
    • Set member fields in the constructor or {@link #onPreExecute}, and refer to them - * in {@link #doInBackground}. - *
    • Set member fields in {@link #doInBackground}, and refer to them in - * {@link #onProgressUpdate} and {@link #onPostExecute}. - *
    - * - *

    Order of execution

    - *

    When first introduced, AsyncTasks were executed serially on a single background - * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed - * to a pool of threads allowing multiple tasks to operate in parallel. Starting with - * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single - * thread to avoid common application errors caused by parallel execution.

    - *

    If you truly want parallel execution, you can invoke - * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with - * {@link #THREAD_POOL_EXECUTOR}.

    + *

    + *

    Cancelling a task

    A task can be cancelled at any time by invoking {@link + * #cancel(boolean)}. Invoking this method will cause subsequent calls to {@link #isCancelled()} to + * return true. After invoking this method, {@link #onCancelled(Object)}, instead of {@link + * #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} returns. To + * ensure that a task is cancelled as quickly as possible, you should always check the return value + * of {@link #isCancelled()} periodically from {@link #doInBackground(Object[])}, if possible + * (inside a loop for instance.)

    + *

    + *

    Threading rules

    There are a few threading rules that must be followed for this class + * to work properly:

    • The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
    • The task instance + * must be created on the UI thread.
    • {@link #execute} must be invoked on the UI + * thread.
    • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, {@link + * #doInBackground}, {@link #onProgressUpdate} manually.
    • The task can be executed only once + * (an exception will be thrown if a second execution is attempted.)
    + *

    + *

    Memory observability

    AsyncTask guarantees that all callback calls are synchronized in + * such a way that the following operations are safe without explicit synchronizations.

      + *
    • Set member fields in the constructor or {@link #onPreExecute}, and refer to them in {@link + * #doInBackground}.
    • Set member fields in {@link #doInBackground}, and refer to them in {@link + * #onProgressUpdate} and {@link #onPostExecute}.
    + *

    + *

    Order of execution

    When first introduced, AsyncTasks were executed serially on a + * single background thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was + * changed to a pool of threads allowing multiple tasks to operate in parallel. Starting with {@link + * android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single thread to avoid common + * application errors caused by parallel execution.

    If you truly want parallel execution, you + * can invoke {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with {@link + * #THREAD_POOL_EXECUTOR}.

    */ public abstract class AsyncTask { + private static final String LOG_TAG = "AsyncTask"; private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; - private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { @@ -221,8 +174,8 @@ public Thread newThread(Runnable r) { new ThreadPoolExecutor.DiscardOldestPolicy()); /** - * An {@link Executor} that executes tasks one at a time in serial - * order. This serialization is global to a particular process. + * An {@link Executor} that executes tasks one at a time in serial order. This serialization is + * global to a particular process. */ public static final Executor SERIAL_EXECUTOR = UIUtils.hasHoneycomb() ? new SerialExecutor() : Executors.newSingleThreadExecutor(sThreadFactory); @@ -231,22 +184,28 @@ public Thread newThread(Runnable r) { Executors.newFixedThreadPool(2, sThreadFactory); private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; private static final InternalHandler sHandler = new InternalHandler(); private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + private final WorkerRunnable mWorker; + private final FutureTask mFuture; private volatile Status mStatus = Status.PENDING; private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); @TargetApi(11) private static class SerialExecutor implements Executor { + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; public synchronized void execute(final Runnable r) { @@ -272,8 +231,8 @@ protected synchronized void scheduleNext() { } /** - * Indicates the current status of the task. Each status will be set only once - * during the lifetime of a task. + * Indicates the current status of the task. Each status will be set only once during the + * lifetime of a task. */ public enum Status { /** @@ -290,12 +249,16 @@ public enum Status { FINISHED, } - /** @hide Used to force static handler to be created. */ + /** + * @hide Used to force static handler to be created. + */ public static void init() { sHandler.getLooper(); } - /** @hide */ + /** + * @hide + */ public static void setDefaultExecutor(Executor exec) { sDefaultExecutor = exec; } @@ -356,12 +319,10 @@ public final Status getStatus() { } /** - * Override this method to perform a computation on a background thread. The - * specified parameters are the parameters passed to {@link #execute} - * by the caller of this task. - * - * This method can call {@link #publishProgress} to publish updates - * on the UI thread. + * Override this method to perform a computation on a background thread. The specified + * parameters are the parameters passed to {@link #execute} by the caller of this task. + *

    + * This method can call {@link #publishProgress} to publish updates on the UI thread. * * @param params The parameters of the task. * @@ -379,13 +340,14 @@ public final Status getStatus() { * @see #onPostExecute * @see #doInBackground */ + @SuppressWarnings("EmptyMethod") protected void onPreExecute() { } /** - *

    Runs on the UI thread after {@link #doInBackground}. The - * specified result is the value returned by {@link #doInBackground}.

    - * + *

    Runs on the UI thread after {@link #doInBackground}. The specified result is the value + * returned by {@link #doInBackground}.

    + *

    *

    This method won't be invoked if the task was cancelled.

    * * @param result The result of the operation computed by {@link #doInBackground}. @@ -399,8 +361,8 @@ protected void onPostExecute(Result result) { } /** - * Runs on the UI thread after {@link #publishProgress} is invoked. - * The specified values are the values passed to {@link #publishProgress}. + * Runs on the UI thread after {@link #publishProgress} is invoked. The specified values are the + * values passed to {@link #publishProgress}. * * @param values The values indicating progress. * @@ -412,15 +374,13 @@ protected void onProgressUpdate(Progress... values) { } /** - *

    Runs on the UI thread after {@link #cancel(boolean)} is invoked and - * {@link #doInBackground(Object[])} has finished.

    - * - *

    The default implementation simply invokes {@link #onCancelled()} and - * ignores the result. If you write your own implementation, do not call - * super.onCancelled(result).

    + *

    Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link + * #doInBackground(Object[])} has finished.

    + *

    + *

    The default implementation simply invokes {@link #onCancelled()} and ignores the result. + * If you write your own implementation, do not call super.onCancelled(result).

    * - * @param result The result, if any, computed in - * {@link #doInBackground(Object[])}, can be null + * @param result The result, if any, computed in {@link #doInBackground(Object[])}, can be null * * @see #cancel(boolean) * @see #isCancelled() @@ -431,25 +391,25 @@ protected void onCancelled(Result result) { } /** - *

    Applications should preferably override {@link #onCancelled(Object)}. - * This method is invoked by the default implementation of - * {@link #onCancelled(Object)}.

    - * - *

    Runs on the UI thread after {@link #cancel(boolean)} is invoked and - * {@link #doInBackground(Object[])} has finished.

    + *

    Applications should preferably override {@link #onCancelled(Object)}. This method is + * invoked by the default implementation of {@link #onCancelled(Object)}.

    + *

    + *

    Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link + * #doInBackground(Object[])} has finished.

    * * @see #onCancelled(Object) * @see #cancel(boolean) * @see #isCancelled() */ + @SuppressWarnings("EmptyMethod") protected void onCancelled() { } /** - * Returns true if this task was cancelled before it completed - * normally. If you are calling {@link #cancel(boolean)} on the task, - * the value returned by this method should be checked periodically from - * {@link #doInBackground(Object[])} to end the task as soon as possible. + * Returns true if this task was cancelled before it completed normally. If you are + * calling {@link #cancel(boolean)} on the task, the value returned by this method should be + * checked periodically from {@link #doInBackground(Object[])} to end the task as soon as + * possible. * * @return true if task was cancelled before it completed * @@ -460,30 +420,24 @@ public final boolean isCancelled() { } /** - *

    Attempts to cancel execution of this task. This attempt will - * fail if the task has already completed, already been cancelled, - * or could not be cancelled for some other reason. If successful, - * and this task has not started when cancel is called, - * this task should never run. If the task has already started, - * then the mayInterruptIfRunning parameter determines - * whether the thread executing this task should be interrupted in - * an attempt to stop the task.

    - * - *

    Calling this method will result in {@link #onCancelled(Object)} being - * invoked on the UI thread after {@link #doInBackground(Object[])} - * returns. Calling this method guarantees that {@link #onPostExecute(Object)} - * is never invoked. After invoking this method, you should check the - * value returned by {@link #isCancelled()} periodically from - * {@link #doInBackground(Object[])} to finish the task as early as - * possible.

    - * - * @param mayInterruptIfRunning true if the thread executing this - * task should be interrupted; otherwise, in-progress tasks are allowed - * to complete. - * - * @return false if the task could not be cancelled, - * typically because it has already completed normally; - * true otherwise + *

    Attempts to cancel execution of this task. This attempt will fail if the task has already + * completed, already been cancelled, or could not be cancelled for some other reason. If + * successful, and this task has not started when cancel is called, this task should + * never run. If the task has already started, then the mayInterruptIfRunning parameter + * determines whether the thread executing this task should be interrupted in an attempt to stop + * the task.

    + *

    + *

    Calling this method will result in {@link #onCancelled(Object)} being invoked on the UI + * thread after {@link #doInBackground(Object[])} returns. Calling this method guarantees that + * {@link #onPostExecute(Object)} is never invoked. After invoking this method, you should check + * the value returned by {@link #isCancelled()} periodically from {@link + * #doInBackground(Object[])} to finish the task as early as possible.

    + * + * @param mayInterruptIfRunning true if the thread executing this task should be + * interrupted; otherwise, in-progress tasks are allowed to complete. + * + * @return false if the task could not be cancelled, typically because it has already + * completed normally; true otherwise * * @see #isCancelled() * @see #onCancelled(Object) @@ -494,23 +448,21 @@ public final boolean cancel(boolean mayInterruptIfRunning) { } /** - * Waits if necessary for the computation to complete, and then - * retrieves its result. + * Waits if necessary for the computation to complete, and then retrieves its result. * * @return The computed result. * * @throws CancellationException If the computation was cancelled. - * @throws ExecutionException If the computation threw an exception. - * @throws InterruptedException If the current thread was interrupted - * while waiting. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted while waiting. */ public final Result get() throws InterruptedException, ExecutionException { return mFuture.get(); } /** - * Waits if necessary for at most the given time for the computation - * to complete, and then retrieves its result. + * Waits if necessary for at most the given time for the computation to complete, and then + * retrieves its result. * * @param timeout Time to wait before cancelling the operation. * @param unit The time unit for the timeout. @@ -518,10 +470,9 @@ public final Result get() throws InterruptedException, ExecutionException { * @return The computed result. * * @throws CancellationException If the computation was cancelled. - * @throws ExecutionException If the computation threw an exception. - * @throws InterruptedException If the current thread was interrupted - * while waiting. - * @throws TimeoutException If the wait timed out. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted while waiting. + * @throws TimeoutException If the wait timed out. */ public final Result get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { @@ -529,30 +480,27 @@ public final Result get(long timeout, TimeUnit unit) throws InterruptedException } /** - * Executes the task with the specified parameters. The task returns - * itself (this) so that the caller can keep a reference to it. - * - *

    Note: this function schedules the task on a queue for a single background - * thread or pool of threads depending on the platform version. When first - * introduced, AsyncTasks were executed serially on a single background thread. - * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed - * to a pool of threads allowing multiple tasks to operate in parallel. Starting - * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being - * executed on a single thread to avoid common application errors caused - * by parallel execution. If you truly want parallel execution, you can use - * the {@link #executeOnExecutor} version of this method - * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings - * on its use. - * + * Executes the task with the specified parameters. The task returns itself (this) so that the + * caller can keep a reference to it. + *

    + *

    Note: this function schedules the task on a queue for a single background thread or pool + * of threads depending on the platform version. When first introduced, AsyncTasks were + * executed serially on a single background thread. Starting with {@link + * android.os.Build.VERSION_CODES#DONUT}, this was changed to a pool of threads allowing + * multiple tasks to operate in parallel. Starting {@link android.os.Build.VERSION_CODES#HONEYCOMB}, + * tasks are back to being executed on a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use the {@link + * #executeOnExecutor} version of this method with {@link #THREAD_POOL_EXECUTOR}; however, see + * commentary there for warnings on its use. + *

    *

    This method must be invoked on the UI thread. * * @param params The parameters of the task. * * @return This instance of AsyncTask. * - * @throws IllegalStateException If {@link #getStatus()} returns either - * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. - * + * @throws IllegalStateException If {@link #getStatus()} returns either {@link + * AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) * @see #execute(Runnable) */ @@ -561,36 +509,32 @@ public final AsyncTask execute(Params... params) { } /** - * Executes the task with the specified parameters. The task returns - * itself (this) so that the caller can keep a reference to it. - * - *

    This method is typically used with {@link #THREAD_POOL_EXECUTOR} to - * allow multiple tasks to run in parallel on a pool of threads managed by - * AsyncTask, however you can also use your own {@link Executor} for custom - * behavior. - * - *

    Warning: Allowing multiple tasks to run in parallel from - * a thread pool is generally not what one wants, because the order - * of their operation is not defined. For example, if these tasks are used - * to modify any state in common (such as writing a file due to a button click), - * there are no guarantees on the order of the modifications. - * Without careful work it is possible in rare cases for the newer version - * of the data to be over-written by an older one, leading to obscure data - * loss and stability issues. Such changes are best - * executed in serial; to guarantee such work is serialized regardless of + * Executes the task with the specified parameters. The task returns itself (this) so that the + * caller can keep a reference to it. + *

    + *

    This method is typically used with {@link #THREAD_POOL_EXECUTOR} to allow multiple tasks + * to run in parallel on a pool of threads managed by AsyncTask, however you can also use your + * own {@link Executor} for custom behavior. + *

    + *

    Warning: Allowing multiple tasks to run in parallel from a thread pool is + * generally not what one wants, because the order of their operation is not defined. + * For example, if these tasks are used to modify any state in common (such as writing a file + * due to a button click), there are no guarantees on the order of the modifications. Without + * careful work it is possible in rare cases for the newer version of the data to be + * over-written by an older one, leading to obscure data loss and stability issues. Such + * changes are best executed in serial; to guarantee such work is serialized regardless of * platform version you can use this function with {@link #SERIAL_EXECUTOR}. - * + *

    *

    This method must be invoked on the UI thread. * - * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a - * convenient process-wide thread pool for tasks that are loosely coupled. + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a convenient + * process-wide thread pool for tasks that are loosely coupled. * @param params The parameters of the task. * * @return This instance of AsyncTask. * - * @throws IllegalStateException If {@link #getStatus()} returns either - * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. - * + * @throws IllegalStateException If {@link #getStatus()} returns either {@link + * AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. * @see #execute(Object[]) */ public final AsyncTask executeOnExecutor(Executor exec, @@ -618,9 +562,8 @@ public final AsyncTask executeOnExecutor(Executor exec } /** - * Convenience version of {@link #execute(Object...)} for use with - * a simple Runnable object. See {@link #execute(Object[])} for more - * information on the order of execution. + * Convenience version of {@link #execute(Object...)} for use with a simple Runnable object. See + * {@link #execute(Object[])} for more information on the order of execution. * * @see #execute(Object[]) * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) @@ -630,13 +573,11 @@ public static void execute(Runnable runnable) { } /** - * This method can be invoked from {@link #doInBackground} to - * publish updates on the UI thread while the background computation is - * still running. Each call to this method will trigger the execution of - * {@link #onProgressUpdate} on the UI thread. - * - * {@link #onProgressUpdate} will note be called if the task has been - * canceled. + * This method can be invoked from {@link #doInBackground} to publish updates on the UI thread + * while the background computation is still running. Each call to this method will trigger the + * execution of {@link #onProgressUpdate} on the UI thread. + *

    + * {@link #onProgressUpdate} will note be called if the task has been canceled. * * @param values The progress values to update the UI with. * @@ -660,6 +601,7 @@ private void finish(Result result) { } private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg) { @@ -677,12 +619,15 @@ public void handleMessage(Message msg) { } private static abstract class WorkerRunnable implements Callable { + Params[] mParams; } @SuppressWarnings({"RawUseOfParameterizedType"}) private static class AsyncTaskResult { + final AsyncTask mTask; + final Data[] mData; AsyncTaskResult(AsyncTask task, Data... data) { @@ -690,4 +635,4 @@ private static class AsyncTaskResult { mData = data; } } -} \ No newline at end of file +} diff --git a/src/uk/org/ngo/squeezer/util/DiskLruCache.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/DiskLruCache.java similarity index 99% rename from src/uk/org/ngo/squeezer/util/DiskLruCache.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/DiskLruCache.java index f5244dfb1..fd17e842d 100644 --- a/src/uk/org/ngo/squeezer/util/DiskLruCache.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/DiskLruCache.java @@ -389,7 +389,8 @@ private void readJournalLine(String line) throws IOException { entry.setLengths(copyOfRange(parts, 2, parts.length)); } else if (parts[0].equals(DIRTY) && parts.length == 2) { entry.currentEditor = new Editor(entry); - } else if (parts[0].equals(READ) && parts.length == 2) { + } else //noinspection StatementWithEmptyBody + if (parts[0].equals(READ) && parts.length == 2) { // this work was already done by calling lruEntries.get() } else { throw new IOException("unexpected journal line: " + line); diff --git a/src/uk/org/ngo/squeezer/util/ImageCache.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageCache.java similarity index 83% rename from src/uk/org/ngo/squeezer/util/ImageCache.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageCache.java index 1990fcdcb..1277d47ab 100644 --- a/src/uk/org/ngo/squeezer/util/ImageCache.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageCache.java @@ -16,32 +16,32 @@ package uk.org.ngo.squeezer.util; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import uk.org.ngo.squeezer.BuildConfig; import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; -import android.os.Bundle; import android.os.Environment; import android.os.StatFs; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.util.LruCache; import android.util.Log; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import uk.org.ngo.squeezer.BuildConfig; + /** * This class holds our bitmap caches (memory and disk). */ public class ImageCache { + private static final String TAG = "ImageCache"; // Default memory cache size @@ -52,19 +52,28 @@ public class ImageCache { // Compression settings when writing images to disk cache private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG; + private static final int DEFAULT_COMPRESS_QUALITY = 70; + private static final int DISK_CACHE_INDEX = 0; // Constants to easily toggle various caches private static final boolean DEFAULT_MEM_CACHE_ENABLED = true; + private static final boolean DEFAULT_DISK_CACHE_ENABLED = true; + private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false; + private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false; private DiskLruCache mDiskLruCache; + private LruCache mMemoryCache; + private ImageCacheParams mCacheParams; + private final Object mDiskCacheLock = new Object(); + private boolean mDiskCacheStarting = true; /** @@ -92,21 +101,22 @@ public ImageCache(Context context, String uniqueName) { * * @param fragmentManager The fragment manager to use when dealing with the retained fragment. * @param cacheParams The cache parameters to use if creating the ImageCache + * * @return An existing retained ImageCache object or a new one if one did not exist */ public static ImageCache findOrCreateCache( FragmentManager fragmentManager, ImageCacheParams cacheParams) { // Search for, or create an instance of the non-UI RetainFragment - final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager); + final RetainFragment mRetainFragment = RetainFragment.getInstance(TAG, fragmentManager); // See if we already have an ImageCache stored in RetainFragment - ImageCache imageCache = (ImageCache) mRetainFragment.getObject(); + ImageCache imageCache = (ImageCache) mRetainFragment.get(TAG); // No existing ImageCache, create one and store it in RetainFragment if (imageCache == null) { imageCache = new ImageCache(cacheParams); - mRetainFragment.setObject(imageCache); + mRetainFragment.put(TAG, imageCache); } return imageCache; @@ -143,9 +153,9 @@ protected int sizeOf(String key, Bitmap bitmap) { /** * Initializes the disk cache. Note that this includes disk access so this should not be - * executed on the main/UI thread. By default an ImageCache does not initialize the disk - * cache when it is created, instead you should call initDiskCache() to initialize it on a - * background thread. + * executed on the main/UI thread. By default an ImageCache does not initialize the disk cache + * when it is created, instead you should call initDiskCache() to initialize it on a background + * thread. */ public void initDiskCache() { // Set up disk cache @@ -177,6 +187,7 @@ public void initDiskCache() { /** * Adds a bitmap to both memory and disk cache. + * * @param data Unique identifier for the bitmap to store * @param bitmap The bitmap to store */ @@ -256,6 +267,7 @@ public void addBitmapToDiskCache(String data, Bitmap bitmap) { * Get from memory cache. * * @param data Unique identifier for which item to get + * * @return The bitmap if found in cache, null otherwise */ public Bitmap getBitmapFromMemCache(String data) { @@ -283,6 +295,7 @@ public Bitmap getBitmapFromMemCache(String data) { * Get from disk cache. * * @param data Unique identifier for which item to get + * * @return The bitmap if found in cache, null otherwise */ public Bitmap getBitmapFromDiskCache(String data) { @@ -291,7 +304,8 @@ public Bitmap getBitmapFromDiskCache(String data) { while (mDiskCacheStarting) { try { mDiskCacheLock.wait(); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + } } if (mDiskLruCache != null) { InputStream inputStream = null; @@ -303,8 +317,7 @@ public Bitmap getBitmapFromDiskCache(String data) { } inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); if (inputStream != null) { - final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - return bitmap; + return BitmapFactory.decodeStream(inputStream); } } } catch (final IOException e) { @@ -314,7 +327,8 @@ public Bitmap getBitmapFromDiskCache(String data) { if (inputStream != null) { inputStream.close(); } - } catch (IOException e) {} + } catch (IOException e) { + } } } return null; @@ -322,8 +336,8 @@ public Bitmap getBitmapFromDiskCache(String data) { } /** - * Clears both the memory and disk cache associated with this ImageCache object. Note that - * this includes disk access so this should not be executed on the main/UI thread. + * Clears both the memory and disk cache associated with this ImageCache object. Note that this + * includes disk access so this should not be executed on the main/UI thread. */ public void clearCache() { if (mMemoryCache != null) { @@ -351,8 +365,8 @@ public void clearCache() { } /** - * Flushes the disk cache associated with this ImageCache object. Note that this includes - * disk access so this should not be executed on the main/UI thread. + * Flushes the disk cache associated with this ImageCache object. Note that this includes disk + * access so this should not be executed on the main/UI thread. */ public void flush() { synchronized (mDiskCacheLock) { @@ -370,8 +384,8 @@ public void flush() { } /** - * Closes the disk cache associated with this ImageCache object. Note that this includes - * disk access so this should not be executed on the main/UI thread. + * Closes the disk cache associated with this ImageCache object. Note that this includes disk + * access so this should not be executed on the main/UI thread. */ public void close() { synchronized (mDiskCacheLock) { @@ -395,14 +409,23 @@ public void close() { * A holder class that contains cache parameters. */ public static class ImageCacheParams { + public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; + public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE; + public File diskCacheDir; + public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT; + public int compressQuality = DEFAULT_COMPRESS_QUALITY; + public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; + public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; + public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START; + public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE; public ImageCacheParams(Context context, String uniqueName) { @@ -414,13 +437,12 @@ public ImageCacheParams(File diskCacheDir) { } /** - * Sets the memory cache size based on a percentage of the device memory class. - * Eg. setting percent to 0.2 would set the memory cache to one fifth of the device memory - * class. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8. - * - * This value should be chosen carefully based on a number of factors - * Refer to the corresponding Android Training class for more discussion: - * http://developer.android.com/training/displaying-bitmaps/ + * Sets the memory cache size based on a percentage of the device memory class. Eg. setting + * percent to 0.2 would set the memory cache to one fifth of the device memory class. Throws + * {@link IllegalArgumentException} if percent is < 0.05 or > .8. + *

    + * This value should be chosen carefully based on a number of factors Refer to the + * corresponding Android Training class for more discussion: http://developer.android.com/training/displaying-bitmaps/ * * @param context Context to use to fetch memory class * @param percent Percent of memory class to use to size memory cache @@ -444,22 +466,25 @@ private static int getMemoryClass(Context context) { * * @param context The context to use * @param uniqueName A unique directory name to append to the cache dir + * * @return The cache dir */ public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir - final String cachePath = - Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || - !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : - context.getCacheDir().getPath(); + File externalCacheDir = getExternalCacheDir(context); + final String cachePath = ((Environment.MEDIA_MOUNTED.equals(Environment + .getExternalStorageState()) || !isExternalStorageRemovable()) + && externalCacheDir != null) + ? getExternalCacheDir(context).getPath() + : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); } /** - * A hashing method that changes a string (like a URL) into a hash suitable for using as a - * disk filename. + * A hashing method that changes a string (like a URL) into a hash suitable for using as a disk + * filename. */ public static String hashKeyForDisk(String key) { String cacheKey; @@ -488,7 +513,9 @@ private static String bytesToHexString(byte[] bytes) { /** * Get the size in bytes of a bitmap. + * * @param bitmap + * * @return size in bytes */ @TargetApi(12) @@ -503,8 +530,7 @@ public static int getBitmapSize(Bitmap bitmap) { /** * Check if external storage is built-in or removable. * - * @return True if external storage is removable (like an SD card), false - * otherwise. + * @return True if external storage is removable (like an SD card), false otherwise. */ @TargetApi(9) public static boolean isExternalStorageRemovable() { @@ -518,6 +544,7 @@ public static boolean isExternalStorageRemovable() { * Get the external app cache directory. * * @param context The context to use + * * @return The external cache dir */ @TargetApi(8) @@ -535,6 +562,7 @@ public static File getExternalCacheDir(Context context) { * Check how much usable space is available at a given path. * * @param path The path to check + * * @return The space available in bytes */ @TargetApi(9) @@ -545,65 +573,4 @@ public static long getUsableSpace(File path) { final StatFs stats = new StatFs(path.getPath()); return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); } - - /** - * Locate an existing instance of this Fragment or if not found, create and - * add it using FragmentManager. - * - * @param fm The FragmentManager manager to use. - * @return The existing instance of the Fragment or the new instance if just - * created. - */ - public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { - // Check to see if we have retained the worker fragment. - RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG); - - // If not retained (or first time running), we need to create and add it. - if (mRetainFragment == null) { - mRetainFragment = new RetainFragment(); - fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss(); - } - - return mRetainFragment; - } - - /** - * A simple non-UI Fragment that stores a single Object and is retained over configuration - * changes. It will be used to retain the ImageCache object. - */ - public static class RetainFragment extends Fragment { - private Object mObject; - - /** - * Empty constructor as per the Fragment documentation - */ - public RetainFragment() {} - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Make sure this Fragment is retained over a configuration change - setRetainInstance(true); - } - - /** - * Store a single object in this Fragment. - * - * @param object The object to store - */ - public void setObject(Object object) { - mObject = object; - } - - /** - * Get the stored object. - * - * @return The stored object - */ - public Object getObject() { - return mObject; - } - } - } diff --git a/src/uk/org/ngo/squeezer/util/ImageFetcher.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageFetcher.java similarity index 90% rename from src/uk/org/ngo/squeezer/util/ImageFetcher.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageFetcher.java index 0ed61136f..69c17b4e6 100644 --- a/src/uk/org/ngo/squeezer/util/ImageFetcher.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright 2012 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,11 @@ package uk.org.ngo.squeezer.util; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.util.Log; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; @@ -27,32 +32,32 @@ import java.net.URL; import uk.org.ngo.squeezer.BuildConfig; -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; /** - * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL. + * A subclass of {@link ImageWorker} that fetches images from a URL. */ public class ImageFetcher extends ImageResizer { + private static final String TAG = "ImageFetcher"; + private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB + private static final String HTTP_CACHE_DIR = "http"; + private static final int IO_BUFFER_SIZE = 8 * 1024; private DiskLruCache mHttpDiskCache; + private File mHttpCacheDir; + private boolean mHttpDiskCacheStarting = true; + private final Object mHttpDiskCacheLock = new Object(); + private static final int DISK_CACHE_INDEX = 0; /** - * Initialize providing a target image width and height for the processing images. - * - * @param context - * @param imageWidth - * @param imageHeight + * Create an ImageFetcher specifying custom parameters. */ public ImageFetcher(Context context, int imageWidth, int imageHeight) { super(context, imageWidth, imageHeight); @@ -60,10 +65,7 @@ public ImageFetcher(Context context, int imageWidth, int imageHeight) { } /** - * Initialize providing a single target image size (used for both width and height); - * - * @param context - * @param imageSize + * Create an ImageFetcher using default parameters. */ public ImageFetcher(Context context, int imageSize) { super(context, imageSize); @@ -157,12 +159,12 @@ protected void closeCacheInternal() { } } - /** * The main process method, which will be called by the ImageWorker in the AsyncTask background * thread. * - * @param data The data to load the bitmap, in this case, a regular http URL + * @param key The key to load the bitmap, in this case, a regular http URL + * * @return The downloaded and resized bitmap */ private Bitmap processBitmap(String data) { @@ -179,7 +181,8 @@ private Bitmap processBitmap(String data) { while (mHttpDiskCacheStarting) { try { mHttpDiskCacheLock.wait(); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + } } if (mHttpDiskCache != null) { @@ -213,7 +216,8 @@ private Bitmap processBitmap(String data) { if (fileDescriptor == null && fileInputStream != null) { try { fileInputStream.close(); - } catch (IOException e) {} + } catch (IOException e) { + } } } } @@ -226,7 +230,8 @@ private Bitmap processBitmap(String data) { if (fileInputStream != null) { try { fileInputStream.close(); - } catch (IOException e) {} + } catch (IOException e) { + } } return bitmap; } @@ -236,11 +241,15 @@ protected Bitmap processBitmap(Object data) { return processBitmap(String.valueOf(data)); } + /** - * Download a bitmap from a URL and write the content to an output stream. + * Download a bitmap from a URL, write it to a disk and return the File pointer. This + * implementation uses a simple disk cache. * + * @param context The context to use * @param urlString The URL to fetch - * @return true if successful, false otherwise + * + * @return A File pointing to the fetched bitmap */ public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { disableConnectionReuseIfNecessary(); @@ -272,14 +281,14 @@ public boolean downloadUrlToStream(String urlString, OutputStream outputStream) if (in != null) { in.close(); } - } catch (final IOException e) {} + } catch (final IOException e) { + } } return false; } /** - * Workaround for bug pre-Froyo, see here for more info: - * http://android-developers.blogspot.com/2011/09/androids-http-clients.html + * Workaround for bug pre-Froyo, see here for more info: http://android-developers.blogspot.com/2011/09/androids-http-clients.html */ public static void disableConnectionReuseIfNecessary() { // HTTP connection reuse which was buggy pre-froyo diff --git a/src/uk/org/ngo/squeezer/util/ImageResizer.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageResizer.java similarity index 96% rename from src/uk/org/ngo/squeezer/util/ImageResizer.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageResizer.java index 209279280..46246ae0b 100644 --- a/src/uk/org/ngo/squeezer/util/ImageResizer.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageResizer.java @@ -16,23 +16,27 @@ package uk.org.ngo.squeezer.util; -import java.io.FileDescriptor; - -import uk.org.ngo.squeezer.BuildConfig; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; +import java.io.FileDescriptor; + +import uk.org.ngo.squeezer.BuildConfig; + /** * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width * and height. Useful for when the input images might be too large to simply load directly into * memory. */ public class ImageResizer extends ImageWorker { + private static final String TAG = "ImageResizer"; + protected int mImageWidth; + protected int mImageHeight; /** @@ -83,6 +87,7 @@ public void setImageSize(int size) { * sampling down the bitmap and returning it from a resource. * * @param resId + * * @return */ private Bitmap processBitmap(int resId) { @@ -104,8 +109,9 @@ protected Bitmap processBitmap(Object data) { * @param resId The resource id of the image data * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height + * that are equal to or greater than the requested width and height */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { @@ -129,8 +135,9 @@ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, * @param filename The full path of the file to decode * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height + * that are equal to or greater than the requested width and height */ public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) { @@ -154,8 +161,9 @@ public static Bitmap decodeSampledBitmapFromFile(String filename, * @param fileDescriptor The file descriptor to read from * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * * @return A bitmap sampled down from the original with the same aspect ratio and dimensions - * that are equal to or greater than the requested width and height + * that are equal to or greater than the requested width and height */ public static Bitmap decodeSampledBitmapFromDescriptor( FileDescriptor fileDescriptor, int reqWidth, int reqHeight) { @@ -182,9 +190,10 @@ public static Bitmap decodeSampledBitmapFromDescriptor( * results in a larger bitmap which isn't as useful for caching purposes. * * @param options An options object with out* params already populated (run through a decode* - * method with inJustDecodeBounds==true + * method with inJustDecodeBounds==true * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap + * * @return The value to be used for inSampleSize */ public static int calculateInSampleSize(BitmapFactory.Options options, diff --git a/src/uk/org/ngo/squeezer/util/ImageWorker.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageWorker.java similarity index 90% rename from src/uk/org/ngo/squeezer/util/ImageWorker.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageWorker.java index 9d226cd0c..40912bb53 100644 --- a/src/uk/org/ngo/squeezer/util/ImageWorker.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/ImageWorker.java @@ -16,43 +16,54 @@ package uk.org.ngo.squeezer.util; -import java.lang.ref.WeakReference; - -import uk.org.ngo.squeezer.BuildConfig; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.support.v4.app.FragmentManager; import android.util.Log; import android.widget.ImageView; +import java.lang.ref.WeakReference; + +import uk.org.ngo.squeezer.BuildConfig; + /** * This class wraps up completing some arbitrary long running work when loading a bitmap to an * ImageView. It handles things like using a memory and disk cache, running the work in a background * thread and setting a placeholder image. */ public abstract class ImageWorker { + private static final String TAG = "ImageWorker"; + private static final int FADE_IN_TIME = 200; private ImageCache mImageCache; + private ImageCache.ImageCacheParams mImageCacheParams; + private Bitmap mLoadingBitmap; + private boolean mFadeInBitmap = true; + private boolean mExitTasksEarly = false; + protected boolean mPauseWork = false; + private final Object mPauseWorkLock = new Object(); protected Resources mResources; private static final int MESSAGE_CLEAR = 0; + private static final int MESSAGE_INIT_DISK_CACHE = 1; + private static final int MESSAGE_FLUSH = 2; + private static final int MESSAGE_CLOSE = 3; protected ImageWorker(Context context) { @@ -60,11 +71,11 @@ protected ImageWorker(Context context) { } /** - * Load an image specified by the data parameter into an ImageView (override - * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk - * cache will be used if an {@link ImageCache} has been set using - * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it - * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the + * Load an image specified by the data parameter into an ImageView (override {@link + * ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk cache + * will be used if an {@link ImageCache} has been set using {@link + * ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it is set + * immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the * bitmap. * * @param data The URL of the image to download. @@ -118,6 +129,7 @@ public void setLoadingImage(int resId) { /** * Adds an {@link ImageCache} to this worker in the background (to prevent disk access on UI * thread). + * * @param fragmentManager * @param cacheParams */ @@ -129,10 +141,10 @@ public void addImageCache(FragmentManager fragmentManager, } /** - * Sets the {@link ImageCache} object to use with this ImageWorker. Usually you will not need - * to call this directly, instead use {@link ImageWorker#addImageCache} which will create and - * add the {@link ImageCache} object in a background thread (to ensure no disk access on the - * main/UI thread). + * Sets the {@link ImageCache} object to use with this ImageWorker. Usually you will not need to + * call this directly, instead use {@link ImageWorker#addImageCache} which will create and add + * the {@link ImageCache} object in a background thread (to ensure no disk access on the main/UI + * thread). * * @param imageCache */ @@ -156,14 +168,16 @@ public void setExitTasksEarly(boolean exitTasksEarly) { * the final bitmap. This will be executed in a background thread and be long running. For * example, you could resize a large bitmap here, or pull down an image from the network. * - * @param data The data to identify which image to process, as provided by - * {@link ImageWorker#loadImage(Object, ImageView)} + * @param data The data to identify which image to process, as provided by {@link + * ImageWorker#loadImage(Object, ImageView)} + * * @return The processed bitmap */ protected abstract Bitmap processBitmap(Object data); /** * Cancels any pending work attached to the provided ImageView. + * * @param imageView */ public static void cancelWork(ImageView imageView) { @@ -202,8 +216,9 @@ public static boolean cancelPotentialWork(Object data, ImageView imageView) { /** * @param imageView Any imageView - * @return Retrieve the currently active work task (if any) associated with this imageView. - * null if there is no such task. + * + * @return Retrieve the currently active work task (if any) associated with this imageView. null + * if there is no such task. */ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { @@ -220,7 +235,9 @@ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { * The actual AsyncTask that will asynchronously process the image. */ private class BitmapWorkerTask extends AsyncTask { + private Object data; + private final WeakReference imageViewReference; public BitmapWorkerTask(ImageView imageView) { @@ -245,7 +262,8 @@ protected Bitmap doInBackground(Object... params) { while (mPauseWork && !isCancelled()) { try { mPauseWorkLock.wait(); - } catch (InterruptedException e) {} + } catch (InterruptedException e) { + } } } @@ -332,12 +350,13 @@ private ImageView getAttachedImageView() { * independently of the finish order. */ private static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = - new WeakReference(bitmapWorkerTask); + new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { @@ -353,15 +372,12 @@ public BitmapWorkerTask getBitmapWorkerTask() { */ private void setImageBitmap(ImageView imageView, Bitmap bitmap) { if (mFadeInBitmap) { - // Transition drawable with a transparent drawable and the final bitmap + // Transition drawable between the pending image and the final bitmap. final TransitionDrawable td = - new TransitionDrawable(new Drawable[] { - new ColorDrawable(android.R.color.transparent), - new BitmapDrawable(mResources, bitmap) + new TransitionDrawable(new Drawable[]{ + imageView.getDrawable(), + new BitmapDrawable(mResources, bitmap) }); - // Set background to loading bitmap - imageView.setBackgroundDrawable( - new BitmapDrawable(mResources, mLoadingBitmap)); imageView.setImageDrawable(td); td.startTransition(FADE_IN_TIME); @@ -383,7 +399,7 @@ protected class CacheAsyncTask extends AsyncTask { @Override protected Void doInBackground(Object... params) { - switch ((Integer)params[0]) { + switch ((Integer) params[0]) { case MESSAGE_CLEAR: clearCacheInternal(); break; diff --git a/src/uk/org/ngo/squeezer/util/Intents.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Intents.java similarity index 72% rename from src/uk/org/ngo/squeezer/util/Intents.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/Intents.java index 765c54ea8..e1dff0663 100644 --- a/src/uk/org/ngo/squeezer/util/Intents.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Intents.java @@ -1,23 +1,24 @@ package uk.org.ngo.squeezer.util; -import java.util.List; - import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import java.util.List; + public class Intents { + /** - * Indicates whether the specified action can be used as an intent. This - * method queries the package manager for installed packages that can - * respond to an intent with the specified action. If no suitable package is - * found, this method returns false. - * + * Indicates whether the specified action can be used as an intent. This method queries the + * package manager for installed packages that can respond to an intent with the specified + * action. If no suitable package is found, this method returns false. + * * @param context The application's environment. * @param action The Intent action to check for availability. - * @return True if an Intent with the specified action can be sent and - * responded to, false otherwise. + * + * @return True if an Intent with the specified action can be sent and responded to, false + * otherwise. */ public static boolean isIntentAvailable(Context context, String action) { final PackageManager packageManager = context.getPackageManager(); @@ -29,15 +30,15 @@ public static boolean isIntentAvailable(Context context, String action) { } /** - * Indicates whether the specified action can be broadcast as an intent. - * This method queries the package manager for installed packages that can - * respond to a broadcats intent with the specified action. If no suitable - * package is found, this method returns false. - * + * Indicates whether the specified action can be broadcast as an intent. This method queries the + * package manager for installed packages that can respond to a broadcats intent with the + * specified action. If no suitable package is found, this method returns false. + * * @param context The application's environment. * @param action The Intent action to check for availability. - * @return True if an Intent with the specified action can be sent and - * responded to, false otherwise. + * + * @return True if an Intent with the specified action can be sent and responded to, false + * otherwise. */ public static boolean isBroadcastReceiverAvailable(Context context, String action) { final PackageManager packageManager = context.getPackageManager(); diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Reflection.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Reflection.java new file mode 100644 index 000000000..3bfa685ea --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Reflection.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2011 Kurt Aaholst + * + * 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 uk.org.ngo.squeezer.util; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +/** + * Reflection utility methods + * + * @author kaa + */ +public class Reflection { + + /** + *

    Return the actual type parameter of the supplied class for the type variable at the + * supplied position in the supplied base class or interface.

    The method returns null if the + * class can't be resolved. See {@link #genericTypeResolver(Class, Class)} for details on what + * can't be resolved, and how to work around it. + * + * @param currentClass The current class which must extend or implement base + * @param base Generic base class or interface which type variable we wish to resolve. + * @param genericArgumentNumber The position of the type variable we are interested in. + * + * @return The actual type parameter at the supplied position in base as a class or + * null. + * + * @see #genericTypeResolver(Class, Class) + */ + public static Class getGenericClass(Class currentClass, + Class base, int genericArgumentNumber) { + Type[] genericTypes = genericTypeResolver(currentClass, base); + Type type = genericArgumentNumber < genericTypes.length + ? genericTypes[genericArgumentNumber] : null; + + if (type instanceof Class) { + return (Class) type; + } + return null; + } + + /** + *

    Resolve actual type arguments of the supplied class for the type variables in the supplied + * generic base class or interface.

    If the types can't be resolved an empty array is + * returned.

    NOTE
    This will only resolve generic parameters when they are + * declared in the class, it wont resolve instances of generic types. So for example: + *

    +     * new LinkedList() cant'be resolved but
    +     * new LinkedList(){} can. (Notice the subclassing of the generic
    +     * collection)
    +     * 
    + * + * @param currentClass The current class which must extend or implement base + * @param base Generic base class or interface which type variables we wish to resolve. + * + * @return Actual type arguments for base used in currentClass. + * + * @see #getGenericClass(Class, Class, int) + */ + public static Type[] genericTypeResolver(Class currentClass, + Class base) { + Type[] actualTypeArguments = null; + + while (currentClass != Object.class) { + if (currentClass.isAssignableFrom(base)) { + return (actualTypeArguments == null ? currentClass.getTypeParameters() + : actualTypeArguments); + } + + if (base.isInterface()) { + Type[] actualTypes = genericInterfaceResolver(currentClass, base, + actualTypeArguments); + if (actualTypes != null) { + return actualTypes; + } + } + + actualTypeArguments = mapTypeArguments(currentClass, + currentClass.getGenericSuperclass(), actualTypeArguments); + currentClass = currentClass.getSuperclass(); + } + + return new Type[0]; + } + + + /** + * Resolve actual type arguments of the supplied class for the type variables in the supplied + * generic interface. + * + * @param currentClass The current class which may implement base + * @param baseInterface Generic interface which type variables we wish to resolve. + * @param actualTypeArguments Resolved type arguments from parent + * + * @return Actual type arguments for baseInterface used in + * currentClass or null. + * + * @see #getGenericClass(Class, Class, int) + */ + private static Type[] genericInterfaceResolver(Class currentClass, + Class baseInterface, Type[] pActualTypeArguments) { + Class[] interfaces = currentClass.getInterfaces(); + Type[] genericInterfaces = currentClass.getGenericInterfaces(); + for (int ifno = 0; ifno < genericInterfaces.length; ifno++) { + Type[] actualTypeArguments = mapTypeArguments(currentClass, genericInterfaces[ifno], + pActualTypeArguments); + + if (genericInterfaces[ifno] instanceof ParameterizedType) { + if (baseInterface + .equals(((ParameterizedType) genericInterfaces[ifno]).getRawType())) { + return actualTypeArguments; + } + } + + Type[] resolvedTypes = genericInterfaceResolver(interfaces[ifno], baseInterface, + actualTypeArguments); + if (resolvedTypes != null) { + return resolvedTypes; + } + } + + return null; + } + + /** + * Map the resolved type arguments of the given class to the type parameters of the supplied + * superclass or direct interface. + * + * @param currentClass The class with the supplied resolved type arguments + * @param type Superclass or direct interface of currentClass + * @param actualTypeArguments Resolved type arguments of of currentClass + * + * @return The resolved type arguments mapped to the given superclass or direct interface + */ + private static Type[] mapTypeArguments(Class currentClass, Type type, + Type[] actualTypeArguments) { + if (type instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) type; + + if (actualTypeArguments == null) { + return pType.getActualTypeArguments(); + } + + TypeVariable[] typeParameters = currentClass.getTypeParameters(); + Type[] actualTypes = pType.getActualTypeArguments(); + Type[] newActualTypeArguments = new Type[actualTypes.length]; + for (int i = 0; i < actualTypes.length; i++) { + newActualTypeArguments[i] = null; + for (int j = 0; j < typeParameters.length; j++) { + if (actualTypes[i].equals(typeParameters[j])) { + newActualTypeArguments[i] = actualTypeArguments[j]; + break; + } + } + } + return newActualTypeArguments; + + } else { + return null; + } + } + +} diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/util/RetainFragment.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/RetainFragment.java new file mode 100644 index 000000000..7d8a558d5 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/RetainFragment.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013 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 uk.org.ngo.squeezer.util; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.util.Log; + +import java.util.Hashtable; +import java.util.Map; + +/** + * Provides a fragment that will be retained across the lifecycle of the activity that hosts it. + *

    + * Get an instance of this class by calling {@link #getInstance(String, FragmentManager)}, and place + * objects that should be persisted across the activity lifecycle using {@link #put(String, + * Object)}. Retrieve persisted objects with {@link #get(String)}. + */ +public class RetainFragment extends Fragment { + + private static final String TAG = RetainFragment.class.getName(); + + private Map mHash = new Hashtable(); + + /** + * Empty constructor as per the Fragment documentation + */ + public RetainFragment() { + } + + public static RetainFragment getInstance(String tag, FragmentManager fm) { + Log.d(TAG, "getInstance() for " + tag); + RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(tag); + if (fragment == null) { + Log.d(TAG, " Creating new instance"); + fragment = new RetainFragment(); + fm.beginTransaction().add(fragment, tag).commitAllowingStateLoss(); + } + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Make sure this Fragment is retained over a configuration change + setRetainInstance(true); + } + + public Object put(String key, Object value) { + mHash.put(key, value); + return value; + } + + public Object get(String key) { + return mHash.get(key); + } +} diff --git a/src/uk/org/ngo/squeezer/util/Scrobble.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Scrobble.java similarity index 99% rename from src/uk/org/ngo/squeezer/util/Scrobble.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/Scrobble.java index e5c5fcb31..47ab5dcb6 100644 --- a/src/uk/org/ngo/squeezer/util/Scrobble.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/Scrobble.java @@ -3,6 +3,7 @@ import uk.org.ngo.squeezer.Squeezer; public class Scrobble { + public static boolean haveScrobbleDroid() { return Intents.isBroadcastReceiverAvailable( Squeezer.getContext(), diff --git a/src/uk/org/ngo/squeezer/util/UIUtils.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/UIUtils.java similarity index 99% rename from src/uk/org/ngo/squeezer/util/UIUtils.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/util/UIUtils.java index 10ca92251..ea0f67dcd 100644 --- a/src/uk/org/ngo/squeezer/util/UIUtils.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/util/UIUtils.java @@ -26,6 +26,7 @@ * An assortment of UI helpers. */ public class UIUtils { + @TargetApi(Build.VERSION_CODES.HONEYCOMB) public static void setActivatedCompat(View view, boolean activated) { if (hasHoneycomb()) { diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/ListItemImageButton.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/ListItemImageButton.java new file mode 100644 index 000000000..84aae5f63 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/ListItemImageButton.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; + +/** + * Display an {@link android.widget.ImageButton} with customised behaviour suitable for use in a + * {@link android.view.ViewGroup}, such as a list item. + *

    + * The custom behaviour is invoked in {@link #setPressed(boolean)}. Default behaviour is that if any + * parent views in the ViewGroup are pressed then the button also appears pressed. + *

    + * This has the side effect of making the button's pressed state drawable be overlaid over the + * parent view's pressed state drawable, appearing to be too bright. + *

    + * This is especially apparent if the user long-presses on the ViewGroup that makes up a row in a + * list. + *

    + * This class checks to see if any parent views are pressed, and if they are, ignores the press on + * this view. + *

    + * See Cyril Mottier's discussion of this, and code, in http://android.cyrilmottier.com/?p=525. + */ +public class ListItemImageButton extends ImageButton { + + public ListItemImageButton(Context context) { + super(context); + } + + public ListItemImageButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ListItemImageButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setPressed(boolean pressed) { + if (pressed && getParent() instanceof View && ((View) getParent()).isPressed()) { + return; + } + super.setPressed(pressed); + } +} diff --git a/src/uk/org/ngo/squeezer/RepeatingImageButton.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/RepeatingImageButton.java similarity index 79% rename from src/uk/org/ngo/squeezer/RepeatingImageButton.java rename to Squeezer/src/main/java/uk/org/ngo/squeezer/widget/RepeatingImageButton.java index 3a13ac575..6f5698802 100644 --- a/src/uk/org/ngo/squeezer/RepeatingImageButton.java +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/RepeatingImageButton.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package uk.org.ngo.squeezer; +package uk.org.ngo.squeezer.widget; import android.content.Context; import android.os.SystemClock; @@ -25,14 +25,16 @@ import android.widget.ImageButton; /** - * A button that will repeatedly call a 'listener' method - * as long as the button is pressed. + * A button that will repeatedly call a 'listener' method as long as the button is pressed. */ public class RepeatingImageButton extends ImageButton { private long mStartTime; + private int mRepeatCount; + private RepeatListener mListener; + private long mInterval = 500; public RepeatingImageButton(Context context) { @@ -50,8 +52,9 @@ public RepeatingImageButton(Context context, AttributeSet attrs, int defStyle) { } /** - * Sets the listener to be called while the button is pressed and - * the interval in milliseconds with which it will be called. + * Sets the listener to be called while the button is pressed and the interval in milliseconds + * with which it will be called. + * * @param l The listener that will be called * @param interval The interval in milliseconds for calls */ @@ -84,14 +87,14 @@ public boolean onTouchEvent(MotionEvent event) { @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - // remove the repeater, but call the hook one more time - removeCallbacks(mRepeater); - if (mStartTime != 0) { - doRepeat(true); - mStartTime = 0; - } + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + // remove the repeater, but call the hook one more time + removeCallbacks(mRepeater); + if (mStartTime != 0) { + doRepeat(true); + mStartTime = 0; + } } return super.onKeyUp(keyCode, event); } @@ -105,7 +108,7 @@ public void run() { } }; - private void doRepeat(boolean last) { + private void doRepeat(boolean last) { long now = SystemClock.elapsedRealtime(); if (mListener != null) { mListener.onRepeat(this, now - mStartTime, last ? -1 : mRepeatCount++); @@ -113,15 +116,16 @@ private void doRepeat(boolean last) { } public interface RepeatListener { + /** - * This method will be called repeatedly at roughly the interval - * specified in setRepeatListener(), for as long as the button - * is pressed. + * This method will be called repeatedly at roughly the interval specified in + * setRepeatListener(), for as long as the button is pressed. + * * @param v The button as a View. * @param duration The number of milliseconds the button has been pressed so far. - * @param repeatcount The number of previous calls in this sequence. - * If this is going to be the last call in this sequence (i.e. the user - * just stopped pressing the button), the value will be -1. + * @param repeatcount The number of previous calls in this sequence. If this is going to be + * the last call in this sequence (i.e. the user just stopped pressing the button), the + * value will be -1. */ void onRepeat(View v, long duration, int repeatcount); } diff --git a/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/SquareImageView.java b/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/SquareImageView.java new file mode 100644 index 000000000..160869fd5 --- /dev/null +++ b/Squeezer/src/main/java/uk/org/ngo/squeezer/widget/SquareImageView.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012 Google Inc. All Rights Reserved. + * + * 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 uk.org.ngo.squeezer.widget; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Sets both view dimensions to whichever of height and width are measured as being smaller, + * resulting in a square image. + */ +public class SquareImageView extends ImageView { + + private boolean mBlockLayout; + + public SquareImageView(Context context) { + super(context); + } + + public SquareImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight(); + int heightWithoutPadding = height - getPaddingTop() - getPaddingBottom(); + + int size = Math.min(heightWithoutPadding, widthWithoutPadding); + + setMeasuredDimension(size + getPaddingLeft() + getPaddingRight(), + size + getPaddingTop() + getPaddingBottom()); + } + + @Override + public void setImageDrawable(Drawable drawable) { + mBlockLayout = true; + super.setImageDrawable(drawable); + mBlockLayout = false; + } + + @Override + public void requestLayout() { + if (!mBlockLayout) { + super.requestLayout(); + } + } +} diff --git a/res/anim/fade_in.xml b/Squeezer/src/main/res/anim/fade_in.xml similarity index 93% rename from res/anim/fade_in.xml rename to Squeezer/src/main/res/anim/fade_in.xml index 67e63e680..968b597f8 100644 --- a/res/anim/fade_in.xml +++ b/Squeezer/src/main/res/anim/fade_in.xml @@ -20,7 +20,7 @@ limitations under the License. + android:duration="500"/> - \ No newline at end of file + diff --git a/Squeezer/src/main/res/anim/slide_in_right.xml b/Squeezer/src/main/res/anim/slide_in_right.xml new file mode 100644 index 000000000..cfaeb9e32 --- /dev/null +++ b/Squeezer/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/Squeezer/src/main/res/anim/slide_out_left.xml b/Squeezer/src/main/res/anim/slide_out_left.xml new file mode 100644 index 000000000..c0621f58a --- /dev/null +++ b/Squeezer/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/Squeezer/src/main/res/drawable-hdpi/actionbar_shadow.9.png b/Squeezer/src/main/res/drawable-hdpi/actionbar_shadow.9.png new file mode 100644 index 000000000..3c80a3fca Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/actionbar_shadow.9.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/btn_repeat_all.png b/Squeezer/src/main/res/drawable-hdpi/btn_repeat_all.png new file mode 100644 index 000000000..8c241dae8 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/btn_repeat_all.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/btn_repeat_off.png b/Squeezer/src/main/res/drawable-hdpi/btn_repeat_off.png new file mode 100644 index 000000000..33911f28d Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/btn_repeat_off.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/btn_repeat_one.png b/Squeezer/src/main/res/drawable-hdpi/btn_repeat_one.png new file mode 100644 index 000000000..c67830413 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/btn_repeat_one.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_album.png b/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_album.png new file mode 100644 index 000000000..8d9cb8dc0 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_album.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_off.png b/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_off.png new file mode 100644 index 000000000..bd7a7a05d Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_off.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_song.png b/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_song.png new file mode 100644 index 000000000..194911d4d Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/btn_shuffle_song.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png b/Squeezer/src/main/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png new file mode 100644 index 000000000..06e5b4730 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/dropdown_ic_arrow_normal_holo_dark.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_disconnect.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_disconnect.png new file mode 100644 index 000000000..fc4c60a12 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_disconnect.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_filter.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_filter.png new file mode 100644 index 000000000..e02bde299 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_filter.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_home.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_home.png new file mode 100644 index 000000000..351f73bd5 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_home.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_next.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_next.png new file mode 100644 index 000000000..71076e467 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_next.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_now_playing.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_now_playing.png new file mode 100644 index 000000000..f88b0a294 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_now_playing.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_overflow.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_overflow.png new file mode 100644 index 000000000..fcbfe06ae Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_overflow.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_pause.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_pause.png new file mode 100644 index 000000000..535ad58b3 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_pause.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_play.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_play.png new file mode 100644 index 000000000..e22b0997f Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_play.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_add.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_add.png new file mode 100644 index 000000000..b80d8d32a Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_add.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_clear.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_clear.png new file mode 100644 index 000000000..492c6e072 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_clear.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_save.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_save.png new file mode 100644 index 000000000..36389f4f4 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_playlist_save.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_power.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_power.png new file mode 100644 index 000000000..7dd355700 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_power.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_preferences.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_preferences.png new file mode 100644 index 000000000..13b80e1f6 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_preferences.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_previous.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_previous.png new file mode 100644 index 000000000..c766ecea5 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_previous.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_sort.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_sort.png new file mode 100644 index 000000000..98b133c61 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_sort.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_view_as_grid.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_view_as_grid.png new file mode 100644 index 000000000..072d16cb7 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_view_as_grid.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_action_view_as_list.png b/Squeezer/src/main/res/drawable-hdpi/ic_action_view_as_list.png new file mode 100644 index 000000000..8e3babc7f Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_action_view_as_list.png differ diff --git a/res/drawable-hdpi/ic_albums.png b/Squeezer/src/main/res/drawable-hdpi/ic_albums.png similarity index 100% rename from res/drawable-hdpi/ic_albums.png rename to Squeezer/src/main/res/drawable-hdpi/ic_albums.png diff --git a/res/drawable-hdpi/ic_artists.png b/Squeezer/src/main/res/drawable-hdpi/ic_artists.png similarity index 100% rename from res/drawable-hdpi/ic_artists.png rename to Squeezer/src/main/res/drawable-hdpi/ic_artists.png diff --git a/res/drawable-hdpi/ic_favorites.png b/Squeezer/src/main/res/drawable-hdpi/ic_favorites.png similarity index 100% rename from res/drawable-hdpi/ic_favorites.png rename to Squeezer/src/main/res/drawable-hdpi/ic_favorites.png diff --git a/res/drawable-hdpi/ic_genres.png b/Squeezer/src/main/res/drawable-hdpi/ic_genres.png similarity index 100% rename from res/drawable-hdpi/ic_genres.png rename to Squeezer/src/main/res/drawable-hdpi/ic_genres.png diff --git a/res/drawable-hdpi/ic_internet_radio.png b/Squeezer/src/main/res/drawable-hdpi/ic_internet_radio.png similarity index 100% rename from res/drawable-hdpi/ic_internet_radio.png rename to Squeezer/src/main/res/drawable-hdpi/ic_internet_radio.png diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_launcher.png b/Squeezer/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..1ae86fbc0 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/res/drawable-hdpi/ic_launcher_lastfm.png b/Squeezer/src/main/res/drawable-hdpi/ic_launcher_lastfm.png similarity index 100% rename from res/drawable-hdpi/ic_launcher_lastfm.png rename to Squeezer/src/main/res/drawable-hdpi/ic_launcher_lastfm.png diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_menu_choose_player.png b/Squeezer/src/main/res/drawable-hdpi/ic_menu_choose_player.png new file mode 100644 index 000000000..222a8d579 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_menu_choose_player.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_menu_playlist.png b/Squeezer/src/main/res/drawable-hdpi/ic_menu_playlist.png new file mode 100644 index 000000000..e45ea1fd9 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_menu_playlist.png differ diff --git a/res/drawable-hdpi/ic_menu_save.png b/Squeezer/src/main/res/drawable-hdpi/ic_menu_save.png similarity index 100% rename from res/drawable-hdpi/ic_menu_save.png rename to Squeezer/src/main/res/drawable-hdpi/ic_menu_save.png diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_menu_search.png b/Squeezer/src/main/res/drawable-hdpi/ic_menu_search.png new file mode 100644 index 000000000..f12e005eb Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_menu_search.png differ diff --git a/res/drawable-hdpi/ic_music_folder.png b/Squeezer/src/main/res/drawable-hdpi/ic_music_folder.png similarity index 100% rename from res/drawable-hdpi/ic_music_folder.png rename to Squeezer/src/main/res/drawable-hdpi/ic_music_folder.png diff --git a/res/drawable-hdpi/ic_my_apps.png b/Squeezer/src/main/res/drawable-hdpi/ic_my_apps.png similarity index 100% rename from res/drawable-hdpi/ic_my_apps.png rename to Squeezer/src/main/res/drawable-hdpi/ic_my_apps.png diff --git a/res/drawable-hdpi/ic_my_music.png b/Squeezer/src/main/res/drawable-hdpi/ic_my_music.png similarity index 100% rename from res/drawable-hdpi/ic_my_music.png rename to Squeezer/src/main/res/drawable-hdpi/ic_my_music.png diff --git a/res/drawable-hdpi/ic_new_music.png b/Squeezer/src/main/res/drawable-hdpi/ic_new_music.png similarity index 100% rename from res/drawable-hdpi/ic_new_music.png rename to Squeezer/src/main/res/drawable-hdpi/ic_new_music.png diff --git a/res/drawable-hdpi/ic_now_playing.png b/Squeezer/src/main/res/drawable-hdpi/ic_now_playing.png similarity index 100% rename from res/drawable-hdpi/ic_now_playing.png rename to Squeezer/src/main/res/drawable-hdpi/ic_now_playing.png diff --git a/res/drawable-hdpi/ic_playlists.png b/Squeezer/src/main/res/drawable-hdpi/ic_playlists.png similarity index 100% rename from res/drawable-hdpi/ic_playlists.png rename to Squeezer/src/main/res/drawable-hdpi/ic_playlists.png diff --git a/res/drawable-hdpi/ic_random.png b/Squeezer/src/main/res/drawable-hdpi/ic_random.png similarity index 100% rename from res/drawable-hdpi/ic_random.png rename to Squeezer/src/main/res/drawable-hdpi/ic_random.png diff --git a/res/drawable-hdpi/ic_search.png b/Squeezer/src/main/res/drawable-hdpi/ic_search.png similarity index 100% rename from res/drawable-hdpi/ic_search.png rename to Squeezer/src/main/res/drawable-hdpi/ic_search.png diff --git a/res/drawable-hdpi/ic_songs.png b/Squeezer/src/main/res/drawable-hdpi/ic_songs.png similarity index 100% rename from res/drawable-hdpi/ic_songs.png rename to Squeezer/src/main/res/drawable-hdpi/ic_songs.png diff --git a/Squeezer/src/main/res/drawable-hdpi/ic_unknown.png b/Squeezer/src/main/res/drawable-hdpi/ic_unknown.png new file mode 100644 index 000000000..22963fcd7 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/ic_unknown.png differ diff --git a/res/drawable-hdpi/ic_volume.png b/Squeezer/src/main/res/drawable-hdpi/ic_volume.png similarity index 100% rename from res/drawable-hdpi/ic_volume.png rename to Squeezer/src/main/res/drawable-hdpi/ic_volume.png diff --git a/res/drawable-hdpi/ic_volume_off.png b/Squeezer/src/main/res/drawable-hdpi/ic_volume_off.png similarity index 100% rename from res/drawable-hdpi/ic_volume_off.png rename to Squeezer/src/main/res/drawable-hdpi/ic_volume_off.png diff --git a/res/drawable-hdpi/ic_years.png b/Squeezer/src/main/res/drawable-hdpi/ic_years.png similarity index 100% rename from res/drawable-hdpi/ic_years.png rename to Squeezer/src/main/res/drawable-hdpi/ic_years.png diff --git a/Squeezer/src/main/res/drawable-hdpi/icon_album_noart.png b/Squeezer/src/main/res/drawable-hdpi/icon_album_noart.png new file mode 100644 index 000000000..c81204338 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/icon_album_noart.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/list_activated_holo.9.png b/Squeezer/src/main/res/drawable-hdpi/list_activated_holo.9.png new file mode 100644 index 000000000..4ea7afa00 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/list_activated_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/list_focused_holo.9.png b/Squeezer/src/main/res/drawable-hdpi/list_focused_holo.9.png new file mode 100644 index 000000000..555270842 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/list_focused_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/list_longpressed_holo.9.png b/Squeezer/src/main/res/drawable-hdpi/list_longpressed_holo.9.png new file mode 100644 index 000000000..4ea7afa00 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/list_longpressed_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/list_pressed_holo_dark.9.png b/Squeezer/src/main/res/drawable-hdpi/list_pressed_holo_dark.9.png new file mode 100644 index 000000000..5654cd694 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/list_pressed_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/list_selector_disabled_holo_dark.9.png b/Squeezer/src/main/res/drawable-hdpi/list_selector_disabled_holo_dark.9.png new file mode 100644 index 000000000..f6fd30dcd Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/list_selector_disabled_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-hdpi/list_selector_pressed_holo_dark.9.png b/Squeezer/src/main/res/drawable-hdpi/list_selector_pressed_holo_dark.9.png new file mode 100644 index 000000000..0ed5ba358 Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/list_selector_pressed_holo_dark.9.png differ diff --git a/res/drawable-hdpi/panel_background.9.png b/Squeezer/src/main/res/drawable-hdpi/panel_background.9.png similarity index 100% rename from res/drawable-hdpi/panel_background.9.png rename to Squeezer/src/main/res/drawable-hdpi/panel_background.9.png diff --git a/Squeezer/src/main/res/drawable-hdpi/presence_online.png b/Squeezer/src/main/res/drawable-hdpi/presence_online.png new file mode 100644 index 000000000..76944469c Binary files /dev/null and b/Squeezer/src/main/res/drawable-hdpi/presence_online.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_disconnect.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_disconnect.png new file mode 100644 index 000000000..704b1588d Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_disconnect.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_filter.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_filter.png new file mode 100644 index 000000000..d04e83853 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_filter.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_home.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_home.png new file mode 100644 index 000000000..2578f5151 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_home.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_now_playing.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_now_playing.png new file mode 100644 index 000000000..906bbe18f Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_now_playing.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_overflow.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_overflow.png new file mode 100644 index 000000000..4fd25167e Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_overflow.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_add.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_add.png new file mode 100644 index 000000000..5de50664f Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_add.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_clear.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_clear.png new file mode 100644 index 000000000..ceb18ab87 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_clear.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_save.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_save.png new file mode 100644 index 000000000..1b357c791 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_playlist_save.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_power.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_power.png new file mode 100644 index 000000000..3df70b6f4 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_power.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_preferences.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_preferences.png new file mode 100644 index 000000000..e5f53d2db Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_preferences.png differ diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_action_sort.png b/Squeezer/src/main/res/drawable-ldpi/ic_action_sort.png new file mode 100644 index 000000000..36e9ebf82 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_action_sort.png differ diff --git a/res/drawable-ldpi/ic_albums.png b/Squeezer/src/main/res/drawable-ldpi/ic_albums.png similarity index 100% rename from res/drawable-ldpi/ic_albums.png rename to Squeezer/src/main/res/drawable-ldpi/ic_albums.png diff --git a/res/drawable-ldpi/ic_artists.png b/Squeezer/src/main/res/drawable-ldpi/ic_artists.png similarity index 100% rename from res/drawable-ldpi/ic_artists.png rename to Squeezer/src/main/res/drawable-ldpi/ic_artists.png diff --git a/res/drawable-ldpi/ic_favorites.png b/Squeezer/src/main/res/drawable-ldpi/ic_favorites.png similarity index 100% rename from res/drawable-ldpi/ic_favorites.png rename to Squeezer/src/main/res/drawable-ldpi/ic_favorites.png diff --git a/res/drawable-ldpi/ic_genres.png b/Squeezer/src/main/res/drawable-ldpi/ic_genres.png similarity index 100% rename from res/drawable-ldpi/ic_genres.png rename to Squeezer/src/main/res/drawable-ldpi/ic_genres.png diff --git a/res/drawable-ldpi/ic_internet_radio.png b/Squeezer/src/main/res/drawable-ldpi/ic_internet_radio.png similarity index 100% rename from res/drawable-ldpi/ic_internet_radio.png rename to Squeezer/src/main/res/drawable-ldpi/ic_internet_radio.png diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_launcher.png b/Squeezer/src/main/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..db4901166 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_launcher.png differ diff --git a/res/drawable-ldpi/ic_launcher_lastfm.png b/Squeezer/src/main/res/drawable-ldpi/ic_launcher_lastfm.png similarity index 100% rename from res/drawable-ldpi/ic_launcher_lastfm.png rename to Squeezer/src/main/res/drawable-ldpi/ic_launcher_lastfm.png diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_menu_choose_player.png b/Squeezer/src/main/res/drawable-ldpi/ic_menu_choose_player.png new file mode 100644 index 000000000..077964d80 Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_menu_choose_player.png differ diff --git a/res/drawable-ldpi/ic_music_folder.png b/Squeezer/src/main/res/drawable-ldpi/ic_music_folder.png similarity index 100% rename from res/drawable-ldpi/ic_music_folder.png rename to Squeezer/src/main/res/drawable-ldpi/ic_music_folder.png diff --git a/res/drawable-ldpi/ic_my_apps.png b/Squeezer/src/main/res/drawable-ldpi/ic_my_apps.png similarity index 100% rename from res/drawable-ldpi/ic_my_apps.png rename to Squeezer/src/main/res/drawable-ldpi/ic_my_apps.png diff --git a/res/drawable-ldpi/ic_my_music.png b/Squeezer/src/main/res/drawable-ldpi/ic_my_music.png similarity index 100% rename from res/drawable-ldpi/ic_my_music.png rename to Squeezer/src/main/res/drawable-ldpi/ic_my_music.png diff --git a/res/drawable-ldpi/ic_new_music.png b/Squeezer/src/main/res/drawable-ldpi/ic_new_music.png similarity index 100% rename from res/drawable-ldpi/ic_new_music.png rename to Squeezer/src/main/res/drawable-ldpi/ic_new_music.png diff --git a/res/drawable-ldpi/ic_now_playing.png b/Squeezer/src/main/res/drawable-ldpi/ic_now_playing.png similarity index 100% rename from res/drawable-ldpi/ic_now_playing.png rename to Squeezer/src/main/res/drawable-ldpi/ic_now_playing.png diff --git a/res/drawable-ldpi/ic_playlists.png b/Squeezer/src/main/res/drawable-ldpi/ic_playlists.png similarity index 100% rename from res/drawable-ldpi/ic_playlists.png rename to Squeezer/src/main/res/drawable-ldpi/ic_playlists.png diff --git a/res/drawable-ldpi/ic_random.png b/Squeezer/src/main/res/drawable-ldpi/ic_random.png similarity index 100% rename from res/drawable-ldpi/ic_random.png rename to Squeezer/src/main/res/drawable-ldpi/ic_random.png diff --git a/res/drawable-ldpi/ic_search.png b/Squeezer/src/main/res/drawable-ldpi/ic_search.png similarity index 100% rename from res/drawable-ldpi/ic_search.png rename to Squeezer/src/main/res/drawable-ldpi/ic_search.png diff --git a/res/drawable-ldpi/ic_songs.png b/Squeezer/src/main/res/drawable-ldpi/ic_songs.png similarity index 100% rename from res/drawable-ldpi/ic_songs.png rename to Squeezer/src/main/res/drawable-ldpi/ic_songs.png diff --git a/Squeezer/src/main/res/drawable-ldpi/ic_unknown.png b/Squeezer/src/main/res/drawable-ldpi/ic_unknown.png new file mode 100644 index 000000000..247100a7f Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/ic_unknown.png differ diff --git a/res/drawable-ldpi/ic_years.png b/Squeezer/src/main/res/drawable-ldpi/ic_years.png similarity index 100% rename from res/drawable-ldpi/ic_years.png rename to Squeezer/src/main/res/drawable-ldpi/ic_years.png diff --git a/res/drawable-ldpi/icon_album_noart.png b/Squeezer/src/main/res/drawable-ldpi/icon_album_noart.png similarity index 100% rename from res/drawable-ldpi/icon_album_noart.png rename to Squeezer/src/main/res/drawable-ldpi/icon_album_noart.png diff --git a/res/drawable-ldpi/panel_background.9.png b/Squeezer/src/main/res/drawable-ldpi/panel_background.9.png similarity index 100% rename from res/drawable-ldpi/panel_background.9.png rename to Squeezer/src/main/res/drawable-ldpi/panel_background.9.png diff --git a/Squeezer/src/main/res/drawable-ldpi/presence_online.png b/Squeezer/src/main/res/drawable-ldpi/presence_online.png new file mode 100644 index 000000000..e16ec810a Binary files /dev/null and b/Squeezer/src/main/res/drawable-ldpi/presence_online.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/actionbar_shadow.9.png b/Squeezer/src/main/res/drawable-mdpi/actionbar_shadow.9.png new file mode 100644 index 000000000..cae1778f4 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/actionbar_shadow.9.png differ diff --git a/res/drawable-mdpi/album_border_large.9.png b/Squeezer/src/main/res/drawable-mdpi/album_border_large.9.png similarity index 100% rename from res/drawable-mdpi/album_border_large.9.png rename to Squeezer/src/main/res/drawable-mdpi/album_border_large.9.png diff --git a/Squeezer/src/main/res/drawable-mdpi/btn_repeat_all.png b/Squeezer/src/main/res/drawable-mdpi/btn_repeat_all.png new file mode 100644 index 000000000..b528e54ae Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/btn_repeat_all.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/btn_repeat_off.png b/Squeezer/src/main/res/drawable-mdpi/btn_repeat_off.png new file mode 100644 index 000000000..65d8ce539 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/btn_repeat_off.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/btn_repeat_one.png b/Squeezer/src/main/res/drawable-mdpi/btn_repeat_one.png new file mode 100644 index 000000000..c5d16591c Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/btn_repeat_one.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_album.png b/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_album.png new file mode 100644 index 000000000..cfd6eb21c Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_album.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_off.png b/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_off.png new file mode 100644 index 000000000..b55289e9e Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_off.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_song.png b/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_song.png new file mode 100644 index 000000000..a0d6e6936 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/btn_shuffle_song.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png b/Squeezer/src/main/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png new file mode 100644 index 000000000..81de1bb46 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/dropdown_ic_arrow_normal_holo_dark.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_disconnect.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_disconnect.png new file mode 100644 index 000000000..3f054706f Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_disconnect.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_filter.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_filter.png new file mode 100644 index 000000000..3270530ca Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_filter.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_home.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_home.png new file mode 100644 index 000000000..6af00487a Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_home.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_next.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_next.png new file mode 100644 index 000000000..5c6699670 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_next.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_now_playing.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_now_playing.png new file mode 100644 index 000000000..4e527a37c Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_now_playing.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_overflow.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_overflow.png new file mode 100644 index 000000000..c755d2edb Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_overflow.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_pause.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_pause.png new file mode 100644 index 000000000..95a39ff2d Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_pause.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_play.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_play.png new file mode 100644 index 000000000..b94fbe6e3 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_play.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_add.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_add.png new file mode 100644 index 000000000..7a7986f84 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_add.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_clear.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_clear.png new file mode 100644 index 000000000..9f9da8450 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_clear.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_save.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_save.png new file mode 100644 index 000000000..b6da8edaa Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_playlist_save.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_power.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_power.png new file mode 100644 index 000000000..54cbc06b4 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_power.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_preferences.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_preferences.png new file mode 100644 index 000000000..1fbfb04c3 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_preferences.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_previous.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_previous.png new file mode 100644 index 000000000..c84e6b1d3 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_previous.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_sort.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_sort.png new file mode 100644 index 000000000..906216329 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_sort.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_view_as_grid.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_view_as_grid.png new file mode 100644 index 000000000..26c642b97 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_view_as_grid.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_action_view_as_list.png b/Squeezer/src/main/res/drawable-mdpi/ic_action_view_as_list.png new file mode 100644 index 000000000..ebf00427a Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_action_view_as_list.png differ diff --git a/res/drawable-mdpi/ic_albums.png b/Squeezer/src/main/res/drawable-mdpi/ic_albums.png similarity index 100% rename from res/drawable-mdpi/ic_albums.png rename to Squeezer/src/main/res/drawable-mdpi/ic_albums.png diff --git a/res/drawable-mdpi/ic_artists.png b/Squeezer/src/main/res/drawable-mdpi/ic_artists.png similarity index 100% rename from res/drawable-mdpi/ic_artists.png rename to Squeezer/src/main/res/drawable-mdpi/ic_artists.png diff --git a/res/drawable-mdpi/ic_favorites.png b/Squeezer/src/main/res/drawable-mdpi/ic_favorites.png similarity index 100% rename from res/drawable-mdpi/ic_favorites.png rename to Squeezer/src/main/res/drawable-mdpi/ic_favorites.png diff --git a/res/drawable-mdpi/ic_genres.png b/Squeezer/src/main/res/drawable-mdpi/ic_genres.png similarity index 100% rename from res/drawable-mdpi/ic_genres.png rename to Squeezer/src/main/res/drawable-mdpi/ic_genres.png diff --git a/res/drawable-mdpi/ic_internet_radio.png b/Squeezer/src/main/res/drawable-mdpi/ic_internet_radio.png similarity index 100% rename from res/drawable-mdpi/ic_internet_radio.png rename to Squeezer/src/main/res/drawable-mdpi/ic_internet_radio.png diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_launcher.png b/Squeezer/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..1407150f9 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/res/drawable-mdpi/ic_launcher_lastfm.png b/Squeezer/src/main/res/drawable-mdpi/ic_launcher_lastfm.png similarity index 100% rename from res/drawable-mdpi/ic_launcher_lastfm.png rename to Squeezer/src/main/res/drawable-mdpi/ic_launcher_lastfm.png diff --git a/res/drawable-mdpi/ic_launcher_scrobbledroid.png b/Squeezer/src/main/res/drawable-mdpi/ic_launcher_scrobbledroid.png similarity index 100% rename from res/drawable-mdpi/ic_launcher_scrobbledroid.png rename to Squeezer/src/main/res/drawable-mdpi/ic_launcher_scrobbledroid.png diff --git a/res/drawable-mdpi/ic_launcher_sls.png b/Squeezer/src/main/res/drawable-mdpi/ic_launcher_sls.png similarity index 100% rename from res/drawable-mdpi/ic_launcher_sls.png rename to Squeezer/src/main/res/drawable-mdpi/ic_launcher_sls.png diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_menu_choose_player.png b/Squeezer/src/main/res/drawable-mdpi/ic_menu_choose_player.png new file mode 100644 index 000000000..890d944b4 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_menu_choose_player.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_menu_playlist.png b/Squeezer/src/main/res/drawable-mdpi/ic_menu_playlist.png new file mode 100644 index 000000000..9c748b0b5 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_menu_playlist.png differ diff --git a/res/drawable-mdpi/ic_menu_save.png b/Squeezer/src/main/res/drawable-mdpi/ic_menu_save.png similarity index 100% rename from res/drawable-mdpi/ic_menu_save.png rename to Squeezer/src/main/res/drawable-mdpi/ic_menu_save.png diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_menu_search.png b/Squeezer/src/main/res/drawable-mdpi/ic_menu_search.png new file mode 100644 index 000000000..587d9e0bf Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_menu_search.png differ diff --git a/res/drawable-mdpi/ic_mp_album_playback.png b/Squeezer/src/main/res/drawable-mdpi/ic_mp_album_playback.png similarity index 100% rename from res/drawable-mdpi/ic_mp_album_playback.png rename to Squeezer/src/main/res/drawable-mdpi/ic_mp_album_playback.png diff --git a/res/drawable-mdpi/ic_mp_artist_playback.png b/Squeezer/src/main/res/drawable-mdpi/ic_mp_artist_playback.png similarity index 100% rename from res/drawable-mdpi/ic_mp_artist_playback.png rename to Squeezer/src/main/res/drawable-mdpi/ic_mp_artist_playback.png diff --git a/res/drawable-mdpi/ic_mp_song_playback.png b/Squeezer/src/main/res/drawable-mdpi/ic_mp_song_playback.png similarity index 100% rename from res/drawable-mdpi/ic_mp_song_playback.png rename to Squeezer/src/main/res/drawable-mdpi/ic_mp_song_playback.png diff --git a/res/drawable-mdpi/ic_music_folder.png b/Squeezer/src/main/res/drawable-mdpi/ic_music_folder.png similarity index 100% rename from res/drawable-mdpi/ic_music_folder.png rename to Squeezer/src/main/res/drawable-mdpi/ic_music_folder.png diff --git a/res/drawable-mdpi/ic_my_apps.png b/Squeezer/src/main/res/drawable-mdpi/ic_my_apps.png similarity index 100% rename from res/drawable-mdpi/ic_my_apps.png rename to Squeezer/src/main/res/drawable-mdpi/ic_my_apps.png diff --git a/res/drawable-mdpi/ic_my_music.png b/Squeezer/src/main/res/drawable-mdpi/ic_my_music.png similarity index 100% rename from res/drawable-mdpi/ic_my_music.png rename to Squeezer/src/main/res/drawable-mdpi/ic_my_music.png diff --git a/res/drawable-mdpi/ic_new_music.png b/Squeezer/src/main/res/drawable-mdpi/ic_new_music.png similarity index 100% rename from res/drawable-mdpi/ic_new_music.png rename to Squeezer/src/main/res/drawable-mdpi/ic_new_music.png diff --git a/res/drawable-mdpi/ic_now_playing.png b/Squeezer/src/main/res/drawable-mdpi/ic_now_playing.png similarity index 100% rename from res/drawable-mdpi/ic_now_playing.png rename to Squeezer/src/main/res/drawable-mdpi/ic_now_playing.png diff --git a/res/drawable-mdpi/ic_playlists.png b/Squeezer/src/main/res/drawable-mdpi/ic_playlists.png similarity index 100% rename from res/drawable-mdpi/ic_playlists.png rename to Squeezer/src/main/res/drawable-mdpi/ic_playlists.png diff --git a/res/drawable-mdpi/ic_random.png b/Squeezer/src/main/res/drawable-mdpi/ic_random.png similarity index 100% rename from res/drawable-mdpi/ic_random.png rename to Squeezer/src/main/res/drawable-mdpi/ic_random.png diff --git a/res/drawable-mdpi/ic_search.png b/Squeezer/src/main/res/drawable-mdpi/ic_search.png similarity index 100% rename from res/drawable-mdpi/ic_search.png rename to Squeezer/src/main/res/drawable-mdpi/ic_search.png diff --git a/res/drawable-mdpi/ic_songs.png b/Squeezer/src/main/res/drawable-mdpi/ic_songs.png similarity index 100% rename from res/drawable-mdpi/ic_songs.png rename to Squeezer/src/main/res/drawable-mdpi/ic_songs.png diff --git a/Squeezer/src/main/res/drawable-mdpi/ic_unknown.png b/Squeezer/src/main/res/drawable-mdpi/ic_unknown.png new file mode 100644 index 000000000..e33feb294 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/ic_unknown.png differ diff --git a/res/drawable-mdpi/ic_volume.png b/Squeezer/src/main/res/drawable-mdpi/ic_volume.png similarity index 100% rename from res/drawable-mdpi/ic_volume.png rename to Squeezer/src/main/res/drawable-mdpi/ic_volume.png diff --git a/res/drawable-mdpi/ic_volume_off.png b/Squeezer/src/main/res/drawable-mdpi/ic_volume_off.png similarity index 100% rename from res/drawable-mdpi/ic_volume_off.png rename to Squeezer/src/main/res/drawable-mdpi/ic_volume_off.png diff --git a/res/drawable-mdpi/ic_years.png b/Squeezer/src/main/res/drawable-mdpi/ic_years.png similarity index 100% rename from res/drawable-mdpi/ic_years.png rename to Squeezer/src/main/res/drawable-mdpi/ic_years.png diff --git a/res/drawable-mdpi/icon.png b/Squeezer/src/main/res/drawable-mdpi/icon.png similarity index 100% rename from res/drawable-mdpi/icon.png rename to Squeezer/src/main/res/drawable-mdpi/icon.png diff --git a/res/drawable-mdpi/icon_alarm.png b/Squeezer/src/main/res/drawable-mdpi/icon_alarm.png similarity index 100% rename from res/drawable-mdpi/icon_alarm.png rename to Squeezer/src/main/res/drawable-mdpi/icon_alarm.png diff --git a/res/drawable-mdpi/icon_album_noart.png b/Squeezer/src/main/res/drawable-mdpi/icon_album_noart.png similarity index 100% rename from res/drawable-mdpi/icon_album_noart.png rename to Squeezer/src/main/res/drawable-mdpi/icon_album_noart.png diff --git a/Squeezer/src/main/res/drawable-mdpi/icon_album_noart_fullscreen.png b/Squeezer/src/main/res/drawable-mdpi/icon_album_noart_fullscreen.png new file mode 100644 index 000000000..cff90ea75 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/icon_album_noart_fullscreen.png differ diff --git a/res/drawable-mdpi/icon_baby.png b/Squeezer/src/main/res/drawable-mdpi/icon_baby.png similarity index 100% rename from res/drawable-mdpi/icon_baby.png rename to Squeezer/src/main/res/drawable-mdpi/icon_baby.png diff --git a/res/drawable-mdpi/icon_blank.png b/Squeezer/src/main/res/drawable-mdpi/icon_blank.png similarity index 100% rename from res/drawable-mdpi/icon_blank.png rename to Squeezer/src/main/res/drawable-mdpi/icon_blank.png diff --git a/res/drawable-mdpi/icon_boom.png b/Squeezer/src/main/res/drawable-mdpi/icon_boom.png similarity index 100% rename from res/drawable-mdpi/icon_boom.png rename to Squeezer/src/main/res/drawable-mdpi/icon_boom.png diff --git a/Squeezer/src/main/res/drawable-mdpi/icon_controller.png b/Squeezer/src/main/res/drawable-mdpi/icon_controller.png new file mode 100644 index 000000000..43219bf9e Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/icon_controller.png differ diff --git a/res/drawable-mdpi/icon_digital_inputs.png b/Squeezer/src/main/res/drawable-mdpi/icon_digital_inputs.png similarity index 100% rename from res/drawable-mdpi/icon_digital_inputs.png rename to Squeezer/src/main/res/drawable-mdpi/icon_digital_inputs.png diff --git a/res/drawable-mdpi/icon_fab4.png b/Squeezer/src/main/res/drawable-mdpi/icon_fab4.png similarity index 100% rename from res/drawable-mdpi/icon_fab4.png rename to Squeezer/src/main/res/drawable-mdpi/icon_fab4.png diff --git a/res/drawable-mdpi/icon_favorites.png b/Squeezer/src/main/res/drawable-mdpi/icon_favorites.png similarity index 100% rename from res/drawable-mdpi/icon_favorites.png rename to Squeezer/src/main/res/drawable-mdpi/icon_favorites.png diff --git a/res/drawable-mdpi/icon_group.png b/Squeezer/src/main/res/drawable-mdpi/icon_group.png similarity index 100% rename from res/drawable-mdpi/icon_group.png rename to Squeezer/src/main/res/drawable-mdpi/icon_group.png diff --git a/res/drawable-mdpi/icon_home.png b/Squeezer/src/main/res/drawable-mdpi/icon_home.png similarity index 100% rename from res/drawable-mdpi/icon_home.png rename to Squeezer/src/main/res/drawable-mdpi/icon_home.png diff --git a/res/drawable-mdpi/icon_image_viewer.png b/Squeezer/src/main/res/drawable-mdpi/icon_image_viewer.png similarity index 100% rename from res/drawable-mdpi/icon_image_viewer.png rename to Squeezer/src/main/res/drawable-mdpi/icon_image_viewer.png diff --git a/res/drawable-mdpi/icon_iradio_noart.png b/Squeezer/src/main/res/drawable-mdpi/icon_iradio_noart.png similarity index 100% rename from res/drawable-mdpi/icon_iradio_noart.png rename to Squeezer/src/main/res/drawable-mdpi/icon_iradio_noart.png diff --git a/Squeezer/src/main/res/drawable-mdpi/icon_iradio_noart_fullscreen.png b/Squeezer/src/main/res/drawable-mdpi/icon_iradio_noart_fullscreen.png new file mode 100644 index 000000000..5e653518c Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/icon_iradio_noart_fullscreen.png differ diff --git a/res/drawable-mdpi/icon_ml_other_library.png b/Squeezer/src/main/res/drawable-mdpi/icon_ml_other_library.png similarity index 100% rename from res/drawable-mdpi/icon_ml_other_library.png rename to Squeezer/src/main/res/drawable-mdpi/icon_ml_other_library.png diff --git a/res/drawable-mdpi/icon_my_apps.png b/Squeezer/src/main/res/drawable-mdpi/icon_my_apps.png similarity index 100% rename from res/drawable-mdpi/icon_my_apps.png rename to Squeezer/src/main/res/drawable-mdpi/icon_my_apps.png diff --git a/res/drawable-mdpi/icon_mymusic.png b/Squeezer/src/main/res/drawable-mdpi/icon_mymusic.png similarity index 100% rename from res/drawable-mdpi/icon_mymusic.png rename to Squeezer/src/main/res/drawable-mdpi/icon_mymusic.png diff --git a/res/drawable-mdpi/icon_nowplaying.png b/Squeezer/src/main/res/drawable-mdpi/icon_nowplaying.png similarity index 100% rename from res/drawable-mdpi/icon_nowplaying.png rename to Squeezer/src/main/res/drawable-mdpi/icon_nowplaying.png diff --git a/res/drawable-mdpi/icon_playlist_noart.png b/Squeezer/src/main/res/drawable-mdpi/icon_playlist_noart.png similarity index 100% rename from res/drawable-mdpi/icon_playlist_noart.png rename to Squeezer/src/main/res/drawable-mdpi/icon_playlist_noart.png diff --git a/res/drawable-mdpi/icon_receiver.png b/Squeezer/src/main/res/drawable-mdpi/icon_receiver.png similarity index 100% rename from res/drawable-mdpi/icon_receiver.png rename to Squeezer/src/main/res/drawable-mdpi/icon_receiver.png diff --git a/res/drawable-mdpi/icon_region_americas.png b/Squeezer/src/main/res/drawable-mdpi/icon_region_americas.png similarity index 100% rename from res/drawable-mdpi/icon_region_americas.png rename to Squeezer/src/main/res/drawable-mdpi/icon_region_americas.png diff --git a/res/drawable-mdpi/icon_region_other.png b/Squeezer/src/main/res/drawable-mdpi/icon_region_other.png similarity index 100% rename from res/drawable-mdpi/icon_region_other.png rename to Squeezer/src/main/res/drawable-mdpi/icon_region_other.png diff --git a/res/drawable-mdpi/icon_sb1n2.png b/Squeezer/src/main/res/drawable-mdpi/icon_sb1n2.png similarity index 100% rename from res/drawable-mdpi/icon_sb1n2.png rename to Squeezer/src/main/res/drawable-mdpi/icon_sb1n2.png diff --git a/res/drawable-mdpi/icon_sb3.png b/Squeezer/src/main/res/drawable-mdpi/icon_sb3.png similarity index 100% rename from res/drawable-mdpi/icon_sb3.png rename to Squeezer/src/main/res/drawable-mdpi/icon_sb3.png diff --git a/res/drawable-mdpi/icon_search.png b/Squeezer/src/main/res/drawable-mdpi/icon_search.png similarity index 100% rename from res/drawable-mdpi/icon_search.png rename to Squeezer/src/main/res/drawable-mdpi/icon_search.png diff --git a/res/drawable-mdpi/icon_slimp3.png b/Squeezer/src/main/res/drawable-mdpi/icon_slimp3.png similarity index 100% rename from res/drawable-mdpi/icon_slimp3.png rename to Squeezer/src/main/res/drawable-mdpi/icon_slimp3.png diff --git a/res/drawable-mdpi/icon_softsqueeze.png b/Squeezer/src/main/res/drawable-mdpi/icon_softsqueeze.png similarity index 100% rename from res/drawable-mdpi/icon_softsqueeze.png rename to Squeezer/src/main/res/drawable-mdpi/icon_softsqueeze.png diff --git a/res/drawable-mdpi/icon_squeezeplay.png b/Squeezer/src/main/res/drawable-mdpi/icon_squeezeplay.png similarity index 100% rename from res/drawable-mdpi/icon_squeezeplay.png rename to Squeezer/src/main/res/drawable-mdpi/icon_squeezeplay.png diff --git a/res/drawable-mdpi/icon_staffpicks.png b/Squeezer/src/main/res/drawable-mdpi/icon_staffpicks.png similarity index 100% rename from res/drawable-mdpi/icon_staffpicks.png rename to Squeezer/src/main/res/drawable-mdpi/icon_staffpicks.png diff --git a/res/drawable-mdpi/icon_sync.png b/Squeezer/src/main/res/drawable-mdpi/icon_sync.png similarity index 100% rename from res/drawable-mdpi/icon_sync.png rename to Squeezer/src/main/res/drawable-mdpi/icon_sync.png diff --git a/res/drawable-mdpi/icon_transporter.png b/Squeezer/src/main/res/drawable-mdpi/icon_transporter.png similarity index 100% rename from res/drawable-mdpi/icon_transporter.png rename to Squeezer/src/main/res/drawable-mdpi/icon_transporter.png diff --git a/res/drawable-mdpi/icon_tunein_url.png b/Squeezer/src/main/res/drawable-mdpi/icon_tunein_url.png similarity index 100% rename from res/drawable-mdpi/icon_tunein_url.png rename to Squeezer/src/main/res/drawable-mdpi/icon_tunein_url.png diff --git a/res/drawable-mdpi/icon_tutorial.png b/Squeezer/src/main/res/drawable-mdpi/icon_tutorial.png similarity index 100% rename from res/drawable-mdpi/icon_tutorial.png rename to Squeezer/src/main/res/drawable-mdpi/icon_tutorial.png diff --git a/res/drawable-mdpi/icon_verify.png b/Squeezer/src/main/res/drawable-mdpi/icon_verify.png similarity index 100% rename from res/drawable-mdpi/icon_verify.png rename to Squeezer/src/main/res/drawable-mdpi/icon_verify.png diff --git a/res/drawable-mdpi/icon_wireless.png b/Squeezer/src/main/res/drawable-mdpi/icon_wireless.png similarity index 100% rename from res/drawable-mdpi/icon_wireless.png rename to Squeezer/src/main/res/drawable-mdpi/icon_wireless.png diff --git a/Squeezer/src/main/res/drawable-mdpi/list_activated_holo.9.png b/Squeezer/src/main/res/drawable-mdpi/list_activated_holo.9.png new file mode 100644 index 000000000..3bf8e0362 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/list_activated_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/list_focused_holo.9.png b/Squeezer/src/main/res/drawable-mdpi/list_focused_holo.9.png new file mode 100644 index 000000000..00f05d8c9 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/list_focused_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/list_longpressed_holo.9.png b/Squeezer/src/main/res/drawable-mdpi/list_longpressed_holo.9.png new file mode 100644 index 000000000..3bf8e0362 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/list_longpressed_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/list_pressed_holo_dark.9.png b/Squeezer/src/main/res/drawable-mdpi/list_pressed_holo_dark.9.png new file mode 100644 index 000000000..6e77525d2 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/list_pressed_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/list_selector_disabled_holo_dark.9.png b/Squeezer/src/main/res/drawable-mdpi/list_selector_disabled_holo_dark.9.png new file mode 100644 index 000000000..92da2f0dd Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/list_selector_disabled_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-mdpi/list_selector_pressed_holo_dark.9.png b/Squeezer/src/main/res/drawable-mdpi/list_selector_pressed_holo_dark.9.png new file mode 100644 index 000000000..bf36a4318 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/list_selector_pressed_holo_dark.9.png differ diff --git a/res/drawable-mdpi/panel_background.9.png b/Squeezer/src/main/res/drawable-mdpi/panel_background.9.png similarity index 100% rename from res/drawable-mdpi/panel_background.9.png rename to Squeezer/src/main/res/drawable-mdpi/panel_background.9.png diff --git a/Squeezer/src/main/res/drawable-mdpi/presence_online.png b/Squeezer/src/main/res/drawable-mdpi/presence_online.png new file mode 100644 index 000000000..c400a1820 Binary files /dev/null and b/Squeezer/src/main/res/drawable-mdpi/presence_online.png differ diff --git a/res/drawable-mdpi/stat_notify_musicplayer.png b/Squeezer/src/main/res/drawable-mdpi/stat_notify_musicplayer.png similarity index 100% rename from res/drawable-mdpi/stat_notify_musicplayer.png rename to Squeezer/src/main/res/drawable-mdpi/stat_notify_musicplayer.png diff --git a/res/drawable-mdpi/volume_down.png b/Squeezer/src/main/res/drawable-mdpi/volume_down.png similarity index 100% rename from res/drawable-mdpi/volume_down.png rename to Squeezer/src/main/res/drawable-mdpi/volume_down.png diff --git a/res/drawable-mdpi/volume_up.png b/Squeezer/src/main/res/drawable-mdpi/volume_up.png similarity index 100% rename from res/drawable-mdpi/volume_up.png rename to Squeezer/src/main/res/drawable-mdpi/volume_up.png diff --git a/Squeezer/src/main/res/drawable-xhdpi/actionbar_shadow.9.png b/Squeezer/src/main/res/drawable-xhdpi/actionbar_shadow.9.png new file mode 100644 index 000000000..30778e3f8 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/actionbar_shadow.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_all.png b/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_all.png new file mode 100644 index 000000000..d42de7392 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_all.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_off.png b/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_off.png new file mode 100644 index 000000000..2f66ad794 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_off.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_one.png b/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_one.png new file mode 100644 index 000000000..d6a0d164a Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/btn_repeat_one.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_album.png b/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_album.png new file mode 100644 index 000000000..90cff5728 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_album.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_off.png b/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_off.png new file mode 100644 index 000000000..bb6024201 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_off.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_song.png b/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_song.png new file mode 100644 index 000000000..ea944694e Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/btn_shuffle_song.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png b/Squeezer/src/main/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png new file mode 100644 index 000000000..36d8cf47e Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/dropdown_ic_arrow_normal_holo_dark.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_disconnect.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_disconnect.png new file mode 100644 index 000000000..c9c6f80cc Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_disconnect.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_filter.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_filter.png new file mode 100644 index 000000000..8d46db3e4 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_filter.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_home.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_home.png new file mode 100644 index 000000000..f3251fc8a Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_home.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_next.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_next.png new file mode 100644 index 000000000..5c030cb15 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_next.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_now_playing.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_now_playing.png new file mode 100644 index 000000000..49df26011 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_now_playing.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_overflow.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_overflow.png new file mode 100644 index 000000000..6ebb45409 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_overflow.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_pause.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_pause.png new file mode 100644 index 000000000..b02cf9093 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_pause.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_play.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_play.png new file mode 100644 index 000000000..e01a8445e Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_play.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_add.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_add.png new file mode 100644 index 000000000..9eabfeda2 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_add.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_clear.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_clear.png new file mode 100644 index 000000000..b38920933 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_clear.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_save.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_save.png new file mode 100644 index 000000000..d99185432 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_playlist_save.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_power.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_power.png new file mode 100644 index 000000000..c19e76a5b Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_power.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_preferences.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_preferences.png new file mode 100644 index 000000000..669f5a15a Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_preferences.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_previous.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_previous.png new file mode 100644 index 000000000..f6eea80bc Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_previous.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_sort.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_sort.png new file mode 100644 index 000000000..34d29f5c9 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_sort.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_view_as_grid.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_view_as_grid.png new file mode 100644 index 000000000..43ca84b9c Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_view_as_grid.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_action_view_as_list.png b/Squeezer/src/main/res/drawable-xhdpi/ic_action_view_as_list.png new file mode 100644 index 000000000..fd6cd4983 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_action_view_as_list.png differ diff --git a/res/drawable-xhdpi/ic_albums.png b/Squeezer/src/main/res/drawable-xhdpi/ic_albums.png similarity index 100% rename from res/drawable-xhdpi/ic_albums.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_albums.png diff --git a/res/drawable-xhdpi/ic_artists.png b/Squeezer/src/main/res/drawable-xhdpi/ic_artists.png similarity index 100% rename from res/drawable-xhdpi/ic_artists.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_artists.png diff --git a/res/drawable-xhdpi/ic_favorites.png b/Squeezer/src/main/res/drawable-xhdpi/ic_favorites.png similarity index 100% rename from res/drawable-xhdpi/ic_favorites.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_favorites.png diff --git a/res/drawable-xhdpi/ic_genres.png b/Squeezer/src/main/res/drawable-xhdpi/ic_genres.png similarity index 100% rename from res/drawable-xhdpi/ic_genres.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_genres.png diff --git a/res/drawable-xhdpi/ic_internet_radio.png b/Squeezer/src/main/res/drawable-xhdpi/ic_internet_radio.png similarity index 100% rename from res/drawable-xhdpi/ic_internet_radio.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_internet_radio.png diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_launcher.png b/Squeezer/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..faf7427ea Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_menu_choose_player.png b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_choose_player.png new file mode 100644 index 000000000..ed91b269e Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_choose_player.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_menu_playlist.png b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_playlist.png new file mode 100644 index 000000000..95708234a Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_playlist.png differ diff --git a/res/drawable-xhdpi/ic_menu_save.png b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_save.png similarity index 100% rename from res/drawable-xhdpi/ic_menu_save.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_menu_save.png diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_menu_search.png b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_search.png new file mode 100644 index 000000000..3549f84dd Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_menu_search.png differ diff --git a/res/drawable-xhdpi/ic_music_folder.png b/Squeezer/src/main/res/drawable-xhdpi/ic_music_folder.png similarity index 100% rename from res/drawable-xhdpi/ic_music_folder.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_music_folder.png diff --git a/res/drawable-xhdpi/ic_my_apps.png b/Squeezer/src/main/res/drawable-xhdpi/ic_my_apps.png similarity index 100% rename from res/drawable-xhdpi/ic_my_apps.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_my_apps.png diff --git a/res/drawable-xhdpi/ic_my_music.png b/Squeezer/src/main/res/drawable-xhdpi/ic_my_music.png similarity index 100% rename from res/drawable-xhdpi/ic_my_music.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_my_music.png diff --git a/res/drawable-xhdpi/ic_new_music.png b/Squeezer/src/main/res/drawable-xhdpi/ic_new_music.png similarity index 100% rename from res/drawable-xhdpi/ic_new_music.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_new_music.png diff --git a/res/drawable-xhdpi/ic_now_playing.png b/Squeezer/src/main/res/drawable-xhdpi/ic_now_playing.png similarity index 100% rename from res/drawable-xhdpi/ic_now_playing.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_now_playing.png diff --git a/res/drawable-xhdpi/ic_playlists.png b/Squeezer/src/main/res/drawable-xhdpi/ic_playlists.png similarity index 100% rename from res/drawable-xhdpi/ic_playlists.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_playlists.png diff --git a/res/drawable-xhdpi/ic_random.png b/Squeezer/src/main/res/drawable-xhdpi/ic_random.png similarity index 100% rename from res/drawable-xhdpi/ic_random.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_random.png diff --git a/res/drawable-xhdpi/ic_search.png b/Squeezer/src/main/res/drawable-xhdpi/ic_search.png similarity index 100% rename from res/drawable-xhdpi/ic_search.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_search.png diff --git a/res/drawable-xhdpi/ic_songs.png b/Squeezer/src/main/res/drawable-xhdpi/ic_songs.png similarity index 100% rename from res/drawable-xhdpi/ic_songs.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_songs.png diff --git a/Squeezer/src/main/res/drawable-xhdpi/ic_unknown.png b/Squeezer/src/main/res/drawable-xhdpi/ic_unknown.png new file mode 100644 index 000000000..f407088ed Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/ic_unknown.png differ diff --git a/res/drawable-xhdpi/ic_years.png b/Squeezer/src/main/res/drawable-xhdpi/ic_years.png similarity index 100% rename from res/drawable-xhdpi/ic_years.png rename to Squeezer/src/main/res/drawable-xhdpi/ic_years.png diff --git a/res/drawable-hdpi/icon_album_noart.png b/Squeezer/src/main/res/drawable-xhdpi/icon_album_noart.png similarity index 100% rename from res/drawable-hdpi/icon_album_noart.png rename to Squeezer/src/main/res/drawable-xhdpi/icon_album_noart.png diff --git a/Squeezer/src/main/res/drawable-xhdpi/list_activated_holo.9.png b/Squeezer/src/main/res/drawable-xhdpi/list_activated_holo.9.png new file mode 100644 index 000000000..eda10e612 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/list_activated_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/list_focused_holo.9.png b/Squeezer/src/main/res/drawable-xhdpi/list_focused_holo.9.png new file mode 100644 index 000000000..b545f8e57 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/list_focused_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/list_longpressed_holo.9.png b/Squeezer/src/main/res/drawable-xhdpi/list_longpressed_holo.9.png new file mode 100644 index 000000000..eda10e612 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/list_longpressed_holo.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/list_pressed_holo_dark.9.png b/Squeezer/src/main/res/drawable-xhdpi/list_pressed_holo_dark.9.png new file mode 100644 index 000000000..e4b33935a Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/list_pressed_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/list_selector_disabled_holo_dark.9.png b/Squeezer/src/main/res/drawable-xhdpi/list_selector_disabled_holo_dark.9.png new file mode 100644 index 000000000..88726b691 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/list_selector_disabled_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/list_selector_pressed_holo_dark.9.png b/Squeezer/src/main/res/drawable-xhdpi/list_selector_pressed_holo_dark.9.png new file mode 100644 index 000000000..df197011a Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/list_selector_pressed_holo_dark.9.png differ diff --git a/Squeezer/src/main/res/drawable-xhdpi/presence_online.png b/Squeezer/src/main/res/drawable-xhdpi/presence_online.png new file mode 100644 index 000000000..e2aef1177 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xhdpi/presence_online.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_action_next.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_next.png new file mode 100644 index 000000000..3c9377271 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_next.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_action_pause.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_pause.png new file mode 100644 index 000000000..293f7127d Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_pause.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_action_play.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_play.png new file mode 100644 index 000000000..97ff9b077 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_play.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_action_previous.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_previous.png new file mode 100644 index 000000000..99d6a79fb Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_previous.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_action_view_as_grid.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_view_as_grid.png new file mode 100644 index 000000000..515f8e083 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_view_as_grid.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_action_view_as_list.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_view_as_list.png new file mode 100644 index 000000000..2c955eee3 Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_action_view_as_list.png differ diff --git a/Squeezer/src/main/res/drawable-xxhdpi/ic_launcher.png b/Squeezer/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..3e285307f Binary files /dev/null and b/Squeezer/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/Squeezer/src/main/res/drawable/actionbar_compat_item.xml b/Squeezer/src/main/res/drawable/actionbar_compat_item.xml new file mode 100644 index 000000000..42b691b3e --- /dev/null +++ b/Squeezer/src/main/res/drawable/actionbar_compat_item.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/Squeezer/src/main/res/drawable/actionbar_compat_item_focused.xml b/Squeezer/src/main/res/drawable/actionbar_compat_item_focused.xml new file mode 100644 index 000000000..0ba7e5c9d --- /dev/null +++ b/Squeezer/src/main/res/drawable/actionbar_compat_item_focused.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/Squeezer/src/main/res/drawable/actionbar_compat_item_pressed.xml b/Squeezer/src/main/res/drawable/actionbar_compat_item_pressed.xml new file mode 100644 index 000000000..c02e4e2dc --- /dev/null +++ b/Squeezer/src/main/res/drawable/actionbar_compat_item_pressed.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/Squeezer/src/main/res/drawable/icon_pending_artwork.9.png b/Squeezer/src/main/res/drawable/icon_pending_artwork.9.png new file mode 100644 index 000000000..a6542770d Binary files /dev/null and b/Squeezer/src/main/res/drawable/icon_pending_artwork.9.png differ diff --git a/res/drawable/list_item_background_current.xml b/Squeezer/src/main/res/drawable/list_item_background_current.xml similarity index 61% rename from res/drawable/list_item_background_current.xml rename to Squeezer/src/main/res/drawable/list_item_background_current.xml index 9ec42946b..94a78bab1 100644 --- a/res/drawable/list_item_background_current.xml +++ b/Squeezer/src/main/res/drawable/list_item_background_current.xml @@ -17,8 +17,10 @@ --> - - - - + + + + diff --git a/res/drawable/list_item_background_normal.xml b/Squeezer/src/main/res/drawable/list_item_background_normal.xml similarity index 62% rename from res/drawable/list_item_background_normal.xml rename to Squeezer/src/main/res/drawable/list_item_background_normal.xml index 352947a28..426fe26de 100644 --- a/res/drawable/list_item_background_normal.xml +++ b/Squeezer/src/main/res/drawable/list_item_background_normal.xml @@ -17,8 +17,10 @@ --> - - - - + + + + diff --git a/Squeezer/src/main/res/drawable/list_selector_background_transition_holo_dark.xml b/Squeezer/src/main/res/drawable/list_selector_background_transition_holo_dark.xml new file mode 100644 index 000000000..d5c765af6 --- /dev/null +++ b/Squeezer/src/main/res/drawable/list_selector_background_transition_holo_dark.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/Squeezer/src/main/res/drawable/list_selector_holo_dark.xml b/Squeezer/src/main/res/drawable/list_selector_holo_dark.xml new file mode 100644 index 000000000..2d88815c8 --- /dev/null +++ b/Squeezer/src/main/res/drawable/list_selector_holo_dark.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/Squeezer/src/main/res/layout-land/now_playing_fragment_full.xml b/Squeezer/src/main/res/layout-land/now_playing_fragment_full.xml new file mode 100644 index 000000000..07b8b50d1 --- /dev/null +++ b/Squeezer/src/main/res/layout-land/now_playing_fragment_full.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Squeezer/src/main/res/layout-v11/actionbar_indeterminate_progress.xml b/Squeezer/src/main/res/layout-v11/actionbar_indeterminate_progress.xml new file mode 100644 index 000000000..a4d4656d3 --- /dev/null +++ b/Squeezer/src/main/res/layout-v11/actionbar_indeterminate_progress.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/res/layout/about_dialog.xml b/Squeezer/src/main/res/layout/about_dialog.xml similarity index 54% rename from res/layout/about_dialog.xml rename to Squeezer/src/main/res/layout/about_dialog.xml index 5b0aef8de..4c8d604de 100644 --- a/res/layout/about_dialog.xml +++ b/Squeezer/src/main/res/layout/about_dialog.xml @@ -1,89 +1,102 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Squeezer/src/main/res/layout/actionbar_compat.xml b/Squeezer/src/main/res/layout/actionbar_compat.xml new file mode 100644 index 000000000..a60613194 --- /dev/null +++ b/Squeezer/src/main/res/layout/actionbar_compat.xml @@ -0,0 +1,21 @@ + + + diff --git a/Squeezer/src/main/res/layout/authentication_dialog.xml b/Squeezer/src/main/res/layout/authentication_dialog.xml new file mode 100644 index 000000000..35b9446ef --- /dev/null +++ b/Squeezer/src/main/res/layout/authentication_dialog.xml @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/Squeezer/src/main/res/layout/context_menu_image_button.xml b/Squeezer/src/main/res/layout/context_menu_image_button.xml new file mode 100644 index 000000000..93bb86460 --- /dev/null +++ b/Squeezer/src/main/res/layout/context_menu_image_button.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/Squeezer/src/main/res/layout/disconnected.xml b/Squeezer/src/main/res/layout/disconnected.xml new file mode 100644 index 000000000..667be7441 --- /dev/null +++ b/Squeezer/src/main/res/layout/disconnected.xml @@ -0,0 +1,39 @@ + + + + + + +