aboutsummaryrefslogtreecommitdiff
path: root/subsonic-android/src/github/daneren2005/subdroid/util
diff options
context:
space:
mode:
Diffstat (limited to 'subsonic-android/src/github/daneren2005/subdroid/util')
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/AlbumView.java55
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/ArtistAdapter.java78
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/BackgroundTask.java96
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/CacheCleaner.java171
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/CancellableTask.java87
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/Constants.java93
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/EntryAdapter.java71
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/ErrorDialog.java61
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/FileUtil.java311
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/HorizontalSlider.java141
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/ImageLoader.java252
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/LRUCache.java102
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/MergeAdapter.java290
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/ModalBackgroundTask.java139
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/MyViewFlipper.java53
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/Pair.java54
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/PlaylistAdapter.java99
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/ProgressListener.java27
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/SackOfViewsAdapter.java181
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/ShufflePlayBuffer.java109
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/SilentBackgroundTask.java67
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/SimpleServiceBinder.java37
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/SongView.java178
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/TabActivityBackgroundTask.java67
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/TimeLimitedCache.java55
-rw-r--r--subsonic-android/src/github/daneren2005/subdroid/util/Util.java852
26 files changed, 3726 insertions, 0 deletions
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/AlbumView.java b/subsonic-android/src/github/daneren2005/subdroid/util/AlbumView.java
new file mode 100644
index 00000000..27cfc77b
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/AlbumView.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import github.daneren2005.subdroid.R;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+
+/**
+ * Used to display albums in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class AlbumView extends LinearLayout {
+
+ private TextView titleView;
+ private TextView artistView;
+ private View coverArtView;
+
+ public AlbumView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
+
+ titleView = (TextView) findViewById(R.id.album_title);
+ artistView = (TextView) findViewById(R.id.album_artist);
+ coverArtView = findViewById(R.id.album_coverart);
+ }
+
+ public void setAlbum(MusicDirectory.Entry album, ImageLoader imageLoader) {
+ titleView.setText(album.getTitle());
+ artistView.setText(album.getArtist());
+ artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE);
+ imageLoader.loadImage(coverArtView, album, false, true);
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/ArtistAdapter.java b/subsonic-android/src/github/daneren2005/subdroid/util/ArtistAdapter.java
new file mode 100644
index 00000000..6973af8c
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/ArtistAdapter.java
@@ -0,0 +1,78 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import github.daneren2005.subdroid.domain.Artist;
+import github.daneren2005.subdroid.R;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import android.content.Context;
+
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.ArrayList;
+
+/**
+ * @author Sindre Mehus
+*/
+public class ArtistAdapter extends ArrayAdapter<Artist> implements SectionIndexer {
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ public ArtistAdapter(Context context, List<Artist> artists) {
+ super(context, R.layout.artist_list_item, artists);
+
+ Set<String> sectionSet = new LinkedHashSet<String>(30);
+ List<Integer> positionList = new ArrayList<Integer>(30);
+ for (int i = 0; i < artists.size(); i++) {
+ Artist artist = artists.get(i);
+ String index = artist.getIndex();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/BackgroundTask.java b/subsonic-android/src/github/daneren2005/subdroid/util/BackgroundTask.java
new file mode 100644
index 00000000..ac8c9da7
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/BackgroundTask.java
@@ -0,0 +1,96 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import github.daneren2005.subdroid.R;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class BackgroundTask<T> implements ProgressListener {
+
+ private static final String TAG = BackgroundTask.class.getSimpleName();
+ private final Activity activity;
+ private final Handler handler;
+
+ public BackgroundTask(Activity activity) {
+ this.activity = activity;
+ handler = new Handler();
+ }
+
+ protected Activity getActivity() {
+ return activity;
+ }
+
+ protected Handler getHandler() {
+ return handler;
+ }
+
+ public abstract void execute();
+
+ protected abstract T doInBackground() throws Throwable;
+
+ protected abstract void done(T result);
+
+ protected void error(Throwable error) {
+ Log.w(TAG, "Got exception: " + error, error);
+ new ErrorDialog(activity, getErrorMessage(error), true);
+ }
+
+ protected String getErrorMessage(Throwable error) {
+
+ if (error instanceof IOException && !Util.isNetworkConnected(activity)) {
+ return activity.getResources().getString(R.string.background_task_no_network);
+ }
+
+ if (error instanceof FileNotFoundException) {
+ return activity.getResources().getString(R.string.background_task_not_found);
+ }
+
+ if (error instanceof IOException) {
+ return activity.getResources().getString(R.string.background_task_network_error);
+ }
+
+ if (error instanceof XmlPullParserException) {
+ return activity.getResources().getString(R.string.background_task_parse_error);
+ }
+
+ String message = error.getMessage();
+ if (message != null) {
+ return message;
+ }
+ return error.getClass().getSimpleName();
+ }
+
+ @Override
+ public abstract void updateProgress(final String message);
+
+ @Override
+ public void updateProgress(int messageId) {
+ updateProgress(activity.getResources().getString(messageId));
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/CacheCleaner.java b/subsonic-android/src/github/daneren2005/subdroid/util/CacheCleaner.java
new file mode 100644
index 00000000..66c8b959
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/CacheCleaner.java
@@ -0,0 +1,171 @@
+package github.daneren2005.subdroid.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.content.Context;
+import android.util.Log;
+import android.os.StatFs;
+import github.daneren2005.subdroid.service.DownloadFile;
+import github.daneren2005.subdroid.service.DownloadService;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheCleaner {
+
+ private static final String TAG = CacheCleaner.class.getSimpleName();
+ private static final double MAX_FILE_SYSTEM_USAGE = 0.95;
+
+ private final Context context;
+ private final DownloadService downloadService;
+
+ public CacheCleaner(Context context, DownloadService downloadService) {
+ this.context = context;
+ this.downloadService = downloadService;
+ }
+
+ public void clean() {
+
+ Log.i(TAG, "Starting cache cleaning.");
+
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return;
+ }
+
+ try {
+
+ List<File> files = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
+ sortByAscendingModificationTime(files);
+
+ Set<File> undeletable = findUndeletableFiles();
+
+ deleteFiles(files, undeletable);
+ deleteEmptyDirs(dirs, undeletable);
+ Log.i(TAG, "Completed cache cleaning.");
+
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+ }
+
+ private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
+ for (File dir : dirs) {
+ if (undeletable.contains(dir)) {
+ continue;
+ }
+
+ File[] children = dir.listFiles();
+
+ // Delete empty directory and associated album artwork.
+ if (children.length == 0) {
+ Util.delete(dir);
+ Util.delete(FileUtil.getAlbumArtFile(dir));
+ }
+ }
+ }
+
+ private void deleteFiles(List<File> files, Set<File> undeletable) {
+
+ if (files.isEmpty()) {
+ return;
+ }
+
+ long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
+
+ long bytesUsedBySubsonic = 0L;
+ for (File file : files) {
+ bytesUsedBySubsonic += file.length();
+ }
+
+ // Ensure that file system is not more than 95% full.
+ StatFs stat = new StatFs(files.get(0).getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
+ long minFsAvailability = Math.round(MAX_FILE_SYSTEM_USAGE * (double) bytesTotalFs);
+
+ long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
+ long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
+ long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
+
+ Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
+ Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes));
+ Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
+ Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
+
+ long bytesDeleted = 0L;
+ for (File file : files) {
+
+ if (file.getName().equals(Constants.ALBUM_ART_FILE)) {
+ // Move artwork to new folder.
+ file.renameTo(FileUtil.getAlbumArtFile(file.getParentFile()));
+
+ } else if (bytesToDelete > bytesDeleted || file.getName().endsWith(".partial") || file.getName().contains(".partial.")) {
+ if (!undeletable.contains(file)) {
+ long size = file.length();
+ if (Util.delete(file)) {
+ bytesDeleted += size;
+ }
+ }
+ }
+ }
+
+ Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
+ Log.i(TAG, "Cache size after : " + Util.formatBytes(bytesUsedBySubsonic - bytesDeleted));
+ }
+
+ private void findCandidatesForDeletion(File file, List<File> files, List<File> dirs) {
+ if (file.isFile()) {
+ String name = file.getName();
+ boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
+ boolean isAlbumArtFile = name.equals(Constants.ALBUM_ART_FILE);
+ if (isCacheFile || isAlbumArtFile) {
+ files.add(file);
+ }
+ } else {
+ // Depth-first
+ for (File child : FileUtil.listFiles(file)) {
+ findCandidatesForDeletion(child, files, dirs);
+ }
+ dirs.add(file);
+ }
+ }
+
+ private void sortByAscendingModificationTime(List<File> files) {
+ Collections.sort(files, new Comparator<File>() {
+ @Override
+ public int compare(File a, File b) {
+ if (a.lastModified() < b.lastModified()) {
+ return -1;
+ }
+ if (a.lastModified() > b.lastModified()) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ }
+
+ private Set<File> findUndeletableFiles() {
+ Set<File> undeletable = new HashSet<File>(5);
+
+ for (DownloadFile downloadFile : downloadService.getDownloads()) {
+ undeletable.add(downloadFile.getPartialFile());
+ undeletable.add(downloadFile.getCompleteFile());
+ }
+
+ undeletable.add(FileUtil.getMusicDirectory(context));
+ return undeletable;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/CancellableTask.java b/subsonic-android/src/github/daneren2005/subdroid/util/CancellableTask.java
new file mode 100644
index 00000000..be30a08b
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/CancellableTask.java
@@ -0,0 +1,87 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import android.util.Log;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class CancellableTask {
+
+ private static final String TAG = CancellableTask.class.getSimpleName();
+
+ private final AtomicBoolean running = new AtomicBoolean(false);
+ private final AtomicBoolean cancelled = new AtomicBoolean(false);
+ private final AtomicReference<Thread> thread = new AtomicReference<Thread>();
+ private final AtomicReference<OnCancelListener> cancelListener = new AtomicReference<OnCancelListener>();
+
+ public void cancel() {
+ Log.d(TAG, "Cancelling " + CancellableTask.this);
+ cancelled.set(true);
+
+ OnCancelListener listener = cancelListener.get();
+ if (listener != null) {
+ try {
+ listener.onCancel();
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when invoking OnCancelListener.", x);
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return cancelled.get();
+ }
+
+ public void setOnCancelListener(OnCancelListener listener) {
+ cancelListener.set(listener);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public abstract void execute();
+
+ public void start() {
+ thread.set(new Thread() {
+ @Override
+ public void run() {
+ running.set(true);
+ Log.d(TAG, "Starting thread for " + CancellableTask.this);
+ try {
+ execute();
+ } finally {
+ running.set(false);
+ Log.d(TAG, "Stopping thread for " + CancellableTask.this);
+ }
+ }
+ });
+ thread.get().start();
+ }
+
+ public static interface OnCancelListener {
+ void onCancel();
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/Constants.java b/subsonic-android/src/github/daneren2005/subdroid/util/Constants.java
new file mode 100644
index 00000000..1b49cd4a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/Constants.java
@@ -0,0 +1,93 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Constants {
+
+ // Character encoding used throughout.
+ public static final String UTF_8 = "UTF-8";
+
+ // REST protocol version and client ID.
+ // Note: Keep it as low as possible to maintain compatibility with older servers.
+ public static final String REST_PROTOCOL_VERSION = "1.2.0";
+ public static final String REST_CLIENT_ID = "android";
+
+ // Names for intent extras.
+ public static final String INTENT_EXTRA_NAME_ID = "subsonic.id";
+ public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name";
+ public static final String INTENT_EXTRA_NAME_ARTIST = "subsonic.artist";
+ public static final String INTENT_EXTRA_NAME_TITLE = "subsonic.title";
+ public static final String INTENT_EXTRA_NAME_AUTOPLAY = "subsonic.playall";
+ public static final String INTENT_EXTRA_NAME_ERROR = "subsonic.error";
+ public static final String INTENT_EXTRA_NAME_QUERY = "subsonic.query";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_ID = "subsonic.playlist.id";
+ public static final String INTENT_EXTRA_NAME_PLAYLIST_NAME = "subsonic.playlist.name";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
+ public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
+ public static final String INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh";
+ public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch";
+ public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ;
+
+ // Notification IDs.
+ public static final int NOTIFICATION_ID_PLAYING = 100;
+ public static final int NOTIFICATION_ID_ERROR = 101;
+
+ // Preferences keys.
+ public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
+ public static final String PREFERENCES_KEY_SERVER_NAME = "serverName";
+ public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl";
+ public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId";
+ public static final String PREFERENCES_KEY_USERNAME = "username";
+ public static final String PREFERENCES_KEY_PASSWORD = "password";
+ public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
+ public static final String PREFERENCES_KEY_THEME = "theme";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile";
+ public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout";
+ public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
+ public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount";
+ public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia";
+ public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons";
+ public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload";
+ public static final String PREFERENCES_KEY_SCROBBLE = "scrobble";
+ public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
+ public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
+ public static final String PREFERENCES_KEY_RANDOM_SIZE = "randomSize";
+
+ // Name of the preferences file.
+ public static final String PREFERENCES_FILE_NAME = "github.daneren2005.subdroid_preferences";
+
+ // Number of free trial days for non-licensed servers.
+ public static final int FREE_TRIAL_DAYS = 30;
+
+ // URL for project donations.
+ public static final String DONATION_URL = "http://subsonic.org/pages/android-donation.jsp";
+
+ public static final String ALBUM_ART_FILE = "folder.jpeg";
+
+ private Constants() {
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/EntryAdapter.java b/subsonic-android/src/github/daneren2005/subdroid/util/EntryAdapter.java
new file mode 100644
index 00000000..66250165
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/EntryAdapter.java
@@ -0,0 +1,71 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.util.List;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import github.daneren2005.subdroid.activity.SubsonicTabActivity;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+
+/**
+ * @author Sindre Mehus
+ */
+public class EntryAdapter extends ArrayAdapter<MusicDirectory.Entry> {
+
+ private final SubsonicTabActivity activity;
+ private final ImageLoader imageLoader;
+ private final boolean checkable;
+
+ public EntryAdapter(SubsonicTabActivity activity, ImageLoader imageLoader, List<MusicDirectory.Entry> entries, boolean checkable) {
+ super(activity, android.R.layout.simple_list_item_1, entries);
+ this.activity = activity;
+ this.imageLoader = imageLoader;
+ this.checkable = checkable;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ MusicDirectory.Entry entry = getItem(position);
+
+ if (entry.isDirectory()) {
+ AlbumView view;
+ // TODO: Reuse AlbumView objects once cover art loading is working.
+// if (convertView != null && convertView instanceof AlbumView) {
+// view = (AlbumView) convertView;
+// } else {
+ view = new AlbumView(activity);
+// }
+ view.setAlbum(entry, imageLoader);
+ return view;
+
+ } else {
+ SongView view;
+ if (convertView != null && convertView instanceof SongView) {
+ view = (SongView) convertView;
+ } else {
+ view = new SongView(activity);
+ }
+ view.setSong(entry, checkable);
+ return view;
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/ErrorDialog.java b/subsonic-android/src/github/daneren2005/subdroid/util/ErrorDialog.java
new file mode 100644
index 00000000..3f2138be
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/ErrorDialog.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import github.daneren2005.subdroid.R;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ErrorDialog {
+
+ public ErrorDialog(Activity activity, int messageId, boolean finishActivityOnCancel) {
+ this(activity, activity.getResources().getString(messageId), finishActivityOnCancel);
+ }
+
+ public ErrorDialog(final Activity activity, String message, final boolean finishActivityOnClose) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setTitle(R.string.error_label);
+ builder.setMessage(message);
+ builder.setCancelable(true);
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ if (finishActivityOnClose) {
+ activity.finish();
+ }
+ }
+ });
+ builder.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (finishActivityOnClose) {
+ activity.finish();
+ }
+ }
+ });
+
+ builder.create().show();
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/FileUtil.java b/subsonic-android/src/github/daneren2005/subdroid/util/FileUtil.java
new file mode 100644
index 00000000..a36b9e9f
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/FileUtil.java
@@ -0,0 +1,311 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.Iterator;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Environment;
+import android.util.Log;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+
+/**
+ * @author Sindre Mehus
+ */
+public class FileUtil {
+
+ private static final String TAG = FileUtil.class.getSimpleName();
+ private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">"};
+ private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">"};
+ private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
+ private static final File DEFAULT_MUSIC_DIR = createDirectory("music");
+
+ public static File getSongFile(Context context, MusicDirectory.Entry song) {
+ File dir = getAlbumDirectory(context, song);
+
+ StringBuilder fileName = new StringBuilder();
+ Integer track = song.getTrack();
+ if (track != null) {
+ if (track < 10) {
+ fileName.append("0");
+ }
+ fileName.append(track).append("-");
+ }
+
+ fileName.append(fileSystemSafe(song.getTitle())).append(".");
+
+ if (song.getTranscodedSuffix() != null) {
+ fileName.append(song.getTranscodedSuffix());
+ } else {
+ fileName.append(song.getSuffix());
+ }
+
+ return new File(dir, fileName.toString());
+ }
+
+ public static File getPlaylistFile(String id) {
+ File playlistDir = getPlaylistDirectory();
+ return new File(playlistDir, id);
+ }
+ public static File getPlaylistDirectory() {
+ File playlistDir = new File(getSubsonicDirectory(), "playlists");
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+
+ public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
+ File albumDir = getAlbumDirectory(context, entry);
+ return getAlbumArtFile(albumDir);
+ }
+
+ public static File getAlbumArtFile(File albumDir) {
+ File albumArtDir = getAlbumArtDirectory();
+ return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg");
+ }
+
+ public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
+ File albumArtFile = getAlbumArtFile(context, entry);
+ if (albumArtFile.exists()) {
+ Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath());
+ return bitmap == null ? null : Bitmap.createScaledBitmap(bitmap, size, size, true);
+ }
+ return null;
+ }
+
+ public static File getAlbumArtDirectory() {
+ File albumArtDir = new File(getSubsonicDirectory(), "artwork");
+ ensureDirectoryExistsAndIsReadWritable(albumArtDir);
+ ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia"));
+ return albumArtDir;
+ }
+
+ private static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
+ File dir;
+ if (entry.getPath() != null) {
+ File f = new File(fileSystemSafeDir(entry.getPath()));
+ dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
+ } else {
+ String artist = fileSystemSafe(entry.getArtist());
+ String album = fileSystemSafe(entry.getAlbum());
+ dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
+ }
+ return dir;
+ }
+
+ public static void createDirectoryForParent(File file) {
+ File dir = file.getParentFile();
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory " + dir);
+ }
+ }
+ }
+
+ private static File createDirectory(String name) {
+ File dir = new File(getSubsonicDirectory(), name);
+ if (!dir.exists() && !dir.mkdirs()) {
+ Log.e(TAG, "Failed to create " + name);
+ }
+ return dir;
+ }
+
+ public static File getSubsonicDirectory() {
+ return new File(Environment.getExternalStorageDirectory(), "subsonic");
+ }
+
+ public static File getDefaultMusicDirectory() {
+ return DEFAULT_MUSIC_DIR;
+ }
+
+ public static File getMusicDirectory(Context context) {
+ String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath());
+ File dir = new File(path);
+ return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory();
+ }
+
+ public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) {
+ if (dir == null) {
+ return false;
+ }
+
+ if (dir.exists()) {
+ if (!dir.isDirectory()) {
+ Log.w(TAG, dir + " exists but is not a directory.");
+ return false;
+ }
+ } else {
+ if (dir.mkdirs()) {
+ Log.i(TAG, "Created directory " + dir);
+ } else {
+ Log.w(TAG, "Failed to create directory " + dir);
+ return false;
+ }
+ }
+
+ if (!dir.canRead()) {
+ Log.w(TAG, "No read permission for directory " + dir);
+ return false;
+ }
+
+ if (!dir.canWrite()) {
+ Log.w(TAG, "No write permission for directory " + dir);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param filename The filename in question.
+ * @return The filename with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafe(String filename) {
+ if (filename == null || filename.trim().length() == 0) {
+ return "unnamed";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-");
+ }
+ return filename;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like colons (":")
+ * with dashes ("-").
+ *
+ * @param path The path of the directory in question.
+ * @return The the directory name with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafeDir(String path) {
+ if (path == null || path.trim().length() == 0) {
+ return "";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE_DIR) {
+ path = path.replace(s, "-");
+ }
+ return path;
+ }
+
+ /**
+ * Similar to {@link File#listFiles()}, but returns a sorted set.
+ * Never returns {@code null}, instead a warning is logged, and an empty set is returned.
+ */
+ public static SortedSet<File> listFiles(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ Log.w(TAG, "Failed to list children for " + dir.getPath());
+ return new TreeSet<File>();
+ }
+
+ return new TreeSet<File>(Arrays.asList(files));
+ }
+
+ public static SortedSet<File> listMusicFiles(File dir) {
+ SortedSet<File> files = listFiles(dir);
+ Iterator<File> iterator = files.iterator();
+ while (iterator.hasNext()) {
+ File file = iterator.next();
+ if (!file.isDirectory() && !isMusicFile(file)) {
+ iterator.remove();
+ }
+ }
+ return files;
+ }
+
+ private static boolean isMusicFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension);
+ }
+
+ /**
+ * Returns the extension (the substring after the last dot) of the given file. The dot
+ * is not included in the returned extension.
+ *
+ * @param name The filename in question.
+ * @return The extension, or an empty string if no extension is found.
+ */
+ public static String getExtension(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? "" : name.substring(index + 1).toLowerCase();
+ }
+
+ /**
+ * Returns the base name (the substring before the last dot) of the given file. The dot
+ * is not included in the returned basename.
+ *
+ * @param name The filename in question.
+ * @return The base name, or an empty string if no basename is found.
+ */
+ public static String getBaseName(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? name : name.substring(0, index);
+ }
+
+ public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
+ File file = new File(context.getCacheDir(), fileName);
+ ObjectOutputStream out = null;
+ try {
+ out = new ObjectOutputStream(new FileOutputStream(file));
+ out.writeObject(obj);
+ Log.i(TAG, "Serialized object to " + file);
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize object to " + file);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ public static <T extends Serializable> T deserialize(Context context, String fileName) {
+ File file = new File(context.getCacheDir(), fileName);
+ if (!file.exists() || !file.isFile()) {
+ return null;
+ }
+
+ ObjectInputStream in = null;
+ try {
+ in = new ObjectInputStream(new FileInputStream(file));
+ T result = (T) in.readObject();
+ Log.i(TAG, "Deserialized object from " + file);
+ return result;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize object from " + file, x);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/HorizontalSlider.java b/subsonic-android/src/github/daneren2005/subdroid/util/HorizontalSlider.java
new file mode 100644
index 00000000..01e24fd5
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/HorizontalSlider.java
@@ -0,0 +1,141 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ProgressBar;
+import github.daneren2005.subdroid.R;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class HorizontalSlider extends ProgressBar {
+
+ private final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slider_knob);
+ private boolean slidingEnabled;
+ private OnSliderChangeListener listener;
+ private static final int PADDING = 2;
+ private boolean sliding;
+ private int sliderPosition;
+ private int startPosition;
+
+ public interface OnSliderChangeListener {
+ void onSliderChanged(View view, int position, boolean inProgress);
+ }
+
+ public HorizontalSlider(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public HorizontalSlider(Context context, AttributeSet attrs) {
+ super(context, attrs, android.R.attr.progressBarStyleHorizontal);
+ }
+
+ public HorizontalSlider(Context context) {
+ super(context);
+ }
+
+ public void setSlidingEnabled(boolean slidingEnabled) {
+ if (this.slidingEnabled != slidingEnabled) {
+ this.slidingEnabled = slidingEnabled;
+ invalidate();
+ }
+ }
+
+ public boolean isSlidingEnabled() {
+ return slidingEnabled;
+ }
+
+ public void setOnSliderChangeListener(OnSliderChangeListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int max = getMax();
+ if (!slidingEnabled || max == 0) {
+ return;
+ }
+
+ int paddingLeft = getPaddingLeft();
+ int paddingRight = getPaddingRight();
+ int paddingTop = getPaddingTop();
+ int paddingBottom = getPaddingBottom();
+
+ int w = getWidth() - paddingLeft - paddingRight;
+ int h = getHeight() - paddingTop - paddingBottom;
+ int position = sliding ? sliderPosition : getProgress();
+
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getWidth();
+ float x = paddingLeft + w * ((float) position / max) - bitmapWidth / 2.0F;
+ x = Math.max(x, paddingLeft);
+ x = Math.min(x, paddingLeft + w - bitmapWidth);
+ float y = paddingTop + h / 2.0F - bitmapHeight / 2.0F;
+
+ canvas.drawBitmap(bitmap, x, y, null);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!slidingEnabled) {
+ return false;
+ }
+
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ sliding = true;
+ startPosition = getProgress();
+ }
+
+ float x = event.getX() - PADDING;
+ float width = getWidth() - 2 * PADDING;
+ sliderPosition = Math.round((float) getMax() * (x / width));
+ sliderPosition = Math.max(sliderPosition, 0);
+
+ setProgress(Math.min(startPosition, sliderPosition));
+ setSecondaryProgress(Math.max(startPosition, sliderPosition));
+ if (listener != null) {
+ listener.onSliderChanged(this, sliderPosition, true);
+ }
+
+ } else if (action == MotionEvent.ACTION_UP) {
+ sliding = false;
+ setProgress(sliderPosition);
+ setSecondaryProgress(0);
+ if (listener != null) {
+ listener.onSliderChanged(this, sliderPosition, false);
+ }
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/ImageLoader.java b/subsonic-android/src/github/daneren2005/subdroid/util/ImageLoader.java
new file mode 100644
index 00000000..0ac08f1f
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/ImageLoader.java
@@ -0,0 +1,252 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.subdroid.R;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+import github.daneren2005.subdroid.service.MusicService;
+import github.daneren2005.subdroid.service.MusicServiceFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Asynchronous loading of images, with caching.
+ * <p/>
+ * There should normally be only one instance of this class.
+ *
+ * @author Sindre Mehus
+ */
+public class ImageLoader implements Runnable {
+
+ private static final String TAG = ImageLoader.class.getSimpleName();
+ private static final int CONCURRENCY = 5;
+
+ private final LRUCache<String, Drawable> cache = new LRUCache<String, Drawable>(100);
+ private final BlockingQueue<Task> queue;
+ private final int imageSizeDefault;
+ private final int imageSizeLarge;
+ private Drawable largeUnknownImage;
+
+ public ImageLoader(Context context) {
+ queue = new LinkedBlockingQueue<Task>(500);
+
+ // Determine the density-dependent image sizes.
+ imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels) * 0.6);
+
+ for (int i = 0; i < CONCURRENCY; i++) {
+ new Thread(this, "ImageLoader").start();
+ }
+
+ createLargeUnknownImage(context);
+ }
+
+ private void createLargeUnknownImage(Context context) {
+ BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large);
+ Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true);
+ bitmap = createReflection(bitmap);
+ largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap);
+ }
+
+ public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
+ if (entry == null || entry.getCoverArt() == null) {
+ setUnknownImage(view, large);
+ return;
+ }
+
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ Drawable drawable = cache.get(getKey(entry.getCoverArt(), size));
+ if (drawable != null) {
+ setImage(view, drawable, large);
+ return;
+ }
+
+ if (!large) {
+ setUnknownImage(view, large);
+ }
+ queue.offer(new Task(view, entry, size, large, large, crossfade));
+ }
+
+ private String getKey(String coverArtId, int size) {
+ return coverArtId + size;
+ }
+
+ private void setImage(View view, Drawable drawable, boolean crossfade) {
+ if (view instanceof TextView) {
+ // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though.
+ TextView textView = (TextView) view;
+ textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+ } else if (view instanceof ImageView) {
+ ImageView imageView = (ImageView) view;
+ if (crossfade) {
+
+ Drawable existingDrawable = imageView.getDrawable();
+ if (existingDrawable == null) {
+ Bitmap emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ existingDrawable = new BitmapDrawable(emptyImage);
+ }
+
+ Drawable[] layers = new Drawable[]{existingDrawable, drawable};
+
+ TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(250);
+ } else {
+ imageView.setImageDrawable(drawable);
+ }
+ }
+ }
+
+ private void setUnknownImage(View view, boolean large) {
+ if (large) {
+ setImage(view, largeUnknownImage, false);
+ } else {
+ if (view instanceof TextView) {
+ ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0);
+ } else if (view instanceof ImageView) {
+ ((ImageView) view).setImageResource(R.drawable.unknown_album);
+ }
+ }
+ }
+
+ public void clear() {
+ queue.clear();
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ Task task = queue.take();
+ task.execute();
+ } catch (Throwable x) {
+ Log.e(TAG, "Unexpected exception in ImageLoader.", x);
+ }
+ }
+ }
+
+ private Bitmap createReflection(Bitmap originalImage) {
+
+ int width = originalImage.getWidth();
+ int height = originalImage.getHeight();
+
+ // The gap we want between the reflection and the original image
+ final int reflectionGap = 4;
+
+ // This will not scale but will flip on the Y axis
+ Matrix matrix = new Matrix();
+ matrix.preScale(1, -1);
+
+ // Create a Bitmap with the flip matix applied to it.
+ // We only want the bottom half of the image
+ Bitmap reflectionImage = Bitmap.createBitmap(originalImage, 0, height / 2, width, height / 2, matrix, false);
+
+ // Create a new bitmap with same width but taller to fit reflection
+ Bitmap bitmapWithReflection = Bitmap.createBitmap(width, (height + height / 2), Bitmap.Config.ARGB_8888);
+
+ // Create a new Canvas with the bitmap that's big enough for
+ // the image plus gap plus reflection
+ Canvas canvas = new Canvas(bitmapWithReflection);
+
+ // Draw in the original image
+ canvas.drawBitmap(originalImage, 0, 0, null);
+
+ // Draw in the gap
+ Paint defaultPaint = new Paint();
+ canvas.drawRect(0, height, width, height + reflectionGap, defaultPaint);
+
+ // Draw in the reflection
+ canvas.drawBitmap(reflectionImage, 0, height + reflectionGap, null);
+
+ // Create a shader that is a linear gradient that covers the reflection
+ Paint paint = new Paint();
+ LinearGradient shader = new LinearGradient(0, originalImage.getHeight(), 0,
+ bitmapWithReflection.getHeight() + reflectionGap, 0x70000000, 0xff000000,
+ Shader.TileMode.CLAMP);
+
+ // Set the paint to use this shader (linear gradient)
+ paint.setShader(shader);
+
+ // Draw a rectangle using the paint with our linear gradient
+ canvas.drawRect(0, height, width, bitmapWithReflection.getHeight() + reflectionGap, paint);
+
+ return bitmapWithReflection;
+ }
+
+ private class Task {
+ private final View view;
+ private final MusicDirectory.Entry entry;
+ private final Handler handler;
+ private final int size;
+ private final boolean reflection;
+ private final boolean saveToFile;
+ private final boolean crossfade;
+
+ public Task(View view, MusicDirectory.Entry entry, int size, boolean reflection, boolean saveToFile, boolean crossfade) {
+ this.view = view;
+ this.entry = entry;
+ this.size = size;
+ this.reflection = reflection;
+ this.saveToFile = saveToFile;
+ this.crossfade = crossfade;
+ handler = new Handler();
+ }
+
+ public void execute() {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
+ Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, null);
+
+ if (reflection) {
+ bitmap = createReflection(bitmap);
+ }
+
+ final Drawable drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap);
+ cache.put(getKey(entry.getCoverArt(), size), drawable);
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ setImage(view, drawable, crossfade);
+ }
+ });
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ }
+ }
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/LRUCache.java b/subsonic-android/src/github/daneren2005/subdroid/util/LRUCache.java
new file mode 100644
index 00000000..f9516e04
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/LRUCache.java
@@ -0,0 +1,102 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Sindre Mehus
+ */
+public class LRUCache<K,V>{
+
+ private final int capacity;
+ private final Map<K, TimestampedValue> map;
+
+ public LRUCache(int capacity) {
+ map = new HashMap<K, TimestampedValue>(capacity);
+ this.capacity = capacity;
+ }
+
+ public synchronized V get(K key) {
+ TimestampedValue value = map.get(key);
+
+ V result = null;
+ if (value != null) {
+ value.updateTimestamp();
+ result = value.getValue();
+ }
+
+ return result;
+ }
+
+ public synchronized void put(K key, V value) {
+ if (map.size() >= capacity) {
+ removeOldest();
+ }
+ map.put(key, new TimestampedValue(value));
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ private void removeOldest() {
+ K oldestKey = null;
+ long oldestTimestamp = Long.MAX_VALUE;
+
+ for (Map.Entry<K, TimestampedValue> entry : map.entrySet()) {
+ K key = entry.getKey();
+ TimestampedValue value = entry.getValue();
+ if (value.getTimestamp() < oldestTimestamp) {
+ oldestTimestamp = value.getTimestamp();
+ oldestKey = key;
+ }
+ }
+
+ if (oldestKey != null) {
+ map.remove(oldestKey);
+ }
+ }
+
+ private final class TimestampedValue {
+
+ private final SoftReference<V> value;
+ private long timestamp;
+
+ public TimestampedValue(V value) {
+ this.value = new SoftReference<V>(value);
+ updateTimestamp();
+ }
+
+ public V getValue() {
+ return value.get();
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void updateTimestamp() {
+ timestamp = System.currentTimeMillis();
+ }
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/MergeAdapter.java b/subsonic-android/src/github/daneren2005/subdroid/util/MergeAdapter.java
new file mode 100644
index 00000000..c83cfb7b
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/MergeAdapter.java
@@ -0,0 +1,290 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package github.daneren2005.subdroid.util;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+/**
+ * Adapter that merges multiple child adapters and views
+ * into a single contiguous whole.
+ * <p/>
+ * Adapters used as pieces within MergeAdapter must
+ * have view type IDs monotonically increasing from 0. Ideally,
+ * adapters also have distinct ranges for their row ids, as
+ * returned by getItemId().
+ */
+public class MergeAdapter extends BaseAdapter {
+
+ private final CascadeDataSetObserver observer = new CascadeDataSetObserver();
+ private final ArrayList<ListAdapter> pieces = new ArrayList<ListAdapter>();
+
+ /**
+ * Stock constructor, simply chaining to the superclass.
+ */
+ public MergeAdapter() {
+ super();
+ }
+
+ /**
+ * Adds a new adapter to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param adapter Source for row views for this section
+ */
+ public void addAdapter(ListAdapter adapter) {
+ pieces.add(adapter);
+ adapter.registerDataSetObserver(observer);
+ }
+
+ public void removeAdapter(ListAdapter adapter) {
+ adapter.unregisterDataSetObserver(observer);
+ pieces.remove(adapter);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ */
+ public ListAdapter addView(View view) {
+ return addView(view, false);
+ }
+
+ /**
+ * Adds a new View to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param view Single view to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addView(View view, boolean enabled) {
+ return addViews(Arrays.asList(view), enabled);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ */
+ public ListAdapter addViews(List<View> views) {
+ return addViews(views, false);
+ }
+
+ /**
+ * Adds a list of views to the roster of things to appear
+ * in the aggregate list.
+ *
+ * @param views List of views to add
+ * @param enabled false if views are disabled, true if enabled
+ */
+ public ListAdapter addViews(List<View> views, boolean enabled) {
+ ListAdapter adapter = enabled ? new EnabledSackAdapter(views) : new SackOfViewsAdapter(views);
+ addAdapter(adapter);
+ return adapter;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItem(position));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getCount();
+ }
+
+ return (total);
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ int total = 0;
+
+ for (ListAdapter piece : pieces) {
+ total += piece.getViewTypeCount();
+ }
+
+ return (Math.max(total, 1)); // needed for setListAdapter() before content add'
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ int typeOffset = 0;
+ int result = -1;
+
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ result = typeOffset + piece.getItemViewType(position);
+ break;
+ }
+
+ position -= size;
+ typeOffset += piece.getViewTypeCount();
+ }
+
+ return (result);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.isEnabled(position));
+ }
+
+ position -= size;
+ }
+
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+
+ return (piece.getView(position, convertView, parent));
+ }
+
+ position -= size;
+ }
+
+ return (null);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ for (ListAdapter piece : pieces) {
+ int size = piece.getCount();
+
+ if (position < size) {
+ return (piece.getItemId(position));
+ }
+
+ position -= size;
+ }
+
+ return (-1);
+ }
+
+ private static class EnabledSackAdapter extends SackOfViewsAdapter {
+ public EnabledSackAdapter(List<View> views) {
+ super(views);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (true);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return (true);
+ }
+ }
+
+ private class CascadeDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/ModalBackgroundTask.java b/subsonic-android/src/github/daneren2005/subdroid/util/ModalBackgroundTask.java
new file mode 100644
index 00000000..0733355f
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/ModalBackgroundTask.java
@@ -0,0 +1,139 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.util.Log;
+import github.daneren2005.subdroid.R;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class ModalBackgroundTask<T> extends BackgroundTask<T> {
+
+ private static final String TAG = ModalBackgroundTask.class.getSimpleName();
+
+ private final AlertDialog progressDialog;
+ private Thread thread;
+ private final boolean finishActivityOnCancel;
+ private boolean cancelled;
+
+ public ModalBackgroundTask(Activity activity, boolean finishActivityOnCancel) {
+ super(activity);
+ this.finishActivityOnCancel = finishActivityOnCancel;
+ progressDialog = createProgressDialog();
+ }
+
+ public ModalBackgroundTask(Activity activity) {
+ this(activity, true);
+ }
+
+ private AlertDialog createProgressDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setIcon(android.R.drawable.ic_dialog_info);
+ builder.setTitle(R.string.background_task_wait);
+ builder.setMessage(R.string.background_task_loading);
+ builder.setCancelable(true);
+ builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ cancel();
+ }
+ });
+ builder.setPositiveButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ cancel();
+ }
+ });
+
+ return builder.create();
+ }
+
+ public void execute() {
+ cancelled = false;
+ progressDialog.show();
+
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (cancelled) {
+ progressDialog.dismiss();
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ progressDialog.dismiss();
+ done(result);
+ }
+ });
+
+ } catch (final Throwable t) {
+ if (cancelled) {
+ return;
+ }
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ progressDialog.dismiss();
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ protected void cancel() {
+ cancelled = true;
+ if (thread != null) {
+ thread.interrupt();
+ }
+
+ if (finishActivityOnCancel) {
+ getActivity().finish();
+ }
+ }
+
+ protected boolean isCancelled() {
+ return cancelled;
+ }
+
+ protected void error(Throwable error) {
+ Log.w(TAG, "Got exception: " + error, error);
+ new ErrorDialog(getActivity(), getErrorMessage(error), finishActivityOnCancel);
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ progressDialog.setMessage(message);
+ }
+ });
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/MyViewFlipper.java b/subsonic-android/src/github/daneren2005/subdroid/util/MyViewFlipper.java
new file mode 100644
index 00000000..31710593
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/MyViewFlipper.java
@@ -0,0 +1,53 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ViewFlipper;
+
+/**
+ * Work-around for Android Issue 6191 (http://code.google.com/p/android/issues/detail?id=6191)
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MyViewFlipper extends ViewFlipper {
+
+ public MyViewFlipper(Context context) {
+ super(context);
+ }
+
+ public MyViewFlipper(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+
+ @Override
+ protected void onDetachedFromWindow() {
+ try {
+ super.onDetachedFromWindow();
+ }
+ catch (IllegalArgumentException e) {
+ // Call stopFlipping() in order to kick off updateRunning()
+ stopFlipping();
+ }
+ }
+}
+
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/Pair.java b/subsonic-android/src/github/daneren2005/subdroid/util/Pair.java
new file mode 100644
index 00000000..e4bc7ee5
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/Pair.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Pair<S, T> implements Serializable {
+
+ private S first;
+ private T second;
+
+ public Pair() {
+ }
+
+ public Pair(S first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public S getFirst() {
+ return first;
+ }
+
+ public void setFirst(S first) {
+ this.first = first;
+ }
+
+ public T getSecond() {
+ return second;
+ }
+
+ public void setSecond(T second) {
+ this.second = second;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/PlaylistAdapter.java b/subsonic-android/src/github/daneren2005/subdroid/util/PlaylistAdapter.java
new file mode 100644
index 00000000..e446bf04
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/PlaylistAdapter.java
@@ -0,0 +1,99 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+import github.daneren2005.subdroid.R;
+import github.daneren2005.subdroid.domain.Playlist;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+* @author Sindre Mehus
+* @version $Id$
+*/
+public class PlaylistAdapter extends ArrayAdapter<Playlist> implements SectionIndexer {
+
+ // Both arrays are indexed by section ID.
+ private final Object[] sections;
+ private final Integer[] positions;
+
+ /**
+ * Note: playlists must be sorted alphabetically.
+ */
+ public PlaylistAdapter(Context context, List<Playlist> playlists) {
+ super(context, R.layout.playlist_list_item, playlists);
+
+ Set<String> sectionSet = new LinkedHashSet<String>(30);
+ List<Integer> positionList = new ArrayList<Integer>(30);
+ for (int i = 0; i < playlists.size(); i++) {
+ Playlist playlist = playlists.get(i);
+ if (playlist.getName().length() > 0) {
+ String index = playlist.getName().substring(0, 1).toUpperCase();
+ if (!sectionSet.contains(index)) {
+ sectionSet.add(index);
+ positionList.add(i);
+ }
+ }
+ }
+ sections = sectionSet.toArray(new Object[sectionSet.size()]);
+ positions = positionList.toArray(new Integer[positionList.size()]);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ section = Math.min(section, positions.length - 1);
+ return positions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int pos) {
+ for (int i = 0; i < sections.length - 1; i++) {
+ if (pos < positions[i + 1]) {
+ return i;
+ }
+ }
+ return sections.length - 1;
+ }
+
+ public static class PlaylistComparator implements Comparator<Playlist> {
+ @Override
+ public int compare(Playlist playlist1, Playlist playlist2) {
+ return playlist1.getName().compareToIgnoreCase(playlist2.getName());
+ }
+
+ public static List<Playlist> sort(List<Playlist> playlists) {
+ Collections.sort(playlists, new PlaylistComparator());
+ return playlists;
+ }
+
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/ProgressListener.java b/subsonic-android/src/github/daneren2005/subdroid/util/ProgressListener.java
new file mode 100644
index 00000000..1381eaf6
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/ProgressListener.java
@@ -0,0 +1,27 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface ProgressListener {
+ void updateProgress(String message);
+ void updateProgress(int messageId);
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/SackOfViewsAdapter.java b/subsonic-android/src/github/daneren2005/subdroid/util/SackOfViewsAdapter.java
new file mode 100644
index 00000000..645e3e49
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/SackOfViewsAdapter.java
@@ -0,0 +1,181 @@
+/***
+ Copyright (c) 2008-2009 CommonsWare, LLC
+ Portions (c) 2009 Google, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may
+ not use this file except in compliance with the License. You may obtain
+ a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package github.daneren2005.subdroid.util;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that simply returns row views from a list.
+ * <p/>
+ * If you supply a size, you must implement newView(), to
+ * create a required view. The adapter will then cache these
+ * views.
+ * <p/>
+ * If you supply a list of views in the constructor, that
+ * list will be used directly. If any elements in the list
+ * are null, then newView() will be called just for those
+ * slots.
+ * <p/>
+ * Subclasses may also wish to override areAllItemsEnabled()
+ * (default: false) and isEnabled() (default: false), if some
+ * of their rows should be selectable.
+ * <p/>
+ * It is assumed each view is unique, and therefore will not
+ * get recycled.
+ * <p/>
+ * Note that this adapter is not designed for long lists. It
+ * is more for screens that should behave like a list. This
+ * is particularly useful if you combine this with other
+ * adapters (e.g., SectionedAdapter) that might have an
+ * arbitrary number of rows, so it all appears seamless.
+ */
+public class SackOfViewsAdapter extends BaseAdapter {
+ private List<View> views = null;
+
+ /**
+ * Constructor creating an empty list of views, but with
+ * a specified count. Subclasses must override newView().
+ */
+ public SackOfViewsAdapter(int count) {
+ super();
+
+ views = new ArrayList<View>(count);
+
+ for (int i = 0; i < count; i++) {
+ views.add(null);
+ }
+ }
+
+ /**
+ * Constructor wrapping a supplied list of views.
+ * Subclasses must override newView() if any of the elements
+ * in the list are null.
+ */
+ public SackOfViewsAdapter(List<View> views) {
+ for (View view : views) {
+ view.setLayoutParams(new ListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+ this.views = views;
+ }
+
+ /**
+ * Get the data item associated with the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public Object getItem(int position) {
+ return (views.get(position));
+ }
+
+ /**
+ * How many items are in the data set represented by this
+ * Adapter.
+ */
+ @Override
+ public int getCount() {
+ return (views.size());
+ }
+
+ /**
+ * Returns the number of types of Views that will be
+ * created by getView().
+ */
+ @Override
+ public int getViewTypeCount() {
+ return (getCount());
+ }
+
+ /**
+ * Get the type of View that will be created by getView()
+ * for the specified item.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public int getItemViewType(int position) {
+ return (position);
+ }
+
+ /**
+ * Are all items in this ListAdapter enabled? If yes it
+ * means all items are selectable and clickable.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ return (false);
+ }
+
+ /**
+ * Returns true if the item at the specified position is
+ * not a separator.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return (false);
+ }
+
+ /**
+ * Get a View that displays the data at the specified
+ * position in the data set.
+ *
+ * @param position Position of the item whose data we want
+ * @param convertView View to recycle, if not null
+ * @param parent ViewGroup containing the returned View
+ */
+ @Override
+ public View getView(int position, View convertView,
+ ViewGroup parent) {
+ View result = views.get(position);
+
+ if (result == null) {
+ result = newView(position, parent);
+ views.set(position, result);
+ }
+
+ return (result);
+ }
+
+ /**
+ * Get the row id associated with the specified position
+ * in the list.
+ *
+ * @param position Position of the item whose data we want
+ */
+ @Override
+ public long getItemId(int position) {
+ return (position);
+ }
+
+ /**
+ * Create a new View to go into the list at the specified
+ * position.
+ *
+ * @param position Position of the item whose data we want
+ * @param parent ViewGroup containing the returned View
+ */
+ protected View newView(int position, ViewGroup parent) {
+ throw new RuntimeException("You must override newView()!");
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/ShufflePlayBuffer.java b/subsonic-android/src/github/daneren2005/subdroid/util/ShufflePlayBuffer.java
new file mode 100644
index 00000000..a4108f0a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/ShufflePlayBuffer.java
@@ -0,0 +1,109 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.content.Context;
+import android.util.Log;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+import github.daneren2005.subdroid.service.MusicService;
+import github.daneren2005.subdroid.service.MusicServiceFactory;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ShufflePlayBuffer {
+
+ private static final String TAG = ShufflePlayBuffer.class.getSimpleName();
+ private static final int CAPACITY = 50;
+ private static final int REFILL_THRESHOLD = 40;
+
+ private final ScheduledExecutorService executorService;
+ private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
+ private Context context;
+ private int currentServer;
+
+ public ShufflePlayBuffer(Context context) {
+ this.context = context;
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+ executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
+ }
+
+ public List<MusicDirectory.Entry> get(int size) {
+ clearBufferIfnecessary();
+
+ List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size);
+ synchronized (buffer) {
+ while (!buffer.isEmpty() && result.size() < size) {
+ result.add(buffer.remove(buffer.size() - 1));
+ }
+ }
+ Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining.");
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ }
+
+ private void refill() {
+
+ // Check if active server has changed.
+ clearBufferIfnecessary();
+
+ if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected(context) && !Util.isOffline(context))) {
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+ int n = CAPACITY - buffer.size();
+ MusicDirectory songs = service.getRandomSongs(n, context, null);
+
+ synchronized (buffer) {
+ buffer.addAll(songs.getChildren());
+ Log.i(TAG, "Refilled shuffle play buffer with " + songs.getChildren().size() + " songs.");
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to refill shuffle play buffer.", x);
+ }
+ }
+
+ private void clearBufferIfnecessary() {
+ synchronized (buffer) {
+ if (currentServer != Util.getActiveServer(context)) {
+ currentServer = Util.getActiveServer(context);
+ buffer.clear();
+ }
+ }
+ }
+
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/SilentBackgroundTask.java b/subsonic-android/src/github/daneren2005/subdroid/util/SilentBackgroundTask.java
new file mode 100644
index 00000000..b8592a66
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/SilentBackgroundTask.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.app.Activity;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class SilentBackgroundTask<T> extends BackgroundTask<T> {
+
+ public SilentBackgroundTask(Activity activity) {
+ super(activity);
+ }
+
+ @Override
+ public void execute() {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ done(result);
+ }
+ });
+
+ } catch (final Throwable t) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ error(t);
+ }
+ });
+ }
+ }
+ };
+ thread.start();
+ }
+
+ @Override
+ public void updateProgress(int messageId) {
+ }
+
+ @Override
+ public void updateProgress(String message) {
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/SimpleServiceBinder.java b/subsonic-android/src/github/daneren2005/subdroid/util/SimpleServiceBinder.java
new file mode 100644
index 00000000..25b46b5a
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/SimpleServiceBinder.java
@@ -0,0 +1,37 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.os.Binder;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SimpleServiceBinder<S> extends Binder {
+
+ private final S service;
+
+ public SimpleServiceBinder(S service) {
+ this.service = service;
+ }
+
+ public S getService() {
+ return service;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/SongView.java b/subsonic-android/src/github/daneren2005/subdroid/util/SongView.java
new file mode 100644
index 00000000..7c29c331
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/SongView.java
@@ -0,0 +1,178 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.CheckedTextView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import github.daneren2005.subdroid.R;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+import github.daneren2005.subdroid.service.DownloadService;
+import github.daneren2005.subdroid.service.DownloadServiceImpl;
+import github.daneren2005.subdroid.service.DownloadFile;
+
+import java.io.File;
+import java.util.WeakHashMap;
+
+/**
+ * Used to display songs in a {@code ListView}.
+ *
+ * @author Sindre Mehus
+ */
+public class SongView extends LinearLayout implements Checkable {
+
+ private static final String TAG = SongView.class.getSimpleName();
+ private static final WeakHashMap<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>();
+ private static Handler handler;
+
+ private CheckedTextView checkedTextView;
+ private TextView titleTextView;
+ private TextView artistTextView;
+ private TextView durationTextView;
+ private TextView statusTextView;
+ private MusicDirectory.Entry song;
+
+ public SongView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true);
+
+ checkedTextView = (CheckedTextView) findViewById(R.id.song_check);
+ titleTextView = (TextView) findViewById(R.id.song_title);
+ artistTextView = (TextView) findViewById(R.id.song_artist);
+ durationTextView = (TextView) findViewById(R.id.song_duration);
+ statusTextView = (TextView) findViewById(R.id.song_status);
+
+ INSTANCES.put(this, null);
+ int instanceCount = INSTANCES.size();
+ if (instanceCount > 50) {
+ Log.w(TAG, instanceCount + " live SongView instances");
+ }
+ startUpdater();
+ }
+
+ public void setSong(MusicDirectory.Entry song, boolean checkable) {
+ this.song = song;
+ StringBuilder artist = new StringBuilder(40);
+
+ String bitRate = null;
+ if (song.getBitRate() != null) {
+ bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate());
+ }
+
+ String fileFormat = null;
+ if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) {
+ fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix());
+ } else {
+ fileFormat = song.getSuffix();
+ }
+
+ artist.append(song.getArtist()).append(" (")
+ .append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat))
+ .append(")");
+
+ titleTextView.setText(song.getTitle());
+ artistTextView.setText(artist);
+ durationTextView.setText(Util.formatDuration(song.getDuration()));
+ checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE);
+
+ update();
+ }
+
+ private void update() {
+ DownloadService downloadService = DownloadServiceImpl.getInstance();
+ if (downloadService == null) {
+ return;
+ }
+
+ DownloadFile downloadFile = downloadService.forSong(song);
+ File completeFile = downloadFile.getCompleteFile();
+ File partialFile = downloadFile.getPartialFile();
+
+ int leftImage = 0;
+ int rightImage = 0;
+
+ if (completeFile.exists()) {
+ leftImage = downloadFile.isSaved() ? R.drawable.saved : R.drawable.downloaded;
+ }
+
+ if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) {
+ statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext()));
+ rightImage = R.drawable.downloading;
+ } else {
+ statusTextView.setText(null);
+ }
+ statusTextView.setCompoundDrawablesWithIntrinsicBounds(leftImage, 0, rightImage, 0);
+
+ boolean playing = downloadService.getCurrentPlaying() == downloadFile;
+ if (playing) {
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.stat_notify_playing, 0, 0, 0);
+ } else {
+ titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+
+ private static synchronized void startUpdater() {
+ if (handler != null) {
+ return;
+ }
+
+ handler = new Handler();
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ updateAll();
+ handler.postDelayed(this, 1000L);
+ }
+ };
+ handler.postDelayed(runnable, 1000L);
+ }
+
+ private static void updateAll() {
+ try {
+ for (SongView view : INSTANCES.keySet()) {
+ if (view.isShown()) {
+ view.update();
+ }
+ }
+ } catch (Throwable x) {
+ Log.w(TAG, "Error when updating song views.", x);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean b) {
+ checkedTextView.setChecked(b);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return checkedTextView.isChecked();
+ }
+
+ @Override
+ public void toggle() {
+ checkedTextView.toggle();
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/TabActivityBackgroundTask.java b/subsonic-android/src/github/daneren2005/subdroid/util/TabActivityBackgroundTask.java
new file mode 100644
index 00000000..0372920f
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/TabActivityBackgroundTask.java
@@ -0,0 +1,67 @@
+package github.daneren2005.subdroid.util;
+
+import github.daneren2005.subdroid.activity.SubsonicTabActivity;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T> {
+
+ private final SubsonicTabActivity tabActivity;
+
+ public TabActivityBackgroundTask(SubsonicTabActivity activity) {
+ super(activity);
+ tabActivity = activity;
+ }
+
+ @Override
+ public void execute() {
+ tabActivity.setProgressVisible(true);
+
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ final T result = doInBackground();
+ if (isCancelled()) {
+ return;
+ }
+
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabActivity.setProgressVisible(false);
+ done(result);
+ }
+ });
+ } catch (final Throwable t) {
+ if (isCancelled()) {
+ return;
+ }
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabActivity.setProgressVisible(false);
+ error(t);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ private boolean isCancelled() {
+ return tabActivity.isDestroyed();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabActivity.updateProgress(message);
+ }
+ });
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/TimeLimitedCache.java b/subsonic-android/src/github/daneren2005/subdroid/util/TimeLimitedCache.java
new file mode 100644
index 00000000..0dd1efaf
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/TimeLimitedCache.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class TimeLimitedCache<T> {
+
+ private SoftReference<T> value;
+ private final long ttlMillis;
+ private long expires;
+
+ public TimeLimitedCache(long ttl, TimeUnit timeUnit) {
+ this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit);
+ }
+
+ public T get() {
+ return System.currentTimeMillis() < expires ? value.get() : null;
+ }
+
+ public void set(T value) {
+ set(value, ttlMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void set(T value, long ttl, TimeUnit timeUnit) {
+ this.value = new SoftReference<T>(value);
+ expires = System.currentTimeMillis() + timeUnit.toMillis(ttl);
+ }
+
+ public void clear() {
+ expires = 0L;
+ value = null;
+ }
+}
diff --git a/subsonic-android/src/github/daneren2005/subdroid/util/Util.java b/subsonic-android/src/github/daneren2005/subdroid/util/Util.java
new file mode 100644
index 00000000..178afd28
--- /dev/null
+++ b/subsonic-android/src/github/daneren2005/subdroid/util/Util.java
@@ -0,0 +1,852 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.subdroid.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Environment;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.view.KeyEvent;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+import android.widget.Toast;
+import github.daneren2005.subdroid.R;
+import github.daneren2005.subdroid.activity.DownloadActivity;
+import github.daneren2005.subdroid.domain.MusicDirectory;
+import github.daneren2005.subdroid.domain.PlayerState;
+import github.daneren2005.subdroid.domain.RepeatMode;
+import github.daneren2005.subdroid.domain.Version;
+import github.daneren2005.subdroid.provider.SubsonicAppWidgetProvider1;
+import github.daneren2005.subdroid.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.subdroid.service.DownloadServiceImpl;
+import org.apache.http.HttpEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.security.MessageDigest;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Util {
+
+ private static final String TAG = Util.class.getSimpleName();
+
+ private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB");
+ private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB");
+ private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB");
+
+ private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null;
+ private static DecimalFormat BYTE_LOCALIZED_FORMAT = null;
+
+ public static final String EVENT_META_CHANGED = "github.daneren2005.subdroid.EVENT_META_CHANGED";
+ public static final String EVENT_PLAYSTATE_CHANGED = "github.daneren2005.subdroid.EVENT_PLAYSTATE_CHANGED";
+
+ private static final Map<Integer, Version> SERVER_REST_VERSIONS = new ConcurrentHashMap<Integer, Version>();
+
+ // Used by hexEncode()
+ private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ private final static Pair<Integer, Integer> NOTIFICATION_TEXT_COLORS = new Pair<Integer, Integer>();
+ private static Toast toast;
+
+ private Util() {
+ }
+
+ public static boolean isOffline(Context context) {
+ return getActiveServer(context) == 0;
+ }
+
+ public static boolean isScreenLitOnDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false);
+ }
+
+ public static RepeatMode getRepeatMode(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name()));
+ }
+
+ public static void setRepeatMode(Context context, RepeatMode repeatMode) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name());
+ editor.commit();
+ }
+
+ public static boolean isScrobblingEnabled(Context context) {
+ if (isOffline(context)) {
+ return false;
+ }
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false);
+ }
+
+ public static void setActiveServer(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
+ editor.commit();
+ }
+
+ public static int getActiveServer(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ }
+
+ public static String getServerName(Context context, int instance) {
+ if (instance == 0) {
+ return context.getResources().getString(R.string.main_offline);
+ }
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+
+ public static void setServerRestVersion(Context context, Version version) {
+ SERVER_REST_VERSIONS.put(getActiveServer(context), version);
+ }
+
+ public static Version getServerRestVersion(Context context) {
+ return SERVER_REST_VERSIONS.get(getActiveServer(context));
+ }
+
+ public static void setSelectedMusicFolderId(Context context, String musicFolderId) {
+ int instance = getActiveServer(context);
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
+ editor.commit();
+ }
+
+ public static String getSelectedMusicFolderId(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = getActiveServer(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ }
+
+ public static String getTheme(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_THEME, null);
+ }
+
+ public static int getMaxBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getPreloadCount(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int preloadCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_PRELOAD_COUNT, "-1"));
+ return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount;
+ }
+
+ public static int getCacheSizeMB(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1"));
+ return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize;
+ }
+
+ public static String getRestUrl(Context context, String method) {
+ StringBuilder builder = new StringBuilder();
+
+ SharedPreferences prefs = getPreferences(context);
+
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+
+ // Slightly obfuscate password
+ password = "enc:" + Util.utf8HexEncode(password);
+
+ builder.append(serverUrl);
+ if (builder.charAt(builder.length() - 1) != '/') {
+ builder.append("/");
+ }
+ builder.append("rest/").append(method).append(".view");
+ builder.append("?u=").append(username);
+ builder.append("&p=").append(password);
+ builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION);
+ builder.append("&c=").append(Constants.REST_CLIENT_ID);
+
+ return builder.toString();
+ }
+
+ public static SharedPreferences getPreferences(Context context) {
+ return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0);
+ }
+
+ public static String getContentType(HttpEntity entity) {
+ if (entity == null || entity.getContentType() == null) {
+ return null;
+ }
+ return entity.getContentType().getValue();
+ }
+
+ public static int getRemainingTrialDays(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L);
+
+ if (installTime == 0L) {
+ installTime = System.currentTimeMillis();
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime);
+ editor.commit();
+ }
+
+ long now = System.currentTimeMillis();
+ long millisPerDay = 24L * 60L * 60L * 1000L;
+ int daysSinceInstall = (int) ((now - installTime) / millisPerDay);
+ return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall);
+ }
+
+ /**
+ * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
+ * <p/>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ public static long copy(InputStream input, OutputStream output)
+ throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ long count = 0;
+ int n;
+ while (-1 != (n = input.read(buffer))) {
+ output.write(buffer, 0, n);
+ count += n;
+ }
+ return count;
+ }
+
+ public static void atomicCopy(File from, File to) throws IOException {
+ FileInputStream in = null;
+ FileOutputStream out = null;
+ File tmp = null;
+ try {
+ tmp = new File(to.getPath() + ".tmp");
+ in = new FileInputStream(from);
+ out = new FileOutputStream(tmp);
+ in.getChannel().transferTo(0, from.length(), out.getChannel());
+ out.close();
+ if (!tmp.renameTo(to)) {
+ throw new IOException("Failed to rename " + tmp + " to " + to);
+ }
+ Log.i(TAG, "Copied " + from + " to " + to);
+ } catch (IOException x) {
+ close(out);
+ delete(to);
+ throw x;
+ } finally {
+ close(in);
+ close(out);
+ delete(tmp);
+ }
+ }
+
+ public static void close(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static boolean delete(File file) {
+ if (file != null && file.exists()) {
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete file " + file);
+ return false;
+ }
+ Log.i(TAG, "Deleted file " + file);
+ }
+ return true;
+ }
+
+ public static void toast(Context context, int messageId) {
+ toast(context, messageId, true);
+ }
+
+ public static void toast(Context context, int messageId, boolean shortDuration) {
+ toast(context, context.getString(messageId), shortDuration);
+ }
+
+ public static void toast(Context context, String message) {
+ toast(context, message, true);
+ }
+
+ public static void toast(Context context, String message, boolean shortDuration) {
+ if (toast == null) {
+ toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ } else {
+ toast.setText(message);
+ toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ }
+ toast.show();
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * To get a localized string, please use formatLocalizedBytes instead.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatBytes(long byteCount) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT;
+ return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ NumberFormat megaByteFormat = MEGA_BYTE_FORMAT;
+ return megaByteFormat.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ NumberFormat kiloByteFormat = KILO_BYTE_FORMAT;
+ return kiloByteFormat.format((double) byteCount / 1024);
+ }
+
+ return byteCount + " B";
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * This version of the method returns a localized string.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatLocalizedBytes(long byteCount, Context context) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
+ GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte));
+ }
+
+ return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
+ MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte));
+ }
+
+ return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ if (KILO_BYTE_LOCALIZED_FORMAT == null) {
+ KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte));
+ }
+
+ return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024);
+ }
+
+ if (BYTE_LOCALIZED_FORMAT == null) {
+ BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte));
+ }
+
+ return BYTE_LOCALIZED_FORMAT.format((double) byteCount);
+ }
+
+ public static String formatDuration(Integer seconds) {
+ if (seconds == null) {
+ return null;
+ }
+
+ int minutes = seconds / 60;
+ int secs = seconds % 60;
+
+ StringBuilder builder = new StringBuilder(6);
+ builder.append(minutes).append(":");
+ if (secs < 10) {
+ builder.append("0");
+ }
+ builder.append(secs);
+ return builder.toString();
+ }
+
+ public static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 == null || object2 == null) {
+ return false;
+ }
+ return object1.equals(object2);
+
+ }
+
+ /**
+ * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to encode.
+ * @return The encoded string.
+ */
+ public static String utf8HexEncode(String s) {
+ if (s == null) {
+ return null;
+ }
+ byte[] utf8;
+ try {
+ utf8 = s.getBytes(Constants.UTF_8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ return hexEncode(utf8);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data Bytes to convert to hexadecimal characters.
+ * @return A string containing hexadecimal characters.
+ */
+ public static String hexEncode(byte[] data) {
+ int length = data.length;
+ char[] out = new char[length << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < length; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param s Data to digest.
+ * @return MD5 digest as a hex string.
+ */
+ public static String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return hexEncode(md5.digest(s.getBytes(Constants.UTF_8)));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ public static boolean isNetworkConnected(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+
+ boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ boolean wifiRequired = isWifiRequiredForDownload(context);
+
+ return connected && (!wifiRequired || wifiConnected);
+ }
+
+ public static boolean isExternalStoragePresent() {
+ return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ }
+
+ private static boolean isWifiRequiredForDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false);
+ }
+
+ public static void info(Context context, int titleId, int messageId) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId);
+ }
+
+ private static void showDialog(Context context, int icon, int titleId, int messageId) {
+ new AlertDialog.Builder(context)
+ .setIcon(icon)
+ .setTitle(titleId)
+ .setMessage(messageId)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+ }
+
+ public static void showPlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler, MusicDirectory.Entry song) {
+
+ // Use the same text for the ticker and the expanded notification
+ String title = song.getTitle();
+ String arist = song.getArtist();
+ String album = song.getAlbum();
+
+ // Set the icon, scrolling text and timestamp
+ final Notification notification = new Notification(R.drawable.stat_notify_playing, title, System.currentTimeMillis());
+ notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+
+ RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.notification);
+
+ // Set the album art.
+ try {
+ int size = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size);
+ if (bitmap == null) {
+ // set default album art
+ contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ } else {
+ contentView.setImageViewBitmap(R.id.notification_image, bitmap);
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to get notification cover art", x);
+ contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ }
+
+ // set the text for the notifications
+ contentView.setTextViewText(R.id.notification_title, title);
+ contentView.setTextViewText(R.id.notification_artist, arist);
+ contentView.setTextViewText(R.id.notification_album, album);
+
+ Pair<Integer, Integer> colors = getNotificationTextColors(context);
+ if (colors.getFirst() != null) {
+ contentView.setTextColor(R.id.notification_title, colors.getFirst());
+ }
+ if (colors.getSecond() != null) {
+ contentView.setTextColor(R.id.notification_artist, colors.getSecond());
+ }
+
+ notification.contentView = contentView;
+
+ Intent notificationIntent = new Intent(context, DownloadActivity.class);
+ notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
+
+ // Create actions for media buttons
+ PendingIntent pendingIntent;
+ Intent prevIntent = new Intent("1");
+ prevIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ contentView.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
+
+ Intent pauseIntent = new Intent("2");
+ pauseIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0);
+ contentView.setOnClickPendingIntent(R.id.control_pause, pendingIntent);
+
+ Intent nextIntent = new Intent("3");
+ nextIntent.setComponent(new ComponentName(context, DownloadServiceImpl.class));
+ nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
+ pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0);
+ contentView.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+
+ // Send the notification and put the service in the foreground.
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ startForeground(downloadService, Constants.NOTIFICATION_ID_PLAYING, notification);
+ }
+ });
+
+ // Update widget
+ SubsonicAppWidgetProvider1.getInstance().notifyChange(context, downloadService, true);
+ }
+
+ public static void hidePlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler) {
+
+ // Remove notification and remove the service from the foreground
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ stopForeground(downloadService, true);
+ }
+ });
+
+ // Update widget
+ SubsonicAppWidgetProvider1.getInstance().notifyChange(context, downloadService, false);
+ }
+
+ public static void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException x) {
+ Log.w(TAG, "Interrupted from sleep.", x);
+ }
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivitiy) {
+ startActivityWithoutTransition(currentActivity, new Intent(currentActivity, newActivitiy));
+ }
+
+ public static void startActivityWithoutTransition(Activity currentActivity, Intent intent) {
+ currentActivity.startActivity(intent);
+ disablePendingTransition(currentActivity);
+ }
+
+ public static void disablePendingTransition(Activity activity) {
+
+ // Activity.overridePendingTransition() was introduced in Android 2.0. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Method method = Activity.class.getMethod("overridePendingTransition", int.class, int.class);
+ method.invoke(activity, 0, 0);
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static Drawable createDrawableFromBitmap(Context context, Bitmap bitmap) {
+ // BitmapDrawable(Resources, Bitmap) was introduced in Android 1.6. Use reflection to maintain
+ // compatibility with 1.5.
+ try {
+ Constructor<BitmapDrawable> constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class);
+ return constructor.newInstance(context.getResources(), bitmap);
+ } catch (Throwable x) {
+ return new BitmapDrawable(bitmap);
+ }
+ }
+
+ public static void registerMediaButtonEventReceiver(Context context) {
+
+ // Only do it if enabled in the settings.
+ SharedPreferences prefs = getPreferences(context);
+ boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true);
+
+ if (enabled) {
+
+ // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+ }
+
+ public static void unregisterMediaButtonEventReceiver(Context context) {
+ // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+
+ private static void startForeground(Service service, int notificationId, Notification notification) {
+ // Service.startForeground() was introduced in Android 2.0.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ Method method = Service.class.getMethod("startForeground", int.class, Notification.class);
+ method.invoke(service, notificationId, notification);
+ Log.i(TAG, "Successfully invoked Service.startForeground()");
+ } catch (Throwable x) {
+ NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(Constants.NOTIFICATION_ID_PLAYING, notification);
+ Log.i(TAG, "Service.startForeground() not available. Using work-around.");
+ }
+ }
+
+ private static void stopForeground(Service service, boolean removeNotification) {
+ // Service.stopForeground() was introduced in Android 2.0.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ Method method = Service.class.getMethod("stopForeground", boolean.class);
+ method.invoke(service, removeNotification);
+ Log.i(TAG, "Successfully invoked Service.stopForeground()");
+ } catch (Throwable x) {
+ NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(Constants.NOTIFICATION_ID_PLAYING);
+ Log.i(TAG, "Service.stopForeground() not available. Using work-around.");
+ }
+ }
+
+ /**
+ * <p>Broadcasts the given song info as the new song being played.</p>
+ */
+ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) {
+ Intent intent = new Intent(EVENT_META_CHANGED);
+
+ if (song != null) {
+ intent.putExtra("title", song.getTitle());
+ intent.putExtra("artist", song.getArtist());
+ intent.putExtra("album", song.getAlbum());
+
+ File albumArtFile = FileUtil.getAlbumArtFile(context, song);
+ intent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ } else {
+ intent.putExtra("title", "");
+ intent.putExtra("artist", "");
+ intent.putExtra("album", "");
+ intent.putExtra("coverart", "");
+ }
+
+ context.sendBroadcast(intent);
+ }
+
+ /**
+ * <p>Broadcasts the given player state as the one being set.</p>
+ */
+ public static void broadcastPlaybackStatusChange(Context context, PlayerState state) {
+ Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED);
+
+ switch (state) {
+ case STARTED:
+ intent.putExtra("state", "play");
+ break;
+ case STOPPED:
+ intent.putExtra("state", "stop");
+ break;
+ case PAUSED:
+ intent.putExtra("state", "pause");
+ break;
+ case COMPLETED:
+ intent.putExtra("state", "complete");
+ break;
+ default:
+ return; // No need to broadcast.
+ }
+
+ context.sendBroadcast(intent);
+ }
+
+ /**
+ * Resolves the default text color for notifications.
+ *
+ * Based on http://stackoverflow.com/questions/4867338/custom-notification-layouts-and-text-colors/7320604#7320604
+ */
+ private static Pair<Integer, Integer> getNotificationTextColors(Context context) {
+ if (NOTIFICATION_TEXT_COLORS.getFirst() == null && NOTIFICATION_TEXT_COLORS.getSecond() == null) {
+ try {
+ Notification notification = new Notification();
+ String title = "title";
+ String content = "content";
+ notification.setLatestEventInfo(context, title, content, null);
+ LinearLayout group = new LinearLayout(context);
+ ViewGroup event = (ViewGroup) notification.contentView.apply(context, group);
+ findNotificationTextColors(event, title, content);
+ group.removeAllViews();
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to resolve notification text colors.", x);
+ }
+ }
+ return NOTIFICATION_TEXT_COLORS;
+ }
+
+ private static void findNotificationTextColors(ViewGroup group, String title, String content) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ if (group.getChildAt(i) instanceof TextView) {
+ TextView textView = (TextView) group.getChildAt(i);
+ String text = textView.getText().toString();
+ if (title.equals(text)) {
+ NOTIFICATION_TEXT_COLORS.setFirst(textView.getTextColors().getDefaultColor());
+ }
+ else if (content.equals(text)) {
+ NOTIFICATION_TEXT_COLORS.setSecond(textView.getTextColors().getDefaultColor());
+ }
+ }
+ else if (group.getChildAt(i) instanceof ViewGroup)
+ findNotificationTextColors((ViewGroup) group.getChildAt(i), title, content);
+ }
+ }
+}