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