aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/github/daneren2005/dsub/util
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2015-04-25 17:03:02 -0700
committerScott Jackson <daneren2005@gmail.com>2015-04-25 17:03:05 -0700
commitcfd014d38cba03ba05f571597b361ab253bff578 (patch)
tree4256723561dec7ef3ed3507382eb7020724ec570 /app/src/main/java/github/daneren2005/dsub/util
parent8a332a20ec272d59fe74520825b18017a8f0cac3 (diff)
downloaddsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.gz
dsub-cfd014d38cba03ba05f571597b361ab253bff578.tar.bz2
dsub-cfd014d38cba03ba05f571597b361ab253bff578.zip
Update to gradle
Diffstat (limited to 'app/src/main/java/github/daneren2005/dsub/util')
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java148
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java307
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java292
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Constants.java206
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/FileUtil.java860
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java600
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java73
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java181
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Notifications.java348
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Pair.java54
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java27
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java31
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java212
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java48
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java37
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java222
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java51
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java55
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/UserUtil.java452
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/Util.java1339
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java57
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java43
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java32
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java104
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java58
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java73
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/Common.java111
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java85
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java176
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java70
-rw-r--r--app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java114
32 files changed, 6551 insertions, 0 deletions
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
new file mode 100644
index 00000000..6e9b8309
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ArtistRadioBuffer.java
@@ -0,0 +1,148 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+public class ArtistRadioBuffer {
+ private static final String TAG = ArtistRadioBuffer.class.getSimpleName();
+
+ private ScheduledExecutorService executorService;
+ private Runnable runnable;
+ private final ArrayList<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
+ private int lastCount = -1;
+ private DownloadService context;
+ private boolean awaitingResults = false;
+ private int capacity;
+ private int refillThreshold;
+
+ private String artistId;
+
+ public ArtistRadioBuffer(DownloadService context) {
+ this.context = context;
+ runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+
+ // Calculate out the capacity and refill threshold based on the user's random size preference
+ int shuffleListSize = Integer.parseInt(Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ // ex: default 20 -> 50
+ capacity = shuffleListSize * 5 / 2;
+ capacity = Math.min(500, capacity);
+
+ // ex: default 20 -> 40
+ refillThreshold = capacity * 4 / 5;
+ }
+
+ public void setArtist(String artistId) {
+ if(!Util.equals(this.artistId, artistId)) {
+ buffer.clear();
+ }
+
+ context.clear();
+ this.artistId = artistId;
+ awaitingResults = true;
+ refill();
+ }
+ public void restoreArtist(String artistId) {
+ this.artistId = artistId;
+ awaitingResults = false;
+ restart();
+ }
+
+ public List<MusicDirectory.Entry> get(int size) {
+ // Make sure fetcher is running if needed
+ restart();
+
+ 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 artist radio buffer. " + buffer.size() + " remaining.");
+ if(result.isEmpty()) {
+ awaitingResults = true;
+ }
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ }
+
+ private void restart() {
+ synchronized(buffer) {
+ if(buffer.size() <= refillThreshold && lastCount != 0 && (executorService == null || executorService.isShutdown())) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0, 10, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ private void refill() {
+ if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) {
+ executorService.shutdown();
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+
+ // Get capacity based
+ int n = capacity - buffer.size();
+ MusicDirectory songs = service.getRandomSongs(n, artistId, context, null);
+
+ synchronized (buffer) {
+ lastCount = 0;
+ for(MusicDirectory.Entry entry: songs.getChildren()) {
+ if(!buffer.contains(entry) && entry.getRating() != 1) {
+ buffer.add(entry);
+ lastCount++;
+ }
+ }
+ Log.i(TAG, "Refilled artist radio buffer with " + lastCount + " songs.");
+ }
+ } catch (Exception x) {
+ // Give it one more try before quitting
+ if(lastCount != -2) {
+ lastCount = -2;
+ } else if(lastCount == -2) {
+ lastCount = 0;
+ }
+ Log.w(TAG, "Failed to refill artist radio buffer.", x);
+ }
+
+ if(awaitingResults) {
+ awaitingResults = false;
+ context.checkDownloads();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
new file mode 100644
index 00000000..9b39ac82
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/BackgroundTask.java
@@ -0,0 +1,307 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.view.ErrorDialog;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class BackgroundTask<T> implements ProgressListener {
+ private static final String TAG = BackgroundTask.class.getSimpleName();
+
+ private final Context context;
+ protected AtomicBoolean cancelled = new AtomicBoolean(false);
+ protected OnCancelListener cancelListener;
+ protected Runnable onCompletionListener = null;
+ protected Task task;
+
+ private static final int DEFAULT_CONCURRENCY = 8;
+ private static final Collection<Thread> threads = Collections.synchronizedCollection(new ArrayList<Thread>());
+ protected static final BlockingQueue<BackgroundTask.Task> queue = new LinkedBlockingQueue<BackgroundTask.Task>(10);
+ private static Handler handler = null;
+ static {
+ try {
+ handler = new Handler(Looper.getMainLooper());
+ } catch(Exception e) {
+ // Not called from main thread
+ }
+ }
+
+ public BackgroundTask(Context context) {
+ this.context = context;
+
+ if(threads.size() < DEFAULT_CONCURRENCY) {
+ for(int i = threads.size(); i < DEFAULT_CONCURRENCY; i++) {
+ Thread thread = new Thread(new TaskRunnable(), String.format("BackgroundTask_%d", i));
+ threads.add(thread);
+ thread.start();
+ }
+ }
+ if(handler == null) {
+ try {
+ handler = new Handler(Looper.getMainLooper());
+ } catch(Exception e) {
+ // Not called from main thread
+ }
+ }
+ }
+
+ public static void stopThreads() {
+ for(Thread thread: threads) {
+ thread.interrupt();
+ }
+ threads.clear();
+ queue.clear();
+ }
+
+ protected Activity getActivity() {
+ return (context instanceof Activity) ? ((Activity) context) : null;
+ }
+
+ 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);
+ Activity activity = getActivity();
+ if(activity != null) {
+ new ErrorDialog(activity, getErrorMessage(error), true);
+ }
+ }
+
+ protected String getErrorMessage(Throwable error) {
+
+ if (error instanceof IOException && !Util.isNetworkConnected(context)) {
+ return context.getResources().getString(R.string.background_task_no_network);
+ }
+
+ if (error instanceof FileNotFoundException) {
+ return context.getResources().getString(R.string.background_task_not_found);
+ }
+
+ if (error instanceof IOException) {
+ return context.getResources().getString(R.string.background_task_network_error);
+ }
+
+ if (error instanceof XmlPullParserException) {
+ return context.getResources().getString(R.string.background_task_parse_error);
+ }
+
+ String message = error.getMessage();
+ if (message != null) {
+ return message;
+ }
+ return error.getClass().getSimpleName();
+ }
+
+ public void cancel() {
+ if(cancelled.compareAndSet(false, true)) {
+ if(isRunning()) {
+ if(cancelListener != null) {
+ cancelListener.onCancel();
+ } else {
+ task.cancel();
+ }
+ }
+
+ task = null;
+ }
+ }
+ public boolean isCancelled() {
+ return cancelled.get();
+ }
+ public void setOnCancelListener(OnCancelListener listener) {
+ cancelListener = listener;
+ }
+
+ public boolean isRunning() {
+ if(task == null) {
+ return false;
+ } else {
+ return task.isRunning();
+ }
+ }
+
+ @Override
+ public abstract void updateProgress(final String message);
+
+ @Override
+ public void updateProgress(int messageId) {
+ updateProgress(context.getResources().getString(messageId));
+ }
+
+ public void setOnCompletionListener(Runnable onCompletionListener) {
+ this.onCompletionListener = onCompletionListener;
+ }
+
+ protected class Task {
+ private Thread thread;
+ private AtomicBoolean taskStart = new AtomicBoolean(false);
+
+ private void execute() throws Exception {
+ // Don't run if cancelled already
+ if(isCancelled()) {
+ return;
+ }
+
+ try {
+ thread = Thread.currentThread();
+ taskStart.set(true);
+
+ final T result = doInBackground();
+ if(isCancelled()) {
+ taskStart.set(false);
+ return;
+ }
+
+ if(handler != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(!isCancelled()) {
+ onDone(result);
+ }
+
+ taskStart.set(false);
+ }
+ });
+ } else {
+ taskStart.set(false);
+ }
+ } catch(InterruptedException interrupt) {
+ if(taskStart.get()) {
+ // Don't exit root thread if task cancelled
+ throw interrupt;
+ }
+ } catch(final Throwable t) {
+ if(isCancelled()) {
+ taskStart.set(false);
+ return;
+ }
+
+ if(handler != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(!isCancelled()) {
+ try {
+ onError(t);
+ } catch(Exception e) {
+ // Don't care
+ }
+ }
+
+ taskStart.set(false);
+ }
+ });
+ } else {
+ taskStart.set(false);
+ }
+ } finally {
+ thread = null;
+ }
+ }
+
+ public void cancel() {
+ if(taskStart.compareAndSet(true, false)) {
+ if (thread != null) {
+ thread.interrupt();
+ }
+ }
+ }
+ public boolean isCancelled() {
+ if(Thread.interrupted()) {
+ return true;
+ } else if(BackgroundTask.this.isCancelled()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ public void onDone(T result) {
+ done(result);
+
+ if(onCompletionListener != null) {
+ onCompletionListener.run();
+ }
+ }
+ public void onError(Throwable t) {
+ error(t);
+ }
+
+ public boolean isRunning() {
+ return taskStart.get();
+ }
+ }
+
+ private class TaskRunnable implements Runnable {
+ private boolean running = true;
+
+ public TaskRunnable() {
+
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ while(running) {
+ try {
+ Task task = queue.take();
+ task.execute();
+ } catch(InterruptedException stop) {
+ Log.e(TAG, "Thread died");
+ running = false;
+ threads.remove(Thread.currentThread());
+ } catch(Throwable t) {
+ Log.e(TAG, "Unexpected crash in BackgroundTask thread", t);
+ }
+ }
+ }
+ }
+
+ public static interface OnCancelListener {
+ void onCancel();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java b/app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java
new file mode 100644
index 00000000..ac8fa72a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/CacheCleaner.java
@@ -0,0 +1,292 @@
+package github.daneren2005.dsub.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.content.Context;
+import android.util.Log;
+import android.os.StatFs;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MediaStoreService;
+
+import java.util.*;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheCleaner {
+
+ private static final String TAG = CacheCleaner.class.getSimpleName();
+ private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
+ private static final long MAX_COVER_ART_SPACE = 100 * 1024L * 1024L;
+
+ private final Context context;
+ private final DownloadService downloadService;
+ private final MediaStoreService mediaStore;
+
+ public CacheCleaner(Context context, DownloadService downloadService) {
+ this.context = context;
+ this.downloadService = downloadService;
+ this.mediaStore = new MediaStoreService(context);
+ }
+
+ public void clean() {
+ new BackgroundCleanup(context).execute();
+ }
+ public void cleanSpace() {
+ new BackgroundSpaceCleanup(context).execute();
+ }
+ public void cleanPlaylists(List<Playlist> playlists) {
+ new BackgroundPlaylistsCleanup(context, playlists).execute();
+ }
+
+ private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
+ for (File dir : dirs) {
+ if (undeletable.contains(dir)) {
+ continue;
+ }
+
+ FileUtil.deleteEmptyDir(dir);
+ }
+ }
+
+ private long getMinimumDelete(List<File> files, List<File> pinned) {
+ if(files.size() == 0) {
+ return 0L;
+ }
+
+ long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
+
+ long bytesUsedBySubsonic = 0L;
+ for (File file : files) {
+ bytesUsedBySubsonic += file.length();
+ }
+ for (File file : pinned) {
+ bytesUsedBySubsonic += file.length();
+ }
+
+ // Ensure that file system is not more than 95% full.
+ StatFs stat = new StatFs(files.get(0).getPath());
+ long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
+ long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
+ long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE;
+
+ long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
+ long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
+ long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
+
+ Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
+ Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes));
+ Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
+ Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
+
+ return bytesToDelete;
+ }
+
+ private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) {
+ if (files.isEmpty()) {
+ return;
+ }
+
+ long bytesDeleted = 0L;
+ for (File file : files) {
+ if(!deletePartials && bytesDeleted > bytesToDelete) break;
+
+ if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial.")))) {
+ if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) {
+ long size = file.length();
+ if (Util.delete(file)) {
+ bytesDeleted += size;
+ mediaStore.deleteFromMediaStore(file);
+ }
+ }
+ }
+ }
+
+ Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
+ }
+
+ private void findCandidatesForDeletion(File file, List<File> files, List<File> pinned, List<File> dirs) {
+ if (file.isFile()) {
+ String name = file.getName();
+ boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
+ if (isCacheFile) {
+ files.add(file);
+ } else {
+ pinned.add(file);
+ }
+ } else {
+ // Depth-first
+ for (File child : FileUtil.listFiles(file)) {
+ findCandidatesForDeletion(child, files, pinned, dirs);
+ }
+ dirs.add(file);
+ }
+ }
+
+ private void sortByAscendingModificationTime(List<File> files) {
+ Collections.sort(files, new Comparator<File>() {
+ @Override
+ public int compare(File a, File b) {
+ if (a.lastModified() < b.lastModified()) {
+ return -1;
+ }
+ if (a.lastModified() > b.lastModified()) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ }
+
+ private Set<File> findUndeletableFiles() {
+ Set<File> undeletable = new HashSet<File>(5);
+
+ for (DownloadFile downloadFile : downloadService.getDownloads()) {
+ undeletable.add(downloadFile.getPartialFile());
+ undeletable.add(downloadFile.getCompleteFile());
+ }
+
+ undeletable.add(FileUtil.getMusicDirectory(context));
+ return undeletable;
+ }
+
+ private void cleanupCoverArt(Context context) {
+ File dir = FileUtil.getAlbumArtDirectory(context);
+
+ List<File> files = new ArrayList<File>();
+ long bytesUsed = 0L;
+ for(File file: dir.listFiles()) {
+ if(file.isFile()) {
+ files.add(file);
+ bytesUsed += file.length();
+ }
+ }
+
+ // Don't waste time sorting if under limit already
+ if(bytesUsed < MAX_COVER_ART_SPACE) {
+ return;
+ }
+
+ sortByAscendingModificationTime(files);
+ long bytesDeleted = 0L;
+ for(File file: files) {
+ // End as soon as the space used is below the threshold
+ if(bytesUsed < MAX_COVER_ART_SPACE) {
+ break;
+ }
+
+ long bytes = file.length();
+ if(file.delete()) {
+ bytesUsed -= bytes;
+ bytesDeleted += bytes;
+ }
+ }
+
+ Log.i(TAG, "Deleted " + Util.formatBytes(bytesDeleted) + " worth of cover art");
+ }
+
+ private class BackgroundCleanup extends SilentBackgroundTask<Void> {
+ public BackgroundCleanup(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected Void doInBackground() {
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return null;
+ }
+
+ try {
+ List<File> files = new ArrayList<File>();
+ List<File> pinned = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
+ sortByAscendingModificationTime(files);
+
+ Set<File> undeletable = findUndeletableFiles();
+
+ deleteFiles(files, undeletable, getMinimumDelete(files, pinned), true);
+ deleteEmptyDirs(dirs, undeletable);
+
+ // Make sure cover art directory does not grow too large
+ cleanupCoverArt(context);
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class BackgroundSpaceCleanup extends SilentBackgroundTask<Void> {
+ public BackgroundSpaceCleanup(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected Void doInBackground() {
+ if (downloadService == null) {
+ Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
+ return null;
+ }
+
+ try {
+ List<File> files = new ArrayList<File>();
+ List<File> pinned = new ArrayList<File>();
+ List<File> dirs = new ArrayList<File>();
+ findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
+
+ long bytesToDelete = getMinimumDelete(files, pinned);
+ if(bytesToDelete > 0L) {
+ sortByAscendingModificationTime(files);
+ Set<File> undeletable = findUndeletableFiles();
+ deleteFiles(files, undeletable, bytesToDelete, false);
+ }
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class BackgroundPlaylistsCleanup extends SilentBackgroundTask<Void> {
+ private final List<Playlist> playlists;
+
+ public BackgroundPlaylistsCleanup(Context context, List<Playlist> playlists) {
+ super(context);
+ this.playlists = playlists;
+ }
+
+ @Override
+ protected Void doInBackground() {
+ try {
+ String server = Util.getServerName(context);
+ SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server));
+ for (Playlist playlist : playlists) {
+ playlistFiles.remove(FileUtil.getPlaylistFile(context, server, playlist.getName()));
+ }
+
+ for(File playlist : playlistFiles) {
+ playlist.delete();
+ }
+ } catch (RuntimeException x) {
+ Log.e(TAG, "Error in playlist cache cleaning.", x);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Constants.java b/app/src/main/java/github/daneren2005/dsub/util/Constants.java
new file mode 100644
index 00000000..31c5bef2
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Constants.java
@@ -0,0 +1,206 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public final class Constants {
+
+ // Character encoding used throughout.
+ public static final String UTF_8 = "UTF-8";
+
+ // REST protocol version and client ID.
+ // Note: Keep it as low as possible to maintain compatibility with older servers.
+ public static final String REST_PROTOCOL_VERSION = "1.2.0";
+ public static final String REST_CLIENT_ID = "DSub";
+ public static final String CHROMECAST_CLIENT_ID = "DSubCC";
+ public static final String LAST_VERSION = "subsonic.version";
+
+ // Names for intent extras.
+ public static final String INTENT_EXTRA_NAME_ID = "subsonic.id";
+ public static final String INTENT_EXTRA_NAME_NAME = "subsonic.name";
+ public static final String INTENT_EXTRA_NAME_DIRECTORY = "subsonic.directory";
+ public static final String INTENT_EXTRA_NAME_CHILD_ID = "subsonic.child.id";
+ 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_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_PLAYLIST_OWNER = "subsonic.playlist.isOwner";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_TYPE = "subsonic.albumlisttype";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_EXTRA = "subsonic.albumlistextra";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_SIZE = "subsonic.albumlistsize";
+ public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
+ public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
+ public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch";
+ public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit" ;
+ public static final String INTENT_EXTRA_NAME_DOWNLOAD = "subsonic.download";
+ public static final String INTENT_EXTRA_NAME_DOWNLOAD_VIEW = "subsonic.download_view";
+ public static final String INTENT_EXTRA_VIEW_ALBUM = "subsonic.view_album";
+ public static final String INTENT_EXTRA_NAME_PODCAST_ID = "subsonic.podcast.id";
+ public static final String INTENT_EXTRA_NAME_PODCAST_NAME = "subsonic.podcast.name";
+ public static final String INTENT_EXTRA_NAME_PODCAST_DESCRIPTION = "subsonic.podcast.description";
+ public static final String INTENT_EXTRA_NAME_SHARE = "subsonic.share";
+ public static final String INTENT_EXTRA_FRAGMENT_TYPE = "fragmentType";
+ public static final String INTENT_EXTRA_REFRESH_LISTINGS = "refreshListings";
+ public static final String INTENT_EXTRA_SEARCH_SONG = "searchSong";
+ public static final String INTENT_EXTRA_TOP_TRACKS = "topTracks";
+ public static final String INTENT_EXTRA_SHOW_ALL = "showAll";
+
+ // Preferences keys.
+ public static final String PREFERENCES_KEY_SERVER_KEY = "server";
+ public static final String PREFERENCES_KEY_SERVER_COUNT = "serverCount";
+ public static final String PREFERENCES_KEY_SERVER_ADD = "serverAdd";
+ public static final String PREFERENCES_KEY_SERVER_REMOVE = "serverRemove";
+ public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
+ public static final String PREFERENCES_KEY_SERVER_NAME = "serverName";
+ public static final String PREFERENCES_KEY_SERVER_URL = "serverUrl";
+ public static final String PREFERENCES_KEY_SERVER_INTERNAL_URL = "serverInternalUrl";
+ public static final String PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID = "serverLocalNetworkSSID";
+ public static final String PREFERENCES_KEY_TEST_CONNECTION = "serverTestConnection";
+ public static final String PREFERENCES_KEY_OPEN_BROWSER = "openBrowser";
+ public static final String PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId";
+ public static final String PREFERENCES_KEY_USERNAME = "username";
+ public static final String PREFERENCES_KEY_PASSWORD = "password";
+ public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
+ public static final String PREFERENCES_KEY_THEME = "theme";
+ public static final String PREFERENCES_KEY_FULL_SCREEN = "fullScreen";
+ public static final String PREFERENCES_KEY_DISPLAY_TRACK = "displayTrack";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile";
+ public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI = "maxVideoBitrateWifi";
+ public static final String PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE = "maxVideoBitrateMobile";
+ public static final String PREFERENCES_KEY_NETWORK_TIMEOUT = "networkTimeout";
+ public static final String PREFERENCES_KEY_CACHE_SIZE = "cacheSize";
+ public static final String PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT_WIFI = "preloadCountWifi";
+ public static final String PREFERENCES_KEY_PRELOAD_COUNT_MOBILE = "preloadCountMobile";
+ public static final String PREFERENCES_KEY_HIDE_MEDIA = "hideMedia";
+ public static final String PREFERENCES_KEY_MEDIA_BUTTONS = "mediaButtons";
+ public static final String PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD = "screenLitOnDownload";
+ public static final String PREFERENCES_KEY_SCROBBLE = "scrobble";
+ public static final String PREFERENCES_KEY_REPEAT_MODE = "repeatMode";
+ public static final String PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD = "wifiRequiredForDownload";
+ public static final String PREFERENCES_KEY_RANDOM_SIZE = "randomSize";
+ public static final String PREFERENCES_KEY_SLEEP_TIMER_DURATION = "sleepTimerDuration";
+ public static final String PREFERENCES_KEY_OFFLINE = "offline";
+ public static final String PREFERENCES_KEY_TEMP_LOSS = "tempLoss";
+ public static final String PREFERENCES_KEY_SHUFFLE_START_YEAR = "startYear";
+ public static final String PREFERENCES_KEY_SHUFFLE_END_YEAR = "endYear";
+ public static final String PREFERENCES_KEY_SHUFFLE_GENRE = "genre";
+ public static final String PREFERENCES_KEY_KEEP_SCREEN_ON = "keepScreenOn";
+ public static final String PREFERENCES_EQUALIZER_ON = "equalizerOn";
+ public static final String PREFERENCES_EQUALIZER_SETTINGS = "equalizerSettings";
+ public static final String PREFERENCES_KEY_PERSISTENT_NOTIFICATION = "persistentNotification";
+ public static final String PREFERENCES_KEY_GAPLESS_PLAYBACK = "gaplessPlayback";
+ public static final String PREFERENCES_KEY_REMOVE_PLAYED = "removePlayed";
+ public static final String PREFERENCES_KEY_SHUFFLE_MODE = "shuffleMode2";
+ public static final String PREFERENCES_KEY_SHUFFLE_MODE_EXTRA = "shuffleModeExtra";
+ public static final String PREFERENCES_KEY_CHAT_REFRESH = "chatRefreshRate";
+ public static final String PREFERENCES_KEY_CHAT_ENABLED = "chatEnabled";
+ public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer";
+ public static final String PREFERENCES_KEY_CONTROL_MODE = "remoteControlMode";
+ public static final String PREFERENCES_KEY_CONTROL_ID = "remoteControlId";
+ public static final String PREFERENCES_KEY_SYNC_ENABLED = "syncEnabled";
+ public static final String PREFERENCES_KEY_SYNC_INTERVAL = "syncInterval";
+ public static final String PREFERENCES_KEY_SYNC_WIFI = "syncWifi";
+ public static final String PREFERENCES_KEY_SYNC_NOTIFICATION = "syncNotification";
+ public static final String PREFERENCES_KEY_SYNC_STARRED = "syncStarred";
+ public static final String PREFERENCES_KEY_SYNC_MOST_RECENT = "syncMostRecent";
+ public static final String PREFERENCES_KEY_PAUSE_DISCONNECT = "pauseOnDisconnect";
+ public static final String PREFERENCES_KEY_HIDE_WIDGET = "hideWidget";
+ public static final String PREFERENCES_KEY_PODCASTS_ENABLED = "podcastsEnabled";
+ public static final String PREFERENCES_KEY_BOOKMARKS_ENABLED = "bookmarksEnabled";
+ public static final String PREFERENCES_KEY_CUSTOM_SORT_ENABLED = "customSortEnabled";
+ public static final String PREFERENCES_KEY_MENU_PLAY_NEXT = "showPlayNext";
+ public static final String PREFERENCES_KEY_MENU_PLAY_LAST = "showPlayLast";
+ public static final String PREFERENCES_KEY_MENU_STAR = "showStar";
+ public static final String PREFERENCES_KEY_MENU_SHARED = "showShared";
+ public static final String PREFERENCES_KEY_SHARED_ENABLED = "sharedEnabled";
+ public static final String PREFERENCES_KEY_BROWSE_TAGS = "browseTags";
+ public static final String PREFERENCES_KEY_OPEN_TO_TAB = "openToTab";
+ public static final String PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE = "overrideSystemLanguage";
+ public static final String PREFERENCES_KEY_PLAY_NOW_AFTER = "playNowAfter";
+ public static final String PREFERENCES_KEY_LARGE_ALBUM_ART = "largeAlbumArt";
+ public static final String PREFERENCES_KEY_ADMIN_ENABLED = "adminEnabled";
+ public static final String PREFERENCES_KEY_PLAYLIST_NAME = "suggestedPlaylistName";
+ public static final String PREFERENCES_KEY_PLAYLIST_ID = "suggestedPlaylistId";
+ public static final String PREFERENCES_KEY_SERVER_SYNC = "serverSync";
+ public static final String PREFERENCES_KEY_RECENT_COUNT = "mostRecentCount";
+ public static final String PREFERENCES_KEY_MENU_RATING = "showRating";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN = "replayGain";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_BUMP = "replayGainBump2";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_UNTAGGED = "replayGainUntagged2";
+ public static final String PREFERENCES_KEY_REPLAY_GAIN_TYPE= "replayGainType";
+ public static final String PREFERENCES_KEY_ALBUMS_PER_FOLDER = "albumsPerFolder";
+ public static final String PREFERENCES_KEY_CAST_PROXY = "castProxy";
+ public static final String PREFERENCES_KEY_DISABLE_EXIT_PROMPT = "disableExitPrompt";
+ public static final String PREFERENCES_KEY_RENAME_DUPLICATES = "renameDuplicates";
+ public static final String PREFERENCES_KEY_FIRST_LEVEL_ARTIST = "firstLevelArtist";
+ public static final String PREFERENCES_KEY_START_ON_HEADPHONES = "startOnHeadphones";
+
+ public static final String OFFLINE_SCROBBLE_COUNT = "scrobbleCount";
+ public static final String OFFLINE_SCROBBLE_ID = "scrobbleID";
+ public static final String OFFLINE_SCROBBLE_SEARCH = "scrobbleTitle";
+ public static final String OFFLINE_SCROBBLE_TIME = "scrobbleTime";
+ public static final String OFFLINE_STAR_COUNT = "starCount";
+ public static final String OFFLINE_STAR_ID = "starID";
+ public static final String OFFLINE_STAR_SEARCH = "starTitle";
+ public static final String OFFLINE_STAR_SETTING = "starSetting";
+
+ public static final String CACHE_KEY_IGNORE = "ignoreArticles";
+
+ public static final String MAIN_BACK_STACK = "backStackIds";
+ public static final String MAIN_BACK_STACK_SIZE = "backStackIdsSize";
+ public static final String FRAGMENT_LIST = "fragmentList";
+ public static final String FRAGMENT_LIST2 = "fragmentList2";
+ public static final String FRAGMENT_EXTRA = "fragmentExtra";
+ public static final String FRAGMENT_DOWNLOAD_FLIPPER = "fragmentDownloadFlipper";
+ public static final String FRAGMENT_NAME = "fragmentName";
+ public static final String FRAGMENT_POSITION = "fragmentPosition";
+
+ // Name of the preferences file.
+ public static final String PREFERENCES_FILE_NAME = "github.daneren2005.dsub_preferences";
+ public static final String OFFLINE_SYNC_NAME = "github.daneren2005.dsub.offline";
+ public static final String OFFLINE_SYNC_DEFAULT = "syncDefaults";
+
+ // Account prefs
+ public static final String SYNC_ACCOUNT_NAME = "Subsonic Account";
+ public static final String SYNC_ACCOUNT_TYPE = "subsonic.org";
+ public static final String SYNC_ACCOUNT_PLAYLIST_AUTHORITY = "github.daneren2005.dsub.playlists.provider";
+ public static final String SYNC_ACCOUNT_PODCAST_AUTHORITY = "github.daneren2005.dsub.podcasts.provider";
+ public static final String SYNC_ACCOUNT_STARRED_AUTHORITY = "github.daneren2005.dsub.starred.provider";
+ public static final String SYNC_ACCOUNT_MOST_RECENT_AUTHORITY = "github.daneren2005.dsub.mostrecent.provider";
+
+ public static final String TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE";
+
+ // Number of free trial days for non-licensed servers.
+ public static final int FREE_TRIAL_DAYS = 30;
+
+ // URL for project donations.
+ public static final String DONATION_URL = "http://subsonic.org/pages/android-donation.jsp";
+
+ public static final String ALBUM_ART_FILE = "albumart.jpg";
+
+ private Constants() {
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java b/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java
new file mode 100644
index 00000000..990eae06
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/FileUtil.java
@@ -0,0 +1,860 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.os.Environment;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+import github.daneren2005.dsub.domain.Artist;
+import github.daneren2005.dsub.domain.Genre;
+import github.daneren2005.dsub.domain.Indexes;
+import github.daneren2005.dsub.domain.Playlist;
+import github.daneren2005.dsub.domain.PodcastChannel;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.MusicFolder;
+import github.daneren2005.dsub.domain.PodcastEpisode;
+import github.daneren2005.dsub.service.MediaStoreService;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * @author Sindre Mehus
+ */
+public class FileUtil {
+
+ private static final String TAG = FileUtil.class.getSimpleName();
+ private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
+ private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">", "|"};
+ private static final List<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
+ private static final List<String> VIDEO_FILE_EXTENSIONS = Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv");
+ private static final List<String> PLAYLIST_FILE_EXTENSIONS = Arrays.asList("m3u");
+ private static File DEFAULT_MUSIC_DIR;
+ private static final Kryo kryo = new Kryo();
+ private static HashMap<String, MusicDirectory.Entry> entryLookup;
+
+ static {
+ kryo.register(MusicDirectory.Entry.class);
+ kryo.register(Indexes.class);
+ kryo.register(Artist.class);
+ kryo.register(MusicFolder.class);
+ kryo.register(PodcastChannel.class);
+ kryo.register(Playlist.class);
+ kryo.register(Genre.class);
+ }
+
+ public static File getAnySong(Context context) {
+ File dir = getMusicDirectory(context);
+ return getAnySong(context, dir);
+ }
+ private static File getAnySong(Context context, File dir) {
+ for(File file: dir.listFiles()) {
+ if(file.isDirectory()) {
+ return getAnySong(context, file);
+ }
+
+ String extension = getExtension(file.getName());
+ if(MUSIC_FILE_EXTENSIONS.contains(extension)) {
+ return file;
+ }
+ }
+
+ return null;
+ }
+
+ public static File getEntryFile(Context context, MusicDirectory.Entry entry) {
+ if(entry.isDirectory()) {
+ return getAlbumDirectory(context, entry);
+ } else {
+ return getSongFile(context, entry);
+ }
+ }
+
+ 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.isVideo()) {
+ String videoPlayerType = Util.getVideoPlayerType(context);
+ if("hls".equals(videoPlayerType)) {
+ // HLS should be able to transcode to mp4 automatically
+ fileName.append("mp4");
+ } else if("raw".equals(videoPlayerType)) {
+ // Download the original video without any transcoding
+ fileName.append(song.getSuffix());
+ }
+ } else {
+ if (song.getTranscodedSuffix() != null) {
+ fileName.append(song.getTranscodedSuffix());
+ } else {
+ fileName.append(song.getSuffix());
+ }
+ }
+
+ return new File(dir, fileName.toString());
+ }
+
+ public static File getPlaylistFile(Context context, String server, String name) {
+ File playlistDir = getPlaylistDirectory(context, server);
+ return new File(playlistDir, fileSystemSafe(name) + ".m3u");
+ }
+ public static void writePlaylistFile(Context context, File file, MusicDirectory playlist) throws IOException {
+ FileWriter fw = new FileWriter(file);
+ BufferedWriter bw = new BufferedWriter(fw);
+ try {
+ fw.write("#EXTM3U\n");
+ for (MusicDirectory.Entry e : playlist.getChildren()) {
+ String filePath = FileUtil.getSongFile(context, e).getAbsolutePath();
+ if(! new File(filePath).exists()){
+ String ext = FileUtil.getExtension(filePath);
+ String base = FileUtil.getBaseName(filePath);
+ filePath = base + ".complete." + ext;
+ }
+ fw.write(filePath + "\n");
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to save playlist: " + playlist.getName());
+ } finally {
+ bw.close();
+ fw.close();
+ }
+ }
+ public static File getPlaylistDirectory(Context context) {
+ File playlistDir = new File(getSubsonicDirectory(context), "playlists");
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+ public static File getPlaylistDirectory(Context context, String server) {
+ File playlistDir = new File(getPlaylistDirectory(context), server);
+ ensureDirectoryExistsAndIsReadWritable(playlistDir);
+ return playlistDir;
+ }
+
+ public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
+ File albumDir = getAlbumDirectory(context, entry);
+ File artFile;
+ File albumFile = getAlbumArtFile(albumDir);
+ File hexFile = getHexAlbumArtFile(context, albumDir);
+ if(albumDir.exists()) {
+ if(hexFile.exists()) {
+ hexFile.renameTo(albumFile);
+ }
+ artFile = albumFile;
+ } else {
+ artFile = hexFile;
+ }
+ return artFile;
+ }
+
+ public static File getAlbumArtFile(File albumDir) {
+ return new File(albumDir, Constants.ALBUM_ART_FILE);
+ }
+ public static File getHexAlbumArtFile(Context context, File albumDir) {
+ return new File(getAlbumArtDirectory(context), Util.md5Hex(albumDir.getPath()) + ".jpeg");
+ }
+
+ public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
+ File albumArtFile = getAlbumArtFile(context, entry);
+ if (albumArtFile.exists()) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(albumArtFile.getPath(), opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+
+ Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath(), opt);
+ return bitmap == null ? null : getScaledBitmap(bitmap, size);
+ }
+ return null;
+ }
+
+ public static File getAvatarDirectory(Context context) {
+ File avatarDir = new File(getSubsonicDirectory(context), "avatars");
+ ensureDirectoryExistsAndIsReadWritable(avatarDir);
+ ensureDirectoryExistsAndIsReadWritable(new File(avatarDir, ".nomedia"));
+ return avatarDir;
+ }
+
+ public static File getAvatarFile(Context context, String username) {
+ return new File(getAvatarDirectory(context), Util.md5Hex(username) + ".jpeg");
+ }
+
+ public static Bitmap getAvatarBitmap(Context context, String username, int size) {
+ File avatarFile = getAvatarFile(context, username);
+ if (avatarFile.exists()) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+
+ Bitmap bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ return bitmap == null ? null : getScaledBitmap(bitmap, size, false);
+ }
+ return null;
+ }
+
+ public static File getMiscDirectory(Context context) {
+ File dir = new File(getSubsonicDirectory(context), "misc");
+ ensureDirectoryExistsAndIsReadWritable(dir);
+ ensureDirectoryExistsAndIsReadWritable(new File(dir, ".nomedia"));
+ return dir;
+ }
+
+ public static File getMiscFile(Context context, String url) {
+ return new File(getMiscDirectory(context), Util.md5Hex(url) + ".jpeg");
+ }
+
+ public static Bitmap getMiscBitmap(Context context, String url, int size) {
+ File avatarFile = getMiscFile(context, url);
+ if (avatarFile.exists()) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+
+ Bitmap bitmap = BitmapFactory.decodeFile(avatarFile.getPath(), opt);
+ return bitmap == null ? null : getScaledBitmap(bitmap, size, false);
+ }
+ return null;
+ }
+
+ public static Bitmap getSampledBitmap(byte[] bytes, int size) {
+ return getSampledBitmap(bytes, size, true);
+ }
+ public static Bitmap getSampledBitmap(byte[] bytes, int size, boolean allowUnscaled) {
+ final BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
+ opt.inPurgeable = true;
+ opt.inSampleSize = Util.calculateInSampleSize(opt, size, Util.getScaledHeight(opt.outHeight, opt.outWidth, size));
+ opt.inJustDecodeBounds = false;
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
+ if(bitmap == null) {
+ return null;
+ } else {
+ return getScaledBitmap(bitmap, size, allowUnscaled);
+ }
+ }
+ public static Bitmap getScaledBitmap(Bitmap bitmap, int size) {
+ return getScaledBitmap(bitmap, size, true);
+ }
+ public static Bitmap getScaledBitmap(Bitmap bitmap, int size, boolean allowUnscaled) {
+ // Don't waste time scaling if the difference is minor
+ // Large album arts still need to be scaled since displayed as is on now playing!
+ if(allowUnscaled && size < 400 && bitmap.getWidth() < (size * 1.1)) {
+ return bitmap;
+ } else {
+ return Bitmap.createScaledBitmap(bitmap, size, Util.getScaledHeight(bitmap, size), true);
+ }
+ }
+
+ public static File getAlbumArtDirectory(Context context) {
+ File albumArtDir = new File(getSubsonicDirectory(context), "artwork");
+ ensureDirectoryExistsAndIsReadWritable(albumArtDir);
+ ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia"));
+ return albumArtDir;
+ }
+
+ public static File getArtistDirectory(Context context, Artist artist) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getName()));
+ return dir;
+ }
+ public static File getArtistDirectory(Context context, MusicDirectory.Entry artist) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(artist.getTitle()));
+ return dir;
+ }
+
+ public static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
+ File dir = null;
+ if (entry.getPath() != null) {
+ File f = new File(fileSystemSafeDir(entry.getPath()));
+ dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
+ } else {
+ MusicDirectory.Entry firstSong;
+ if(!Util.isOffline(context)) {
+ firstSong = lookupChild(context, entry, false);
+ if(firstSong != null) {
+ File songFile = FileUtil.getSongFile(context, firstSong);
+ dir = songFile.getParentFile();
+ }
+ }
+
+ if(dir == null) {
+ String artist = fileSystemSafe(entry.getArtist());
+ String album = fileSystemSafe(entry.getAlbum());
+ if("unnamed".equals(album)) {
+ album = fileSystemSafe(entry.getTitle());
+ }
+ dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
+ }
+ }
+ return dir;
+ }
+
+ public static MusicDirectory.Entry lookupChild(Context context, MusicDirectory.Entry entry, boolean allowDir) {
+ // Initialize lookupMap if first time called
+ String lookupName = Util.getCacheName(context, "entryLookup");
+ if(entryLookup == null) {
+ entryLookup = deserialize(context, lookupName, HashMap.class);
+
+ // Create it if
+ if(entryLookup == null) {
+ entryLookup = new HashMap<String, MusicDirectory.Entry>();
+ }
+ }
+
+ // Check if this lookup has already been done before
+ MusicDirectory.Entry child = entryLookup.get(entry.getId());
+ if(child != null) {
+ return child;
+ }
+
+ // Do a special lookup since 4.7+ doesn't match artist/album to entry.getPath
+ String s = Util.getRestUrl(context, null, false) + entry.getId();
+ String cacheName = (Util.isTagBrowsing(context) ? "album-" : "directory-") + s.hashCode() + ".ser";
+ MusicDirectory entryDir = FileUtil.deserialize(context, cacheName, MusicDirectory.class);
+
+ if(entryDir != null) {
+ List<MusicDirectory.Entry> songs = entryDir.getChildren(allowDir, true);
+ if(songs.size() > 0) {
+ child = songs.get(0);
+ entryLookup.put(entry.getId(), child);
+ serialize(context, entryLookup, lookupName);
+ return child;
+ }
+ }
+
+ return null;
+ }
+
+ public static String getPodcastPath(Context context, PodcastEpisode episode) {
+ return fileSystemSafe(episode.getArtist()) + "/" + fileSystemSafe(episode.getTitle());
+ }
+ public static File getPodcastFile(Context context, String server) {
+ File dir = getPodcastDirectory(context);
+ return new File(dir.getPath() + "/" + fileSystemSafe(server));
+ }
+ public static File getPodcastDirectory(Context context) {
+ File dir = new File(context.getCacheDir(), "podcasts");
+ ensureDirectoryExistsAndIsReadWritable(dir);
+ return dir;
+ }
+ public static File getPodcastDirectory(Context context, PodcastChannel channel) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel.getName()));
+ return dir;
+ }
+ public static File getPodcastDirectory(Context context, String channel) {
+ File dir = new File(getMusicDirectory(context).getPath() + "/" + fileSystemSafe(channel));
+ return dir;
+ }
+
+ public static void createDirectoryForParent(File file) {
+ File dir = file.getParentFile();
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ Log.e(TAG, "Failed to create directory " + dir);
+ }
+ }
+ }
+
+ private static File createDirectory(Context context, String name) {
+ File dir = new File(getSubsonicDirectory(context), name);
+ if (!dir.exists() && !dir.mkdirs()) {
+ Log.e(TAG, "Failed to create " + name);
+ }
+ return dir;
+ }
+
+ public static File getSubsonicDirectory(Context context) {
+ return context.getExternalFilesDir(null);
+ }
+
+ public static File getDefaultMusicDirectory(Context context) {
+ if(DEFAULT_MUSIC_DIR == null) {
+ File[] dirs;
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ dirs = context.getExternalMediaDirs();
+ } else {
+ dirs = ContextCompat.getExternalFilesDirs(context, null);
+ }
+
+ DEFAULT_MUSIC_DIR = new File(getBestDir(dirs), "music");
+ Log.d(TAG, "Default: " + DEFAULT_MUSIC_DIR.getAbsolutePath());
+
+ if (!DEFAULT_MUSIC_DIR.exists() && !DEFAULT_MUSIC_DIR.mkdirs()) {
+ Log.e(TAG, "Failed to create default dir " + DEFAULT_MUSIC_DIR);
+
+ // Some devices seem to have screwed up the new media directory API. Go figure. Try again with standard locations
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ dirs = ContextCompat.getExternalFilesDirs(context, null);
+
+ DEFAULT_MUSIC_DIR = new File(getBestDir(dirs), "music");
+ if (!DEFAULT_MUSIC_DIR.exists() && !DEFAULT_MUSIC_DIR.mkdirs()) {
+ Log.e(TAG, "Failed to create default dir " + DEFAULT_MUSIC_DIR);
+ } else {
+ Log.w(TAG, "Stupid OEM's messed up media dir API added in 5.0");
+ }
+ }
+ }
+ }
+
+ return DEFAULT_MUSIC_DIR;
+ }
+ private static File getBestDir(File[] dirs) {
+ // Past 5.0 we can query directly for SD Card
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ for(int i = 0; i < dirs.length; i++) {
+ if(dirs[i] != null && Environment.isExternalStorageRemovable(dirs[i])) {
+ return dirs[i];
+ }
+ }
+ }
+
+ // Before 5.0, we have to guess. Most of the time the SD card is last
+ for(int i = dirs.length - 1; i >= 0; i--) {
+ if(dirs[i] != null) {
+ return dirs[i];
+ }
+ }
+
+ // Should be impossible to be reached
+ return dirs[0];
+ }
+
+ public static File getMusicDirectory(Context context) {
+ String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, getDefaultMusicDirectory(context).getPath());
+ File dir = new File(path);
+ return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory(context);
+ }
+ public static boolean deleteMusicDirectory(Context context) {
+ File musicDirectory = FileUtil.getMusicDirectory(context);
+ MediaStoreService mediaStore = new MediaStoreService(context);
+ return recursiveDelete(musicDirectory, mediaStore);
+ }
+ public static void deleteSerializedCache(Context context) {
+ for(File file: context.getCacheDir().listFiles()) {
+ if(file.getName().indexOf(".ser") != -1) {
+ file.delete();
+ }
+ }
+ }
+ public static boolean deleteArtworkCache(Context context) {
+ File artDirectory = FileUtil.getAlbumArtDirectory(context);
+ return recursiveDelete(artDirectory);
+ }
+ public static boolean deleteAvatarCache(Context context) {
+ File artDirectory = FileUtil.getAvatarDirectory(context);
+ return recursiveDelete(artDirectory);
+ }
+
+ public static boolean recursiveDelete(File dir) {
+ return recursiveDelete(dir, null);
+ }
+ public static boolean recursiveDelete(File dir, MediaStoreService mediaStore) {
+ if (dir != null && dir.exists()) {
+ File[] list = dir.listFiles();
+ if(list != null) {
+ for(File file: list) {
+ if(file.isDirectory()) {
+ if(!recursiveDelete(file, mediaStore)) {
+ return false;
+ }
+ } else if(file.exists()) {
+ if(!file.delete()) {
+ return false;
+ } else if(mediaStore != null) {
+ mediaStore.deleteFromMediaStore(file);
+ }
+ }
+ }
+ }
+ return dir.delete();
+ }
+ return false;
+ }
+
+ public static void deleteEmptyDir(File dir) {
+ try {
+ File[] children = dir.listFiles();
+ if(children == null) {
+ return;
+ }
+
+ // No songs left in the folder
+ if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) {
+ Util.delete(children[0]);
+ children = dir.listFiles();
+ }
+
+ // Delete empty directory
+ if (children.length == 0) {
+ Util.delete(dir);
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Error while trying to delete empty dir", e);
+ }
+ }
+
+ public static void unpinSong(Context context, File saveFile) {
+ // Don't try to unpin a song which isn't actually pinned
+ if(saveFile.getName().contains(".complete")) {
+ return;
+ }
+
+ // Unpin file, rename to .complete
+ File completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
+ ".complete." + FileUtil.getExtension(saveFile.getName()));
+
+ if(!saveFile.renameTo(completeFile)) {
+ Log.w(TAG, "Failed to upin " + saveFile + " to " + completeFile);
+ } else {
+ try {
+ new MediaStoreService(context).renameInMediaStore(completeFile, saveFile);
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to write to media store");
+ }
+ }
+ }
+
+ 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;
+ }
+ public static boolean verifyCanWrite(File dir) {
+ if(ensureDirectoryExistsAndIsReadWritable(dir)) {
+ try {
+ File tmp = new File(dir, "checkWrite");
+ tmp.createNewFile();
+ if(tmp.exists()) {
+ if(tmp.delete()) {
+ return true;
+ } else {
+ Log.w(TAG, "Failed to delete temp file, retrying");
+
+ // This should never be reached since this is a file DSub created!
+ Thread.sleep(100L);
+ tmp = new File(dir, "checkWrite");
+ if(tmp.delete()) {
+ return true;
+ } else {
+ Log.w(TAG, "Failed retry to delete temp file");
+ return false;
+ }
+ }
+ } else {
+ Log.w(TAG, "Temp file does not actually exist");
+ return false;
+ }
+ } catch(Exception e) {
+ Log.w(TAG, "Failed to create tmp file", e);
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param filename The filename in question.
+ * @return The filename with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafe(String filename) {
+ if (filename == null || filename.trim().length() == 0) {
+ return "unnamed";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-");
+ }
+ return filename;
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like colons (":")
+ * with dashes ("-").
+ *
+ * @param path The path of the directory in question.
+ * @return The the directory name with special characters replaced by hyphens.
+ */
+ private static String fileSystemSafeDir(String path) {
+ if (path == null || path.trim().length() == 0) {
+ return "";
+ }
+
+ for (String s : FILE_SYSTEM_UNSAFE_DIR) {
+ path = path.replace(s, "-");
+ }
+ return path;
+ }
+
+ /**
+ * Similar to {@link File#listFiles()}, but returns a sorted set.
+ * Never returns {@code null}, instead a warning is logged, and an empty set is returned.
+ */
+ public static SortedSet<File> listFiles(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ Log.w(TAG, "Failed to list children for " + dir.getPath());
+ return new TreeSet<File>();
+ }
+
+ return new TreeSet<File>(Arrays.asList(files));
+ }
+
+ public static SortedSet<File> listMediaFiles(File dir) {
+ SortedSet<File> files = listFiles(dir);
+ Iterator<File> iterator = files.iterator();
+ while (iterator.hasNext()) {
+ File file = iterator.next();
+ if (!file.isDirectory() && !isMediaFile(file)) {
+ iterator.remove();
+ }
+ }
+ return files;
+ }
+
+ private static boolean isMediaFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension);
+ }
+
+ public static boolean isMusicFile(File file) {
+ String extension = getExtension(file.getName());
+ return MUSIC_FILE_EXTENSIONS.contains(extension);
+ }
+ public static boolean isVideoFile(File file) {
+ String extension = getExtension(file.getName());
+ return VIDEO_FILE_EXTENSIONS.contains(extension);
+ }
+
+ public static boolean isPlaylistFile(File file) {
+ String extension = getExtension(file.getName());
+ return PLAYLIST_FILE_EXTENSIONS.contains(extension);
+ }
+
+ /**
+ * Returns the extension (the substring after the last dot) of the given file. The dot
+ * is not included in the returned extension.
+ *
+ * @param name The filename in question.
+ * @return The extension, or an empty string if no extension is found.
+ */
+ public static String getExtension(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? "" : name.substring(index + 1).toLowerCase();
+ }
+
+ /**
+ * Returns the base name (the substring before the last dot) of the given file. The dot
+ * is not included in the returned basename.
+ *
+ * @param name The filename in question.
+ * @return The base name, or an empty string if no basename is found.
+ */
+ public static String getBaseName(String name) {
+ int index = name.lastIndexOf('.');
+ return index == -1 ? name : name.substring(0, index);
+ }
+
+ public static Pair<Long, Long> getUsedSize(Context context, File file) {
+ long number = 0L;
+ long size = 0L;
+
+ if(file.isFile()) {
+ if(isMediaFile(file)) {
+ return new Pair<Long, Long>(1L, file.length());
+ } else {
+ return new Pair<Long, Long>(0L, 0L);
+ }
+ } else {
+ for (File child : FileUtil.listFiles(file)) {
+ Pair<Long, Long> pair = getUsedSize(context, child);
+ number += pair.getFirst();
+ size += pair.getSecond();
+ }
+ return new Pair<Long, Long>(number, size);
+ }
+ }
+
+ public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
+ Output out = null;
+ try {
+ RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "rw");
+ out = new Output(new FileOutputStream(file.getFD()));
+ synchronized (kryo) {
+ kryo.writeObject(out, obj);
+ }
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize object to " + fileName);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ public static <T extends Serializable> T deserialize(Context context, String fileName, Class<T> tClass) {
+ return deserialize(context, fileName, tClass, 0);
+ }
+
+ public static <T extends Serializable> T deserialize(Context context, String fileName, Class<T> tClass, int hoursOld) {
+ Input in = null;
+ try {
+ File file = new File(context.getCacheDir(), fileName);
+ if(!file.exists()) {
+ return null;
+ }
+
+ if(hoursOld != 0) {
+ Date fileDate = new Date(file.lastModified());
+ // Convert into hours
+ long age = (new Date().getTime() - fileDate.getTime()) / 1000 / 3600;
+ if(age > hoursOld) {
+ return null;
+ }
+ }
+
+ RandomAccessFile randomFile = new RandomAccessFile(file, "r");
+
+ in = new Input(new FileInputStream(randomFile.getFD()));
+ synchronized (kryo) {
+ T result = kryo.readObject(in, tClass);
+ return result;
+ }
+ } catch(FileNotFoundException e) {
+ // Different error message
+ Log.w(TAG, "No serialization for object from " + fileName);
+ return null;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize object from " + fileName);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+
+ public static <T extends Serializable> boolean serializeCompressed(Context context, T obj, String fileName) {
+ Output out = null;
+ try {
+ RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "rw");
+ out = new Output(new DeflaterOutputStream(new FileOutputStream(file.getFD())));
+ synchronized (kryo) {
+ kryo.writeObject(out, obj);
+ }
+ return true;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to serialize compressed object to " + fileName);
+ return false;
+ } finally {
+ Util.close(out);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+ public static <T extends Serializable> T deserializeCompressed(Context context, String fileName, Class<T> tClass) {
+ Input in = null;
+ try {
+ RandomAccessFile file = new RandomAccessFile(context.getCacheDir() + "/" + fileName, "r");
+
+ in = new Input(new InflaterInputStream(new FileInputStream(file.getFD())));
+ synchronized (kryo) {
+ T result = kryo.readObject(in, tClass);
+ return result;
+ }
+ } catch(FileNotFoundException e) {
+ // Different error message
+ Log.w(TAG, "No serialization compressed for object from " + fileName);
+ return null;
+ } catch (Throwable x) {
+ Log.w(TAG, "Failed to deserialize compressed object from " + fileName);
+ return null;
+ } finally {
+ Util.close(in);
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
new file mode 100644
index 00000000..1a0e8242
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ImageLoader.java
@@ -0,0 +1,600 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+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.media.RemoteControlClient;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.support.v4.util.LruCache;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.ArtistInfo;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.ServerInfo;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+
+/**
+ * Asynchronous loading of images, with caching.
+ * <p/>
+ * There should normally be only one instance of this class.
+ *
+ * @author Sindre Mehus
+ */
+public class ImageLoader {
+ private static final String TAG = ImageLoader.class.getSimpleName();
+
+ private Context context;
+ private LruCache<String, Bitmap> cache;
+ private Handler handler;
+ private Bitmap nowPlaying;
+ private final int imageSizeDefault;
+ private final int imageSizeLarge;
+ private final int avatarSizeDefault;
+ private boolean clearingCache = false;
+
+ private final static int[] COLORS = {0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444};
+
+ public ImageLoader(Context context) {
+ this.context = context;
+ handler = new Handler(Looper.getMainLooper());
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 4;
+
+ // Determine the density-dependent image sizes.
+ imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ imageSizeLarge = Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
+ avatarSizeDefault = context.getResources().getDrawable(R.drawable.ic_social_person).getIntrinsicHeight();
+
+ cache = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(String key, Bitmap bitmap) {
+ return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
+ }
+
+ @Override
+ protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
+ if(evicted) {
+ if(oldBitmap != nowPlaying && key.indexOf("unknown") != 0 || clearingCache) {
+ if(sizeOf("", oldBitmap) > 500) {
+ oldBitmap.recycle();
+ }
+ } else {
+ cache.put(key, oldBitmap);
+ }
+ }
+ }
+ };
+ }
+
+ public void clearCache() {
+ nowPlaying = null;
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ clearingCache = true;
+ cache.evictAll();
+ clearingCache = false;
+ return null;
+ }
+ }.execute();
+ }
+
+ private Bitmap getUnknownImage(MusicDirectory.Entry entry, int size) {
+ String key;
+ int color;
+ if(entry == null) {
+ key = getKey("unknown", size);
+ color = COLORS[0];
+
+ return getUnknownImage(key, size, color, null, null);
+ } else {
+ key = getKey(entry.getId() + "unknown", size);
+ String hash;
+ if(entry.getAlbum() != null) {
+ hash = entry.getAlbum();
+ } else if(entry.getArtist() != null) {
+ hash = entry.getArtist();
+ } else {
+ hash = entry.getId();
+ }
+ color = COLORS[Math.abs(hash.hashCode()) % COLORS.length];
+
+ return getUnknownImage(key, size, color, entry.getAlbum(), entry.getArtist());
+ }
+ }
+ private Bitmap getUnknownImage(String key, int size, int color, String topText, String bottomText) {
+ Bitmap bitmap = cache.get(key);
+ if(bitmap == null) {
+ bitmap = createUnknownImage(size, color, topText, bottomText);
+ cache.put(key, bitmap);
+ }
+
+ return bitmap;
+ }
+ private Bitmap createUnknownImage(int size, int primaryColor, String topText, String bottomText) {
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint color = new Paint();
+ color.setColor(primaryColor);
+ canvas.drawRect(0, 0, size, size * 2.0f / 3.0f, color);
+
+ color.setShader(new LinearGradient(0, 0, 0, size / 3.0f, Color.rgb(82, 82, 82), Color.BLACK, Shader.TileMode.MIRROR));
+ canvas.drawRect(0, size * 2.0f / 3.0f, size, size, color);
+
+ if(topText != null || bottomText != null) {
+ Paint font = new Paint();
+ font.setFlags(Paint.ANTI_ALIAS_FLAG);
+ font.setColor(Color.WHITE);
+ font.setTextSize(3.0f + size * 0.07f);
+
+ if(topText != null) {
+ canvas.drawText(topText, size * 0.05f, size * 0.6f, font);
+ }
+
+ if(bottomText != null) {
+ canvas.drawText(bottomText, size * 0.05f, size * 0.8f, font);
+ }
+ }
+
+ return bitmap;
+ }
+
+ public Bitmap getCachedImage(Context context, MusicDirectory.Entry entry, boolean large) {
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ if(entry == null || entry.getCoverArt() == null) {
+ return getUnknownImage(entry, size);
+ }
+
+ Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size));
+ if(bitmap == null || bitmap.isRecycled()) {
+ bitmap = FileUtil.getAlbumArtBitmap(context, entry, size);
+ String key = getKey(entry.getCoverArt(), size);
+ cache.put(key, bitmap);
+ cache.get(key);
+ }
+
+ return bitmap;
+ }
+
+ public SilentBackgroundTask loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
+ // TODO: If we know this a artist, try to load artist info instead
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ if(entry != null && !entry.isAlbum() && ServerInfo.checkServerVersion(context, "1.11") && !Util.isOffline(context)) {
+ SilentBackgroundTask task = new ArtistImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade);
+ task.execute();
+ return task;
+ } else if(entry != null && entry.getCoverArt() == null && entry.isDirectory() && !Util.isOffline(context)) {
+ // Try to lookup child cover art
+ MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, entry, true);
+ if(firstChild != null) {
+ entry.setCoverArt(firstChild.getCoverArt());
+ }
+ }
+
+ Bitmap bitmap;
+ if (entry == null || entry.getCoverArt() == null) {
+ bitmap = getUnknownImage(entry, size);
+ setImage(view, Util.createDrawableFromBitmap(context, bitmap), crossfade);
+ return null;
+ }
+
+ bitmap = cache.get(getKey(entry.getCoverArt(), size));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(view, drawable, crossfade);
+ if(large) {
+ nowPlaying = bitmap;
+ }
+ return null;
+ }
+
+ if (!large) {
+ setImage(view, Util.createDrawableFromBitmap(context, null), false);
+ }
+ ImageTask task = new ViewImageTask(view.getContext(), entry, size, imageSizeLarge, large, view, crossfade);
+ task.execute();
+ return task;
+ }
+
+ public SilentBackgroundTask<Void> loadImage(View view, String url, boolean large) {
+ Bitmap bitmap;
+ int size = large ? imageSizeLarge : imageSizeDefault;
+ if (url == null) {
+ String key = getKey(url + "unknown", size);
+ int color = COLORS[Math.abs(key.hashCode()) % COLORS.length];
+ bitmap = getUnknownImage(key, size, color, null, null);
+ setImage(view, Util.createDrawableFromBitmap(context, bitmap), true);
+ return null;
+ }
+
+ bitmap = cache.get(getKey(url, size));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(view, drawable, true);
+ return null;
+ }
+
+ SilentBackgroundTask<Void> task = new ViewUrlTask(view.getContext(), view, url, size);
+ task.execute();
+ return task;
+ }
+
+ public SilentBackgroundTask<Void> loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) {
+ Bitmap bitmap;
+ if (entry == null || entry.getCoverArt() == null) {
+ bitmap = getUnknownImage(entry, imageSizeLarge);
+ setImage(remoteControl, Util.createDrawableFromBitmap(context, bitmap));
+ return null;
+ }
+
+ bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge));
+ if (bitmap != null && !bitmap.isRecycled()) {
+ Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ setImage(remoteControl, drawable);
+ return null;
+ }
+
+ setImage(remoteControl, Util.createDrawableFromBitmap(context, null));
+ ImageTask task = new RemoteControlClientImageTask(context, entry, imageSizeLarge, imageSizeLarge, false, remoteControl);
+ task.execute();
+ return task;
+ }
+
+ public SilentBackgroundTask<Void> loadAvatar(Context context, ImageView view, String username) {
+ Bitmap bitmap = cache.get(username);
+ if (bitmap != null && !bitmap.isRecycled()) {
+ Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
+ view.setImageDrawable(drawable);
+ return null;
+ }
+
+ SilentBackgroundTask<Void> task = new AvatarTask(context, view, username);
+ task.execute();
+ return task;
+ }
+
+ private String getKey(String coverArtId, int size) {
+ return coverArtId + size;
+ }
+
+ private void setImage(View view, final 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) {
+ final ImageView imageView = (ImageView) view;
+ if (crossfade && drawable != null) {
+ Drawable existingDrawable = imageView.getDrawable();
+ if (existingDrawable == null) {
+ Bitmap emptyImage;
+ if(drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) {
+ emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ } else {
+ emptyImage = Bitmap.createBitmap(imageSizeDefault, imageSizeDefault, Bitmap.Config.ARGB_8888);
+ }
+ existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
+ } else if(existingDrawable instanceof TransitionDrawable) {
+ // This should only ever be used if user is skipping through many songs quickly
+ TransitionDrawable tmp = (TransitionDrawable) existingDrawable;
+ existingDrawable = tmp.getDrawable(tmp.getNumberOfLayers() - 1);
+ }
+ if(existingDrawable != null && drawable != null) {
+ Drawable[] layers = new Drawable[]{existingDrawable, drawable};
+ final TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(250);
+
+ // Get rid of transition drawable after transition occurs
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // Only execute if still on same transition drawable
+ if (imageView.getDrawable() == transitionDrawable) {
+ imageView.setImageDrawable(drawable);
+ }
+ }
+ }, 500L);
+ } else {
+ imageView.setImageDrawable(drawable);
+ }
+ } else {
+ imageView.setImageDrawable(drawable);
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setImage(RemoteControlClient remoteControl, Drawable drawable) {
+ if(remoteControl != null && drawable != null) {
+ Bitmap origBitmap = ((BitmapDrawable)drawable).getBitmap();
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && origBitmap != null) {
+ origBitmap = origBitmap.copy(origBitmap.getConfig(), false);
+ }
+ if ( origBitmap != null && !origBitmap.isRecycled()) {
+ remoteControl.editMetadata(false).putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, origBitmap).apply();
+ } else {
+ if(origBitmap != null) {
+ Log.e(TAG, "Tried to load a recycled bitmap.");
+ }
+ remoteControl.editMetadata(false)
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null)
+ .apply();
+ }
+ }
+ }
+
+ public abstract class ImageTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final MusicDirectory.Entry mEntry;
+ private final int mSize;
+ private final int mSaveSize;
+ private final boolean mIsNowPlaying;
+ protected Drawable mDrawable;
+
+ public ImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying) {
+ super(context);
+ mContext = context;
+ mEntry = entry;
+ mSize = size;
+ mSaveSize = saveSize;
+ mIsNowPlaying = isNowPlaying;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getCoverArt(mContext, mEntry, mSize, null, this);
+ String key = getKey(mEntry.getCoverArt(), mSize);
+ cache.put(key, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(key);
+ if(mIsNowPlaying) {
+ nowPlaying = bitmap;
+ }
+
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ cancelled.set(true);
+ }
+
+ return null;
+ }
+ }
+
+ private class ViewImageTask extends ImageTask {
+ protected boolean mCrossfade;
+ private View mView;
+
+ public ViewImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, View view, boolean crossfade) {
+ super(context, entry, size, saveSize, isNowPlaying);
+
+ mView = view;
+ mCrossfade = crossfade;
+ }
+
+ @Override
+ protected void done(Void result) {
+ setImage(mView, mDrawable, mCrossfade);
+ }
+ }
+
+ private class RemoteControlClientImageTask extends ImageTask {
+ private RemoteControlClient mRemoteControl;
+
+ public RemoteControlClientImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, RemoteControlClient remoteControl) {
+ super(context, entry, size, saveSize, isNowPlaying);
+
+ mRemoteControl = remoteControl;
+ }
+
+ @Override
+ protected void done(Void result) {
+ setImage(mRemoteControl, mDrawable);
+ }
+ }
+
+ private class ArtistImageTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final MusicDirectory.Entry mEntry;
+ private final int mSize;
+ private final int mSaveSize;
+ private final boolean mIsNowPlaying;
+ private Drawable mDrawable;
+ private boolean mCrossfade;
+ private View mView;
+
+ private SilentBackgroundTask subTask;
+
+ public ArtistImageTask(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean isNowPlaying, View view, boolean crossfade) {
+ super(context);
+ mContext = context;
+ mEntry = entry;
+ mSize = size;
+ mSaveSize = saveSize;
+ mIsNowPlaying = isNowPlaying;
+ mView = view;
+ mCrossfade = crossfade;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ ArtistInfo artistInfo = musicService.getArtistInfo(mEntry.getId(), false, true, mContext, null);
+ String url = artistInfo.getImageUrl();
+
+ // Figure out whether we are going to get a artist image or the standard image
+ if(url != null && !"".equals(url.trim())) {
+ // If getting the artist image fails for any reason, retry for the standard version
+ subTask = new ViewUrlTask(mContext, mView, url, mSize) {
+ @Override
+ protected void failedToDownload() {
+ // Call loadImage so we can take advantage of all of it's logic checks
+ loadImage(mView, mEntry, mSize == imageSizeLarge, mCrossfade);
+
+ // Delete subTask so it doesn't get called in done
+ subTask = null;
+ }
+ };
+ } else {
+ if (mEntry != null && mEntry.getCoverArt() == null && mEntry.isDirectory() && !Util.isOffline(context)) {
+ // Try to lookup child cover art
+ MusicDirectory.Entry firstChild = FileUtil.lookupChild(context, mEntry, true);
+ if (firstChild != null) {
+ mEntry.setCoverArt(firstChild.getCoverArt());
+ }
+ }
+
+ if (mEntry != null && mEntry.getCoverArt() != null) {
+ subTask = new ViewImageTask(mContext, mEntry, mSize, mSaveSize, mIsNowPlaying, mView, mCrossfade);
+ } else {
+ // If entry is null as well, we need to just set as a blank image
+ Bitmap bitmap = getUnknownImage(mEntry, mSize);
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ return null;
+ }
+ }
+
+ // Execute whichever way we decided to go
+ subTask.doInBackground();
+ return null;
+ }
+
+ @Override
+ public void done(Void result) {
+ if(subTask != null) {
+ subTask.done(result);
+ } else if(mDrawable != null) {
+ setImage(mView, mDrawable, mCrossfade);
+ }
+ }
+ }
+
+ private class ViewUrlTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final String mUrl;
+ private final ImageView mView;
+ private Drawable mDrawable;
+ private int mSize;
+
+ public ViewUrlTask(Context context, View view, String url, int size) {
+ super(context);
+ mContext = context;
+ mView = (ImageView) view;
+ mUrl = url;
+ mSize = size;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getBitmap(mUrl, mSize, mContext, null, this);
+ if(bitmap != null) {
+ String key = getKey(mUrl, mSize);
+ cache.put(key, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(key);
+
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ }
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download from url " + mUrl, x);
+ cancelled.set(true);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(mDrawable != null) {
+ mView.setImageDrawable(mDrawable);
+ } else {
+ failedToDownload();
+ }
+ }
+
+ protected void failedToDownload() {
+
+ }
+ }
+
+ private class AvatarTask extends SilentBackgroundTask<Void> {
+ private final Context mContext;
+ private final String mUsername;
+ private final ImageView mView;
+ private Drawable mDrawable;
+
+ public AvatarTask(Context context, ImageView view, String username) {
+ super(context);
+ mContext = context;
+ mView = view;
+ mUsername = username;
+ }
+
+ @Override
+ protected Void doInBackground() throws Throwable {
+ try {
+ MusicService musicService = MusicServiceFactory.getMusicService(mContext);
+ Bitmap bitmap = musicService.getAvatar(mUsername, avatarSizeDefault, mContext, null, this);
+ if(bitmap != null) {
+ cache.put(mUsername, bitmap);
+ // Make sure key is the most recently "used"
+ cache.get(mUsername);
+
+ mDrawable = Util.createDrawableFromBitmap(mContext, bitmap);
+ }
+ } catch (Throwable x) {
+ Log.e(TAG, "Failed to download album art.", x);
+ cancelled.set(true);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(mDrawable != null) {
+ mView.setImageDrawable(mDrawable);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java b/app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java
new file mode 100644
index 00000000..116da816
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/LoadingTask.java
@@ -0,0 +1,73 @@
+package github.daneren2005.dsub.util;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class LoadingTask<T> extends BackgroundTask<T> {
+
+ private final Activity tabActivity;
+ private ProgressDialog loading;
+ private final boolean cancellable;
+
+ public LoadingTask(Activity activity) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = true;
+ }
+ public LoadingTask(Activity activity, final boolean cancellable) {
+ super(activity);
+ tabActivity = activity;
+ this.cancellable = cancellable;
+ }
+
+ @Override
+ public void execute() {
+ loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ cancel();
+ }
+ });
+
+ queue.offer(task = new Task() {
+ @Override
+ public void onDone(T result) {
+ if(loading.isShowing()) {
+ loading.dismiss();
+ }
+ done(result);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ if(loading.isShowing()) {
+ loading.dismiss();
+ }
+ error(t);
+ }
+ });
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return (tabActivity instanceof SubsonicActivity && ((SubsonicActivity) tabActivity).isDestroyedCompat()) || cancelled.get();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ if(!cancelled.get()) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ loading.setMessage(message);
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
new file mode 100644
index 00000000..9aa54c4b
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java
@@ -0,0 +1,181 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util;
+
+import android.os.Build;
+import android.support.v7.media.MediaRouteProvider;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.domain.RemoteControlState;
+import github.daneren2005.dsub.provider.DLNARouteProvider;
+import github.daneren2005.dsub.provider.JukeboxRouteProvider;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+import github.daneren2005.dsub.util.compat.CastCompat;
+
+import static android.support.v7.media.MediaRouter.RouteInfo;
+
+/**
+ * Created by owner on 2/8/14.
+ */
+public class MediaRouteManager extends MediaRouter.Callback {
+ private static final String TAG = MediaRouteManager.class.getSimpleName();
+ private static boolean castAvailable = false;
+
+ private DownloadService downloadService;
+ private MediaRouter router;
+ private MediaRouteSelector selector;
+ private List<MediaRouteProvider> providers = new ArrayList<MediaRouteProvider>();
+ private List<MediaRouteProvider> onlineProviders = new ArrayList<MediaRouteProvider>();
+
+ static {
+ try {
+ CastCompat.checkAvailable();
+ castAvailable = true;
+ } catch(Throwable t) {
+ castAvailable = false;
+ }
+ }
+
+ public MediaRouteManager(DownloadService downloadService) {
+ this.downloadService = downloadService;
+ router = MediaRouter.getInstance(downloadService);
+
+ // Check if play services is available
+ int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(downloadService);
+ if(result != ConnectionResult.SUCCESS){
+ Log.w(TAG, "No play services, failed with result: " + result);
+ castAvailable = false;
+ }
+
+ addProviders();
+ buildSelector();
+ }
+
+ public void destroy() {
+ for(MediaRouteProvider provider: providers) {
+ router.removeProvider(provider);
+ }
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, RouteInfo info) {
+ if(castAvailable) {
+ RemoteController controller = CastCompat.getController(downloadService, info);
+ if(controller != null) {
+ downloadService.setRemoteEnabled(RemoteControlState.CHROMECAST, controller);
+ }
+ }
+
+ if(downloadService.isRemoteEnabled()) {
+ downloadService.registerRoute(router);
+ }
+ }
+ @Override
+ public void onRouteUnselected(MediaRouter router, RouteInfo info) {
+ if(downloadService.isRemoteEnabled()) {
+ downloadService.unregisterRoute(router);
+ }
+
+ downloadService.setRemoteEnabled(RemoteControlState.LOCAL);
+ }
+
+ public void setDefaultRoute() {
+ router.selectRoute(router.getDefaultRoute());
+ }
+
+ public void startScan() {
+ router.addCallback(selector, this, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+ public void stopScan() {
+ router.removeCallback(this);
+ }
+
+ public MediaRouteSelector getSelector() {
+ return selector;
+ }
+
+ public RouteInfo getSelectedRoute() {
+ return router.getSelectedRoute();
+ }
+ public RouteInfo getRouteForId(String id) {
+ if(id == null) {
+ return null;
+ }
+
+ // Try to find matching id
+ for(RouteInfo info: router.getRoutes()) {
+ if(id.equals(info.getId())) {
+ router.selectRoute(info);
+ return info;
+ }
+ }
+
+ return null;
+ }
+ public RemoteController getRemoteController(RouteInfo info) {
+ if(castAvailable) {
+ return CastCompat.getController(downloadService, info);
+ } else {
+ return null;
+ }
+ }
+
+ public void addOnlineProviders() {
+ JukeboxRouteProvider jukeboxProvider = new JukeboxRouteProvider(downloadService);
+ router.addProvider(jukeboxProvider);
+ providers.add(jukeboxProvider);
+ onlineProviders.add(jukeboxProvider);
+ }
+ public void removeOnlineProviders() {
+ for(MediaRouteProvider provider: onlineProviders) {
+ router.removeProvider(provider);
+ }
+ }
+
+ private void addProviders() {
+ if(!Util.isOffline(downloadService)) {
+ addOnlineProviders();
+ }
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ DLNARouteProvider dlnaProvider = new DLNARouteProvider(downloadService);
+ router.addProvider(dlnaProvider);
+ providers.add(dlnaProvider);
+ }
+ }
+ public void buildSelector() {
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ if(UserUtil.canJukebox()) {
+ builder.addControlCategory(JukeboxRouteProvider.CATEGORY_JUKEBOX_ROUTE);
+ }
+ if(castAvailable) {
+ builder.addControlCategory(CastCompat.getCastControlCategory());
+ }
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA);
+ }
+ selector = builder.build();
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Notifications.java b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
new file mode 100644
index 00000000..d078d77e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Notifications.java
@@ -0,0 +1,348 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.os.Handler;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.provider.DSubWidgetProvider;
+import github.daneren2005.dsub.service.DownloadFile;
+import github.daneren2005.dsub.service.DownloadService;
+
+public final class Notifications {
+ private static final String TAG = Notifications.class.getSimpleName();
+
+ // Notification IDs.
+ public static final int NOTIFICATION_ID_PLAYING = 100;
+ public static final int NOTIFICATION_ID_DOWNLOADING = 102;
+ public static final String NOTIFICATION_SYNC_GROUP = "github.daneren2005.dsub.sync";
+
+ private static boolean playShowing = false;
+ private static boolean downloadShowing = false;
+ private static boolean downloadForeground = false;
+
+ private final static Pair<Integer, Integer> NOTIFICATION_TEXT_COLORS = new Pair<Integer, Integer>();
+
+ public static void showPlayingNotification(final Context context, final DownloadService downloadService, final Handler handler, MusicDirectory.Entry song) {
+ // Set the icon, scrolling text and timestamp
+ final Notification notification = new Notification(R.drawable.stat_notify_playing, song.getTitle(), System.currentTimeMillis());
+
+ final boolean playing = downloadService.getPlayerState() == PlayerState.STARTED;
+ if(playing) {
+ notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
+ }
+ boolean remote = downloadService.isRemoteEnabled();
+ if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.JELLY_BEAN){
+ RemoteViews expandedContentView = new RemoteViews(context.getPackageName(), R.layout.notification_expanded);
+ setupViews(expandedContentView ,context, song, true, playing, remote);
+ notification.bigContentView = expandedContentView;
+ notification.priority = Notification.PRIORITY_HIGH;
+ }
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ notification.visibility = Notification.VISIBILITY_PUBLIC;
+ }
+
+ RemoteViews smallContentView = new RemoteViews(context.getPackageName(), R.layout.notification);
+ setupViews(smallContentView, context, song, false, playing, remote);
+ notification.contentView = smallContentView;
+
+ Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ notification.contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
+
+ playShowing = true;
+ if(downloadForeground && downloadShowing) {
+ downloadForeground = false;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size());
+ downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ }
+ });
+ } else {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if(playing) {
+ downloadService.startForeground(NOTIFICATION_ID_PLAYING, notification);
+ } else {
+ playShowing = false;
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ downloadService.stopForeground(true);
+ notificationManager.notify(NOTIFICATION_ID_PLAYING, notification);
+ }
+ }
+ });
+ }
+
+ // Update widget
+ DSubWidgetProvider.notifyInstances(context, downloadService, playing);
+ }
+
+ private static void setupViews(RemoteViews rv, Context context, MusicDirectory.Entry song, boolean expanded, boolean playing, boolean remote){
+
+ // Use the same text for the ticker and the expanded notification
+ String title = song.getTitle();
+ String arist = song.getArtist();
+ String album = song.getAlbum();
+
+ // Set the album art.
+ try {
+ ImageLoader imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ Bitmap bitmap = null;
+ if(imageLoader != null) {
+ bitmap = imageLoader.getCachedImage(context, song, false);
+ }
+ if (bitmap == null) {
+ // set default album art
+ rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ } else {
+ rv.setImageViewBitmap(R.id.notification_image, bitmap);
+ }
+ } catch (Exception x) {
+ Log.w(TAG, "Failed to get notification cover art", x);
+ rv.setImageViewResource(R.id.notification_image, R.drawable.unknown_album);
+ }
+
+ // set the text for the notifications
+ rv.setTextViewText(R.id.notification_title, title);
+ rv.setTextViewText(R.id.notification_artist, arist);
+ rv.setTextViewText(R.id.notification_album, album);
+
+ boolean persistent = Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_PERSISTENT_NOTIFICATION, false);
+ if(persistent) {
+ if(expanded) {
+ rv.setImageViewResource(R.id.control_pause, playing ? R.drawable.notification_pause : R.drawable.notification_play);
+ } else {
+ rv.setImageViewResource(R.id.control_previous, playing ? R.drawable.notification_pause : R.drawable.notification_play);
+ rv.setImageViewResource(R.id.control_pause, R.drawable.notification_next);
+ rv.setImageViewResource(R.id.control_next, R.drawable.notification_close);
+ }
+ }
+
+ // Create actions for media buttons
+ PendingIntent pendingIntent;
+ int previous = 0, pause = 0, next = 0, close = 0;
+ if(persistent && !expanded) {
+ pause = R.id.control_previous;
+ next = R.id.control_pause;
+ close = R.id.control_next;
+ } else {
+ previous = R.id.control_previous;
+ pause = R.id.control_pause;
+ next = R.id.control_next;
+ }
+
+ if((remote || persistent) && close == 0 && expanded) {
+ close = R.id.notification_close;
+ rv.setViewVisibility(close, View.VISIBLE);
+ }
+
+ if(previous > 0) {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_PREVIOUS");
+ prevIntent.setComponent(new ComponentName(context, DownloadService.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(previous, pendingIntent);
+ }
+ if(pause > 0) {
+ if(playing) {
+ Intent pauseIntent = new Intent("KEYCODE_MEDIA_PLAY_PAUSE");
+ pauseIntent.setComponent(new ComponentName(context, DownloadService.class));
+ pauseIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ pendingIntent = PendingIntent.getService(context, 0, pauseIntent, 0);
+ rv.setOnClickPendingIntent(pause, pendingIntent);
+ } else {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_START");
+ prevIntent.setComponent(new ComponentName(context, DownloadService.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(pause, pendingIntent);
+ }
+ }
+ if(next > 0) {
+ Intent nextIntent = new Intent("KEYCODE_MEDIA_NEXT");
+ nextIntent.setComponent(new ComponentName(context, DownloadService.class));
+ nextIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
+ pendingIntent = PendingIntent.getService(context, 0, nextIntent, 0);
+ rv.setOnClickPendingIntent(next, pendingIntent);
+ }
+ if(close > 0) {
+ Intent prevIntent = new Intent("KEYCODE_MEDIA_STOP");
+ prevIntent.setComponent(new ComponentName(context, DownloadService.class));
+ prevIntent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP));
+ pendingIntent = PendingIntent.getService(context, 0, prevIntent, 0);
+ rv.setOnClickPendingIntent(close, pendingIntent);
+ }
+ }
+
+ public static void hidePlayingNotification(final Context context, final DownloadService downloadService, Handler handler) {
+ playShowing = false;
+
+ // Remove notification and remove the service from the foreground
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ }
+ });
+
+ // Get downloadNotification in foreground if playing
+ if(downloadShowing) {
+ showDownloadingNotification(context, downloadService, handler, downloadService.getCurrentDownloading(), downloadService.getBackgroundDownloads().size());
+ }
+
+ // Update widget
+ DSubWidgetProvider.notifyInstances(context, downloadService, false);
+ }
+
+ public static void showDownloadingNotification(final Context context, final DownloadService downloadService, Handler handler, DownloadFile file, int size) {
+ Intent cancelIntent = new Intent(context, DownloadService.class);
+ cancelIntent.setAction(DownloadService.CANCEL_DOWNLOADS);
+ PendingIntent cancelPI = PendingIntent.getService(context, 0, cancelIntent, 0);
+
+ String currentDownloading, currentSize;
+ if(file != null) {
+ currentDownloading = file.getSong().getTitle();
+ currentSize = Util.formatLocalizedBytes(file.getEstimatedSize(), context);
+ } else {
+ currentDownloading = "none";
+ currentSize = "0";
+ }
+
+ NotificationCompat.Builder builder;
+ builder = new NotificationCompat.Builder(context)
+ .setSmallIcon(android.R.drawable.stat_sys_download)
+ .setContentTitle(context.getResources().getString(R.string.download_downloading_title, size))
+ .setContentText(context.getResources().getString(R.string.download_downloading_summary, currentDownloading))
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(context.getResources().getString(R.string.download_downloading_summary_expanded, currentDownloading, currentSize)))
+ .setProgress(10, 5, true)
+ .setOngoing(true)
+ .addAction(R.drawable.notification_close,
+ context.getResources().getString(R.string.common_cancel),
+ cancelPI);
+
+ Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD, true);
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_DOWNLOAD_VIEW, true);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ builder.setContentIntent(PendingIntent.getActivity(context, 1, notificationIntent, 0));
+
+ final Notification notification = builder.build();
+ downloadShowing = true;
+ if(playShowing) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_ID_DOWNLOADING, notification);
+ } else {
+ downloadForeground = true;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.startForeground(NOTIFICATION_ID_DOWNLOADING, notification);
+ }
+ });
+ }
+
+ }
+ public static void hideDownloadingNotification(final Context context, final DownloadService downloadService, Handler handler) {
+ downloadShowing = false;
+ if(playShowing) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(NOTIFICATION_ID_DOWNLOADING);
+ } else {
+ downloadForeground = false;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ downloadService.stopForeground(true);
+ }
+ });
+ }
+ }
+
+ public static void showSyncNotification(final Context context, int stringId, String extra) {
+ if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_SYNC_NOTIFICATION, true)) {
+ if(extra == null) {
+ extra = "";
+ }
+
+ NotificationCompat.Builder builder;
+ builder = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.stat_notify_sync)
+ .setContentTitle(context.getResources().getString(stringId))
+ .setContentText(extra)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(extra.replace(", ", "\n")))
+ .setOngoing(false)
+ .setGroup(NOTIFICATION_SYNC_GROUP)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setAutoCancel(true);
+
+ Intent notificationIntent = new Intent(context, SubsonicFragmentActivity.class);
+ notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ String tab = null, type = null;
+ switch(stringId) {
+ case R.string.sync_new_albums:
+ type = "newest";
+ break;
+ case R.string.sync_new_playlists:
+ tab = "Playlist";
+ break;
+ case R.string.sync_new_podcasts:
+ tab = "Podcast";
+ break;
+ case R.string.sync_new_starred:
+ type = "starred";
+ break;
+ }
+ if(tab != null) {
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_FRAGMENT_TYPE, tab);
+ }
+ if(type != null) {
+ notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
+ }
+
+ builder.setContentIntent(PendingIntent.getActivity(context, stringId, notificationIntent, 0));
+
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(stringId, builder.build());
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Pair.java b/app/src/main/java/github/daneren2005/dsub/util/Pair.java
new file mode 100644
index 00000000..54386a62
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Pair.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Pair<S, T> implements Serializable {
+
+ private S first;
+ private T second;
+
+ public Pair() {
+ }
+
+ public Pair(S first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public S getFirst() {
+ return first;
+ }
+
+ public void setFirst(S first) {
+ this.first = first;
+ }
+
+ public T getSecond() {
+ return second;
+ }
+
+ public void setSecond(T second) {
+ this.second = second;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
new file mode 100644
index 00000000..c6d58f42
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ProgressListener.java
@@ -0,0 +1,27 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+/**
+ * @author Sindre Mehus
+ */
+public interface ProgressListener {
+ void updateProgress(String message);
+ void updateProgress(int messageId);
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
new file mode 100644
index 00000000..7eb6d137
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SettingsBackupAgent.java
@@ -0,0 +1,31 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+*/
+package github.daneren2005.dsub.util;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import github.daneren2005.dsub.util.Constants;
+
+public class SettingsBackupAgent extends BackupAgentHelper {
+ public void onCreate() {
+ super.onCreate();
+ SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, Constants.PREFERENCES_FILE_NAME);
+ addHelper("mypreferences", helper);
+ }
+ }
diff --git a/app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java b/app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java
new file mode 100644
index 00000000..7da35f77
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/ShufflePlayBuffer.java
@@ -0,0 +1,212 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.util.FileUtil;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class ShufflePlayBuffer {
+
+ private static final String TAG = ShufflePlayBuffer.class.getSimpleName();
+ private static final String CACHE_FILENAME = "shuffleBuffer.ser";
+
+ private ScheduledExecutorService executorService;
+ private Runnable runnable;
+ private boolean firstRun = true;
+ private final ArrayList<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
+ private int lastCount = -1;
+ private DownloadService context;
+ private boolean awaitingResults = false;
+ private int capacity;
+ private int refillThreshold;
+
+ private SharedPreferences.OnSharedPreferenceChangeListener listener;
+ private int currentServer;
+ private String currentFolder = "";
+ private String genre = "";
+ private String startYear = "";
+ private String endYear = "";
+
+ public ShufflePlayBuffer(DownloadService context) {
+ this.context = context;
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ runnable = new Runnable() {
+ @Override
+ public void run() {
+ refill();
+ }
+ };
+ executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
+
+ // Calculate out the capacity and refill threshold based on the user's random size preference
+ int shuffleListSize = Integer.parseInt(Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_RANDOM_SIZE, "20"));
+ // ex: default 20 -> 50
+ capacity = shuffleListSize * 5 / 2;
+ capacity = Math.min(500, capacity);
+
+ // ex: default 20 -> 40
+ refillThreshold = capacity * 4 / 5;
+ }
+
+ public List<MusicDirectory.Entry> get(int size) {
+ clearBufferIfnecessary();
+ // Make sure fetcher is running if needed
+ restart();
+
+ List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size);
+ synchronized (buffer) {
+ boolean removed = false;
+ while (!buffer.isEmpty() && result.size() < size) {
+ result.add(buffer.remove(buffer.size() - 1));
+ removed = true;
+ }
+
+ // Re-cache if anything is taken out
+ if(removed) {
+ FileUtil.serialize(context, buffer, CACHE_FILENAME);
+ }
+ }
+ Log.i(TAG, "Taking " + result.size() + " songs from shuffle play buffer. " + buffer.size() + " remaining.");
+ if(result.isEmpty()) {
+ awaitingResults = true;
+ }
+ return result;
+ }
+
+ public void shutdown() {
+ executorService.shutdown();
+ Util.getPreferences(context).unregisterOnSharedPreferenceChangeListener(listener);
+ }
+
+ private void restart() {
+ synchronized(buffer) {
+ if(buffer.size() <= refillThreshold && lastCount != 0 && executorService.isShutdown()) {
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.scheduleWithFixedDelay(runnable, 0, 10, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ private void refill() {
+ // Check if active server has changed.
+ clearBufferIfnecessary();
+
+ if (buffer != null && (buffer.size() > refillThreshold || (!Util.isNetworkConnected(context) && !Util.isOffline(context)) || lastCount == 0)) {
+ executorService.shutdown();
+ return;
+ }
+
+ try {
+ MusicService service = MusicServiceFactory.getMusicService(context);
+
+ // Get capacity based
+ int n = capacity - buffer.size();
+ String folder = null;
+ if(!Util.isTagBrowsing(context)) {
+ folder = Util.getSelectedMusicFolderId(context);
+ }
+ MusicDirectory songs = service.getRandomSongs(n, folder, genre, startYear, endYear, context, null);
+
+ synchronized (buffer) {
+ lastCount = 0;
+ for(MusicDirectory.Entry entry: songs.getChildren()) {
+ if(!buffer.contains(entry) && entry.getRating() != 1) {
+ buffer.add(entry);
+ lastCount++;
+ }
+ }
+ Log.i(TAG, "Refilled shuffle play buffer with " + lastCount + " songs.");
+
+ // Cache buffer
+ FileUtil.serialize(context, buffer, CACHE_FILENAME);
+ }
+ } catch (Exception x) {
+ // Give it one more try before quitting
+ if(lastCount != -2) {
+ lastCount = -2;
+ } else if(lastCount == -2) {
+ lastCount = 0;
+ }
+ Log.w(TAG, "Failed to refill shuffle play buffer.", x);
+ }
+
+ if(awaitingResults) {
+ awaitingResults = false;
+ context.checkDownloads();
+ }
+ }
+
+ private void clearBufferIfnecessary() {
+ synchronized (buffer) {
+ final SharedPreferences prefs = Util.getPreferences(context);
+ if (currentServer != Util.getActiveServer(context)
+ || !Util.equals(currentFolder, Util.getSelectedMusicFolderId(context))
+ || (genre != null && !genre.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "")))
+ || (startYear != null && !startYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "")))
+ || (endYear != null && !endYear.equals(prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "")))) {
+ lastCount = -1;
+ currentServer = Util.getActiveServer(context);
+ currentFolder = Util.getSelectedMusicFolderId(context);
+ genre = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_GENRE, "");
+ startYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_START_YEAR, "");
+ endYear = prefs.getString(Constants.PREFERENCES_KEY_SHUFFLE_END_YEAR, "");
+ buffer.clear();
+
+ if(firstRun) {
+ ArrayList cacheList = FileUtil.deserialize(context, CACHE_FILENAME, ArrayList.class);
+ if(cacheList != null) {
+ buffer.addAll(cacheList);
+ }
+
+ listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ clearBufferIfnecessary();
+ restart();
+ }
+ };
+ prefs.registerOnSharedPreferenceChangeListener(listener);
+ firstRun = false;
+ } else {
+ // Clear cache
+ File file = new File(context.getCacheDir(), CACHE_FILENAME);
+ file.delete();
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java
new file mode 100644
index 00000000..b99b7e0e
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SilentBackgroundTask.java
@@ -0,0 +1,48 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2010 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.content.Context;
+
+/**
+ * @author Sindre Mehus
+ */
+public abstract class SilentBackgroundTask<T> extends BackgroundTask<T> {
+ public SilentBackgroundTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void execute() {
+ queue.offer(task = new Task());
+ }
+
+ @Override
+ protected void done(T result) {
+ // Don't do anything unless overriden
+ }
+
+ @Override
+ public void updateProgress(int messageId) {
+ }
+
+ @Override
+ public void updateProgress(String message) {
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java b/app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java
new file mode 100644
index 00000000..9c0b36a9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SimpleServiceBinder.java
@@ -0,0 +1,37 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.os.Binder;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SimpleServiceBinder<S> extends Binder {
+
+ private final S service;
+
+ public SimpleServiceBinder(S service) {
+ this.service = service;
+ }
+
+ public S getService() {
+ return service;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java b/app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java
new file mode 100644
index 00000000..a369715f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/SyncUtil.java
@@ -0,0 +1,222 @@
+package github.daneren2005.dsub.util;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.activity.SubsonicFragmentActivity;
+
+/**
+ * Created by Scott on 11/24/13.
+ */
+public final class SyncUtil {
+ private static String TAG = SyncUtil.class.getSimpleName();
+ private static ArrayList<SyncSet> syncedPlaylists;
+ private static ArrayList<SyncSet> syncedPodcasts;
+ private static String url;
+
+ private static void checkRestURL(Context context) {
+ int instance = Util.getActiveServer(context);
+ String newURL = Util.getRestUrl(context, null, instance, false);
+ if(url == null || !url.equals(newURL)) {
+ syncedPlaylists = null;
+ syncedPodcasts = null;
+ url = newURL;
+ }
+ }
+
+ // Playlist sync
+ public static boolean isSyncedPlaylist(Context context, String playlistId) {
+ checkRestURL(context);
+ if(syncedPlaylists == null) {
+ syncedPlaylists = getSyncedPlaylists(context);
+ }
+ return syncedPlaylists.contains(new SyncSet(playlistId));
+ }
+ public static ArrayList<SyncSet> getSyncedPlaylists(Context context) {
+ return getSyncedPlaylists(context, Util.getActiveServer(context));
+ }
+ public static ArrayList<SyncSet> getSyncedPlaylists(Context context, int instance) {
+ String syncFile = getPlaylistSyncFile(context, instance);
+ ArrayList<SyncSet> playlists = FileUtil.deserializeCompressed(context, syncFile, ArrayList.class);
+ if(playlists == null) {
+ playlists = new ArrayList<SyncSet>();
+
+ // Try to convert old style into new style
+ ArrayList<String> oldPlaylists = FileUtil.deserialize(context, syncFile, ArrayList.class);
+ // If exists, time to convert!
+ if(oldPlaylists != null) {
+ for(String id: oldPlaylists) {
+ playlists.add(new SyncSet(id));
+ }
+
+ FileUtil.serializeCompressed(context, playlists, syncFile);
+ }
+ }
+ return playlists;
+ }
+ public static void setSyncedPlaylists(Context context, int instance, ArrayList<SyncSet> playlists) {
+ FileUtil.serializeCompressed(context, playlists, getPlaylistSyncFile(context, instance));
+ }
+ public static void addSyncedPlaylist(Context context, String playlistId) {
+ String playlistFile = getPlaylistSyncFile(context);
+ ArrayList<SyncSet> playlists = getSyncedPlaylists(context);
+ SyncSet set = new SyncSet(playlistId);
+ if(!playlists.contains(set)) {
+ playlists.add(set);
+ }
+ FileUtil.serializeCompressed(context, playlists, playlistFile);
+ syncedPlaylists = playlists;
+ }
+ public static void removeSyncedPlaylist(Context context, String playlistId) {
+ int instance = Util.getActiveServer(context);
+ removeSyncedPlaylist(context, playlistId, instance);
+ }
+ public static void removeSyncedPlaylist(Context context, String playlistId, int instance) {
+ String playlistFile = getPlaylistSyncFile(context, instance);
+ ArrayList<SyncSet> playlists = getSyncedPlaylists(context, instance);
+ SyncSet set = new SyncSet(playlistId);
+ if(playlists.contains(set)) {
+ playlists.remove(set);
+ FileUtil.serializeCompressed(context, playlists, playlistFile);
+ syncedPlaylists = playlists;
+ }
+ }
+ public static String getPlaylistSyncFile(Context context) {
+ int instance = Util.getActiveServer(context);
+ return getPlaylistSyncFile(context, instance);
+ }
+ public static String getPlaylistSyncFile(Context context, int instance) {
+ return "sync-playlist-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ // Podcast sync
+ public static boolean isSyncedPodcast(Context context, String podcastId) {
+ checkRestURL(context);
+ if(syncedPodcasts == null) {
+ syncedPodcasts = getSyncedPodcasts(context);
+ }
+ return syncedPodcasts.contains(new SyncSet(podcastId));
+ }
+ public static ArrayList<SyncSet> getSyncedPodcasts(Context context) {
+ return getSyncedPodcasts(context, Util.getActiveServer(context));
+ }
+ public static ArrayList<SyncSet> getSyncedPodcasts(Context context, int instance) {
+ ArrayList<SyncSet> podcasts = FileUtil.deserialize(context, getPodcastSyncFile(context, instance), ArrayList.class);
+ if(podcasts == null) {
+ podcasts = new ArrayList<SyncSet>();
+ }
+ return podcasts;
+ }
+ public static void addSyncedPodcast(Context context, String podcastId, List<String> synced) {
+ String podcastFile = getPodcastSyncFile(context);
+ ArrayList<SyncSet> podcasts = getSyncedPodcasts(context);
+ SyncSet set = new SyncSet(podcastId, synced);
+ if(!podcasts.contains(set)) {
+ podcasts.add(set);
+ }
+ FileUtil.serialize(context, podcasts, podcastFile);
+ syncedPodcasts = podcasts;
+ }
+ public static void removeSyncedPodcast(Context context, String podcastId) {
+ removeSyncedPodcast(context, podcastId, Util.getActiveServer(context));
+ }
+ public static void removeSyncedPodcast(Context context, String podcastId, int instance) {
+ String podcastFile = getPodcastSyncFile(context, instance);
+ ArrayList<SyncSet> podcasts = getSyncedPodcasts(context, instance);
+ SyncSet set = new SyncSet(podcastId);
+ if(podcasts.contains(set)) {
+ podcasts.remove(set);
+ FileUtil.serialize(context, podcasts, podcastFile);
+ syncedPodcasts = podcasts;
+ }
+ }
+ public static String getPodcastSyncFile(Context context) {
+ int instance = Util.getActiveServer(context);
+ return getPodcastSyncFile(context, instance);
+ }
+ public static String getPodcastSyncFile(Context context, int instance) {
+ return "sync-podcast-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ // Starred
+ public static ArrayList<String> getSyncedStarred(Context context, int instance) {
+ ArrayList<String> list = FileUtil.deserializeCompressed(context, getStarredSyncFile(context, instance), ArrayList.class);
+ if(list == null) {
+ list = new ArrayList<String>();
+ }
+ return list;
+ }
+ public static void setSyncedStarred(ArrayList<String> syncedList, Context context, int instance) {
+ FileUtil.serializeCompressed(context, syncedList, SyncUtil.getStarredSyncFile(context, instance));
+ }
+ public static String getStarredSyncFile(Context context, int instance) {
+ return "sync-starred-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ // Most Recently Added
+ public static ArrayList<String> getSyncedMostRecent(Context context, int instance) {
+ ArrayList<String> list = FileUtil.deserialize(context, getMostRecentSyncFile(context, instance), ArrayList.class);
+ if(list == null) {
+ list = new ArrayList<String>();
+ }
+ return list;
+ }
+ public static void removeMostRecentSyncFiles(Context context) {
+ int total = Util.getServerCount(context);
+ for(int i = 0; i < total; i++) {
+ File file = new File(context.getCacheDir(), getMostRecentSyncFile(context, i));
+ file.delete();
+ }
+ }
+ public static String getMostRecentSyncFile(Context context, int instance) {
+ return "sync-most_recent-" + (Util.getRestUrl(context, null, instance, false)).hashCode() + ".ser";
+ }
+
+ public static String joinNames(List<String> names) {
+ StringBuilder builder = new StringBuilder();
+ for (String val : names) {
+ builder.append(val).append(", ");
+ }
+ builder.setLength(builder.length() - 2);
+ return builder.toString();
+ }
+
+ public static class SyncSet implements Serializable {
+ public String id;
+ public List<String> synced;
+
+ protected SyncSet() {
+
+ }
+ public SyncSet(String id) {
+ this.id = id;
+ }
+ public SyncSet(String id, List<String> synced) {
+ this.id = id;
+ this.synced = synced;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(obj instanceof SyncSet) {
+ return this.id.equals(((SyncSet)obj).id);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java b/app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java
new file mode 100644
index 00000000..759e893a
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/TabBackgroundTask.java
@@ -0,0 +1,51 @@
+package github.daneren2005.dsub.util;
+
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public abstract class TabBackgroundTask<T> extends BackgroundTask<T> {
+
+ private final SubsonicFragment tabFragment;
+
+ public TabBackgroundTask(SubsonicFragment fragment) {
+ super(fragment.getActivity());
+ tabFragment = fragment;
+ }
+
+ @Override
+ public void execute() {
+ tabFragment.setProgressVisible(true);
+
+ queue.offer(task = new Task() {
+ @Override
+ public void onDone(T result) {
+ tabFragment.setProgressVisible(false);
+ done(result);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ tabFragment.setProgressVisible(false);
+ error(t);
+ }
+ });
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return !tabFragment.isAdded() || cancelled.get();
+ }
+
+ @Override
+ public void updateProgress(final String message) {
+ getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ tabFragment.updateProgress(message);
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java b/app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java
new file mode 100644
index 00000000..8b7df783
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/TimeLimitedCache.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class TimeLimitedCache<T> {
+
+ private SoftReference<T> value;
+ private final long ttlMillis;
+ private long expires;
+
+ public TimeLimitedCache(long ttl, TimeUnit timeUnit) {
+ this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit);
+ }
+
+ public T get() {
+ return System.currentTimeMillis() < expires ? value.get() : null;
+ }
+
+ public void set(T value) {
+ set(value, ttlMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public void set(T value, long ttl, TimeUnit timeUnit) {
+ this.value = new SoftReference<T>(value);
+ expires = System.currentTimeMillis() + timeUnit.toMillis(ttl);
+ }
+
+ public void clear() {
+ expires = 0L;
+ value = null;
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
new file mode 100644
index 00000000..29618424
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/UserUtil.java
@@ -0,0 +1,452 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.User;
+import github.daneren2005.dsub.fragments.SubsonicFragment;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.MusicService;
+import github.daneren2005.dsub.service.MusicServiceFactory;
+import github.daneren2005.dsub.service.OfflineException;
+import github.daneren2005.dsub.service.ServerTooOldException;
+import github.daneren2005.dsub.adapter.SettingsAdapter;
+
+public final class UserUtil {
+ private static final String TAG = UserUtil.class.getSimpleName();
+ private static final long MIN_VERIFY_DURATION = 1000L * 60L * 60L;
+
+ private static int instance = -1;
+ private static User currentUser;
+ private static long lastVerifiedTime = 0;
+
+
+ public static void refreshCurrentUser(Context context, boolean forceRefresh) {
+ refreshCurrentUser(context, forceRefresh, false);
+ }
+ public static void refreshCurrentUser(Context context, boolean forceRefresh, boolean unAuth) {
+ currentUser = null;
+ if(unAuth) {
+ lastVerifiedTime = 0;
+ }
+ seedCurrentUser(context, forceRefresh);
+ }
+
+ public static void seedCurrentUser(Context context) {
+ seedCurrentUser(context, false);
+ }
+ public static void seedCurrentUser(final Context context, final boolean refresh) {
+ // Only try to seed if online
+ if(Util.isOffline(context)) {
+ currentUser = null;
+ return;
+ }
+
+ final int instance = Util.getActiveServer(context);
+ if(UserUtil.instance == instance && currentUser != null) {
+ return;
+ } else {
+ UserUtil.instance = instance;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ currentUser = MusicServiceFactory.getMusicService(context).getUser(refresh, getCurrentUsername(context, instance), context, null);
+
+ // If running, redo cast selector
+ DownloadService downloadService = DownloadService.getInstance();
+ if(downloadService != null) {
+ downloadService.userSettingsChanged();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void done(Void result) {
+ if(context instanceof ActionBarActivity) {
+ ((ActionBarActivity) context).supportInvalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ // Don't do anything, supposed to be background pull
+ Log.e(TAG, "Failed to seed user information");
+ }
+ }.execute();
+ }
+
+ public static User getCurrentUser() {
+ return currentUser;
+ }
+
+ public static String getCurrentUsername(Context context, int instance) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ }
+
+ public static String getCurrentUsername(Context context) {
+ return getCurrentUsername(context, Util.getActiveServer(context));
+ }
+
+ public static boolean isCurrentAdmin() {
+ return isCurrentRole(User.ADMIN);
+ }
+
+ public static boolean canPodcast() {
+ return isCurrentRole(User.PODCAST);
+ }
+ public static boolean canShare() {
+ return isCurrentRole(User.SHARE);
+ }
+ public static boolean canJukebox() {
+ return isCurrentRole(User.JUKEBOX);
+ }
+ public static boolean canScrobble() {
+ return isCurrentRole(User.SCROBBLING, true);
+ }
+
+ public static boolean isCurrentRole(String role) {
+ return isCurrentRole(role, false);
+ }
+ public static boolean isCurrentRole(String role, boolean defaultValue) {
+ if(currentUser == null) {
+ return defaultValue;
+ }
+
+ for(User.Setting setting: currentUser.getSettings()) {
+ if(setting.getName().equals(role)) {
+ return setting.getValue() == true;
+ }
+ }
+
+ return defaultValue;
+ }
+
+ public static void confirmCredentials(final Activity context, final Runnable onSuccess) {
+ final long currentTime = System.currentTimeMillis();
+ // If already ran this check within last x time, just go ahead and auth
+ if((currentTime - lastVerifiedTime) < MIN_VERIFY_DURATION) {
+ onSuccess.run();
+ } else {
+ View layout = context.getLayoutInflater().inflate(R.layout.confirm_password, null);
+ final TextView passwordView = (TextView) layout.findViewById(R.id.password);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.admin_confirm_password)
+ .setView(layout)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ String password = passwordView.getText().toString();
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ String correctPassword = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + Util.getActiveServer(context), null);
+
+ if(password != null && password.equals(correctPassword)) {
+ lastVerifiedTime = currentTime;
+ onSuccess.run();
+ } else {
+ Util.toast(context, R.string.admin_confirm_password_bad);
+ }
+ }
+ })
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+ }
+
+ public static void changePassword(final Activity context, final User user) {
+ View layout = context.getLayoutInflater().inflate(R.layout.change_password, null);
+ final TextView passwordView = (TextView) layout.findViewById(R.id.new_password);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.admin_change_password)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, null)
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String password = passwordView.getText().toString();
+ // Don't allow blank passwords
+ if ("".equals(password)) {
+ Util.toast(context, R.string.admin_change_password_invalid);
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.changePassword(user.getUsername(), password, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ Util.toast(context, context.getResources().getString(R.string.admin_change_password_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_change_password_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+
+ dialog.dismiss();
+ }
+ });
+ }
+
+ public static void updateSettings(final Context context, final User user) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.updateUser(user, context, null);
+ user.setSettings(user.getSettings());
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ Util.toast(context, context.getResources().getString(R.string.admin_update_permissions_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_update_permissions_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+
+ public static void changeEmail(final Activity context, final User user) {
+ View layout = context.getLayoutInflater().inflate(R.layout.change_email, null);
+ final TextView emailView = (TextView) layout.findViewById(R.id.new_email);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.admin_change_email)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, null)
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String email = emailView.getText().toString();
+ // Don't allow blank emails
+ if ("".equals(email)) {
+ Util.toast(context, R.string.admin_change_email_invalid);
+ return;
+ }
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.changeEmail(user.getUsername(), email, context, null);
+ user.setEmail(email);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ Util.toast(context, context.getResources().getString(R.string.admin_change_email_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_change_email_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+
+ dialog.dismiss();
+ }
+ });
+ }
+
+ public static void deleteUser(final Context context, final User user, final ArrayAdapter adapter) {
+ Util.confirmDialog(context, R.string.common_delete, user.getUsername(), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.deleteUser(user.getUsername(), context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ if(adapter != null) {
+ adapter.remove(user);
+ adapter.notifyDataSetChanged();
+ }
+
+ Util.toast(context, context.getResources().getString(R.string.admin_delete_user_success, user.getUsername()));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_delete_user_error, user.getUsername());
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+ }
+ });
+ }
+
+ public static void addNewUser(final Activity context, final SubsonicFragment fragment) {
+ final User user = new User();
+ for(String role: User.ROLES) {
+ if(role.equals(User.SETTINGS) || role.equals(User.STREAM)) {
+ user.addSetting(role, true);
+ } else {
+ user.addSetting(role, false);
+ }
+ }
+
+ View layout = context.getLayoutInflater().inflate(R.layout.create_user, null);
+ final TextView usernameView = (TextView) layout.findViewById(R.id.username);
+ final TextView emailView = (TextView) layout.findViewById(R.id.email);
+ final TextView passwordView = (TextView) layout.findViewById(R.id.password);
+ final ListView listView = (ListView) layout.findViewById(R.id.settings_list);
+ listView.setAdapter(new SettingsAdapter(context, user, true));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.menu_add_user)
+ .setView(layout)
+ .setPositiveButton(R.string.common_save, null)
+ .setNegativeButton(R.string.common_cancel, null)
+ .setCancelable(true);
+
+ final AlertDialog dialog = builder.create();
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String username = usernameView.getText().toString();
+ // Don't allow blank emails
+ if ("".equals(username)) {
+ Util.toast(context, R.string.admin_change_username_invalid);
+ return;
+ }
+
+ final String email = emailView.getText().toString();
+ // Don't allow blank emails
+ if ("".equals(email)) {
+ Util.toast(context, R.string.admin_change_email_invalid);
+ return;
+ }
+
+ final String password = passwordView.getText().toString();
+ if ("".equals(password)) {
+ Util.toast(context, R.string.admin_change_password_invalid);
+ return;
+ }
+
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setPassword(password);
+
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ MusicService musicService = MusicServiceFactory.getMusicService(context);
+ musicService.createUser(user, context, null);
+ return null;
+ }
+
+ @Override
+ protected void done(Void v) {
+ fragment.onRefresh();
+ Util.toast(context, context.getResources().getString(R.string.admin_create_user_success));
+ }
+
+ @Override
+ protected void error(Throwable error) {
+ String msg;
+ if (error instanceof OfflineException || error instanceof ServerTooOldException) {
+ msg = getErrorMessage(error);
+ } else {
+ msg = context.getResources().getString(R.string.admin_create_user_error);
+ }
+
+ Util.toast(context, msg);
+ }
+ }.execute();
+
+ dialog.dismiss();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/Util.java b/app/src/main/java/github/daneren2005/dsub/util/Util.java
new file mode 100644
index 00000000..75d8d5dd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/Util.java
@@ -0,0 +1,1339 @@
+/*
+ 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
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package github.daneren2005.dsub.util;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Environment;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.Gravity;
+import android.widget.TextView;
+import android.widget.Toast;
+import github.daneren2005.dsub.R;
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.domain.PlayerState;
+import github.daneren2005.dsub.domain.RepeatMode;
+import github.daneren2005.dsub.receiver.MediaButtonIntentReceiver;
+import github.daneren2005.dsub.service.DownloadService;
+
+import org.apache.http.HttpEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+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.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @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;
+ private static SimpleDateFormat DATE_FORMAT_SHORT = new SimpleDateFormat("MMM d h:mm a");
+ private static SimpleDateFormat DATE_FORMAT_LONG = new SimpleDateFormat("MMM d, yyyy h:mm a");
+ private static int CURRENT_YEAR = new Date().getYear();
+
+ public static final String EVENT_META_CHANGED = "github.daneren2005.dsub.EVENT_META_CHANGED";
+ public static final String EVENT_PLAYSTATE_CHANGED = "github.daneren2005.dsub.EVENT_PLAYSTATE_CHANGED";
+
+ public static final String AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
+ public static final String AVRCP_METADATA_CHANGED = "com.android.music.metachanged";
+
+ private static OnAudioFocusChangeListener focusListener;
+ private static boolean pauseFocus = false;
+ private static boolean lowerFocus = false;
+
+ // 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 static Toast toast;
+
+ private Util() {
+ }
+
+ public static boolean isOffline(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false);
+ }
+
+ public static void setOffline(Context context, boolean offline) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_OFFLINE, offline);
+ editor.commit();
+ }
+
+ public static boolean isScreenLitOnDownload(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false);
+ }
+
+ public static RepeatMode getRepeatMode(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name()));
+ }
+
+ public static void setRepeatMode(Context context, RepeatMode repeatMode) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name());
+ editor.commit();
+ }
+
+ public static boolean isScrobblingEnabled(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, true) && (isOffline(context) || UserUtil.canScrobble());
+ }
+
+ 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.getBoolean(Constants.PREFERENCES_KEY_OFFLINE, false) ? 0 : prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ }
+
+ public static int getServerCount(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_COUNT, 1);
+ }
+
+ public static void removeInstanceName(Context context, int instance, int activeInstance) {
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+
+ int newInstance = instance + 1;
+
+ // Get what the +1 server details are
+ String server = prefs.getString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null);
+ String serverName = prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
+ String userName = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
+ String musicFolderId = prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null);
+
+ // Store the +1 server details in the to be deleted instance
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + instance, server);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, serverName);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + instance, serverUrl);
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + instance, userName);
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + instance, password);
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId);
+
+ // Delete the +1 server instance
+ // Calling method will loop up to fill this in if +2 server exists
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_KEY + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_NAME + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_SERVER_URL + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_USERNAME + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_PASSWORD + newInstance, null);
+ editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + newInstance, null);
+ editor.commit();
+
+ if (instance == activeInstance) {
+ if(instance != 1) {
+ Util.setActiveServer(context, 1);
+ } else {
+ Util.setOffline(context, true);
+ }
+ } else if (newInstance == activeInstance) {
+ Util.setActiveServer(context, instance);
+ }
+ }
+
+ public static String getServerName(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+ public static String getServerName(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null);
+ }
+
+ public static 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) {
+ return getSelectedMusicFolderId(context, getActiveServer(context));
+ }
+ public static String getSelectedMusicFolderId(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null);
+ }
+
+ public static boolean getAlbumListsPerFolder(Context context) {
+ return getAlbumListsPerFolder(context, getActiveServer(context));
+ }
+ public static boolean getAlbumListsPerFolder(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, false);
+ }
+ public static void setAlbumListsPerFolder(Context context, boolean perFolder) {
+ int instance = getActiveServer(context);
+ SharedPreferences prefs = getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(Constants.PREFERENCES_KEY_ALBUMS_PER_FOLDER + instance, perFolder);
+ editor.commit();
+ }
+
+ public static String getTheme(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_THEME, null);
+ }
+ public static void setTheme(Context context, String theme) {
+ SharedPreferences.Editor editor = getPreferences(context).edit();
+ editor.putString(Constants.PREFERENCES_KEY_THEME, theme);
+ editor.commit();
+ }
+
+ public static void applyTheme(Context context, String theme) {
+ if ("dark".equals(theme)) {
+ context.setTheme(R.style.Theme_DSub_Dark);
+ } else if ("black".equals(theme)) {
+ context.setTheme(R.style.Theme_DSub_Black);
+ } else if ("holo".equals(theme)) {
+ context.setTheme(R.style.Theme_DSub_Holo);
+ } else {
+ context.setTheme(R.style.Theme_DSub_Light);
+ }
+
+ SharedPreferences prefs = Util.getPreferences(context);
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_OVERRIDE_SYSTEM_LANGUAGE, false)) {
+ Configuration config = new Configuration();
+ config.locale = Locale.ENGLISH;
+ context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
+ }
+ }
+
+ public static boolean getDisplayTrack(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_DISPLAY_TRACK, false);
+ }
+
+ public static int getMaxBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getMaxVideoBitrate(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 0;
+ }
+
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ SharedPreferences prefs = getPreferences(context);
+ return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_VIDEO_BITRATE_MOBILE, "0"));
+ }
+
+ public static int getPreloadCount(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return 3;
+ }
+
+ SharedPreferences prefs = getPreferences(context);
+ boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ int preloadCount = Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_PRELOAD_COUNT_WIFI : Constants.PREFERENCES_KEY_PRELOAD_COUNT_MOBILE, "-1"));
+ return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount;
+ }
+
+ public static int getCacheSizeMB(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1"));
+ return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize;
+ }
+
+ public static String getRestUrl(Context context, String method) {
+ return getRestUrl(context, method, true);
+ }
+ public static String getRestUrl(Context context, String method, boolean allowAltAddress) {
+ SharedPreferences prefs = getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ return getRestUrl(context, method, prefs, instance, allowAltAddress);
+ }
+ public static String getRestUrl(Context context, String method, int instance) {
+ return getRestUrl(context, method, instance, true);
+ }
+ public static String getRestUrl(Context context, String method, int instance, boolean allowAltAddress) {
+ SharedPreferences prefs = getPreferences(context);
+ return getRestUrl(context, method, prefs, instance, allowAltAddress);
+ }
+ public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance) {
+ return getRestUrl(context, method, prefs, instance, true);
+ }
+ public static String getRestUrl(Context context, String method, SharedPreferences prefs, int instance, boolean allowAltAddress) {
+ StringBuilder builder = new StringBuilder();
+
+ String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ if(allowAltAddress && Util.isWifiConnected(context)) {
+ String SSID = prefs.getString(Constants.PREFERENCES_KEY_SERVER_LOCAL_NETWORK_SSID + instance, "");
+ String currentSSID = Util.getSSID(context);
+
+ String[] ssidParts = SSID.split(",");
+ if("".equals(SSID) || SSID.equals(currentSSID) || Arrays.asList(ssidParts).contains(currentSSID)) {
+ String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
+ if(internalUrl != null && !"".equals(internalUrl) && !"http://".equals(internalUrl)) {
+ serverUrl = internalUrl;
+ }
+ }
+ }
+
+ String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
+ String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
+
+ // Slightly obfuscate password
+ password = "enc:" + Util.utf8HexEncode(password);
+
+ builder.append(serverUrl);
+ if (builder.charAt(builder.length() - 1) != '/') {
+ builder.append("/");
+ }
+ builder.append("rest/").append(method).append(".view");
+ builder.append("?u=").append(username);
+ builder.append("&p=").append(password);
+ builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION);
+ builder.append("&c=").append(Constants.REST_CLIENT_ID);
+
+ return builder.toString();
+ }
+
+ public static String replaceInternalUrl(Context context, String url) {
+ // Only change to internal when using https
+ if(url.indexOf("https") != -1) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
+ String internalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_INTERNAL_URL + instance, null);
+ if(internalUrl != null && !"".equals(internalUrl)) {
+ String externalUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
+ url = url.replace(internalUrl, externalUrl);
+ }
+ }
+
+ // Use separate profile for Chromecast so users can do ogg on phone, mp3 for CC
+ return url.replace("c=" + Constants.REST_CLIENT_ID, "c=" + Constants.CHROMECAST_CLIENT_ID);
+ }
+
+ public static boolean isTagBrowsing(Context context) {
+ return isTagBrowsing(context, Util.getActiveServer(context));
+ }
+ public static boolean isTagBrowsing(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_BROWSE_TAGS + instance, false);
+ }
+
+ public static boolean isSyncEnabled(Context context, int instance) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_SERVER_SYNC + instance, true);
+ }
+
+ public static String getParentFromEntry(Context context, MusicDirectory.Entry entry) {
+ if(Util.isTagBrowsing(context)) {
+ if(!entry.isDirectory()) {
+ return entry.getAlbumId();
+ } else if(entry.isAlbum()) {
+ return entry.getArtistId();
+ } else {
+ return null;
+ }
+ } else {
+ return entry.getParent();
+ }
+ }
+
+ public static String openToTab(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_OPEN_TO_TAB, null);
+ }
+
+ public static boolean disableExitPrompt(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_DISABLE_EXIT_PROMPT, false);
+ }
+
+ public static String getVideoPlayerType(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getString(Constants.PREFERENCES_KEY_VIDEO_PLAYER, "raw");
+ }
+
+ public static SharedPreferences getPreferences(Context context) {
+ return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0);
+ }
+ public static SharedPreferences getOfflineSync(Context context) {
+ return context.getSharedPreferences(Constants.OFFLINE_SYNC_NAME, 0);
+ }
+
+ public static String getSyncDefault(Context context) {
+ SharedPreferences prefs = Util.getOfflineSync(context);
+ return prefs.getString(Constants.OFFLINE_SYNC_DEFAULT, null);
+ }
+ public static void setSyncDefault(Context context, String defaultValue) {
+ SharedPreferences.Editor editor = Util.getOfflineSync(context).edit();
+ editor.putString(Constants.OFFLINE_SYNC_DEFAULT, defaultValue);
+ editor.commit();
+ }
+
+ public static String getCacheName(Context context, String name, String id) {
+ return getCacheName(context, getActiveServer(context), name, id);
+ }
+ public static String getCacheName(Context context, int instance, String name, String id) {
+ String s = getRestUrl(context, null, instance, false) + id;
+ return name + "-" + s.hashCode() + ".ser";
+ }
+ public static String getCacheName(Context context, String name) {
+ return getCacheName(context, getActiveServer(context), name);
+ }
+ public static String getCacheName(Context context, int instance, String name) {
+ String s = getRestUrl(context, null, instance, false);
+ return name + "-" + s.hashCode() + ".ser";
+ }
+
+ public static int offlineScrobblesCount(Context context) {
+ SharedPreferences offline = getOfflineSync(context);
+ return offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0);
+ }
+ public static int offlineStarsCount(Context context) {
+ SharedPreferences offline = getOfflineSync(context);
+ return offline.getInt(Constants.OFFLINE_STAR_COUNT, 0);
+ }
+
+ public static String parseOfflineIDSearch(Context context, String id, String cacheLocation) {
+ // Try to get this info based off of tags first
+ String name = parseOfflineIDSearch(id);
+ if(name != null) {
+ return name;
+ }
+
+ // Otherwise go nuts trying to parse from file structure
+ name = id.replace(cacheLocation, "");
+ if(name.startsWith("/")) {
+ name = name.substring(1);
+ }
+ name = name.replace(".complete", "").replace(".partial", "");
+ int index = name.lastIndexOf(".");
+ name = index == -1 ? name : name.substring(0, index);
+ String[] details = name.split("/");
+
+ String title = details[details.length - 1];
+ if(index == -1) {
+ if(details.length > 1) {
+ String artist = "artist:\"" + details[details.length - 2] + "\"";
+ String simpleArtist = "artist:\"" + title + "\"";
+ title = "album:\"" + title + "\"";
+ if(details[details.length - 1].equals(details[details.length - 2])) {
+ name = title;
+ } else {
+ name = "(" + artist + " AND " + title + ")" + " OR " + simpleArtist;
+ }
+ } else {
+ name = "artist:\"" + title + "\" OR album:\"" + title + "\"";
+ }
+ } else {
+ String artist;
+ if(details.length > 2) {
+ artist = "artist:\"" + details[details.length - 3] + "\"";
+ } else {
+ artist = "(artist:\"" + details[0] + "\" OR album:\"" + details[0] + "\")";
+ }
+ title = "title:\"" + title.substring(title.indexOf('-') + 1) + "\"";
+ name = artist + " AND " + title;
+ }
+
+ return name;
+ }
+
+ public static String parseOfflineIDSearch(String id) {
+ MusicDirectory.Entry entry = new MusicDirectory.Entry();
+ File file = new File(id);
+
+ if(file.exists()) {
+ entry.loadMetadata(file);
+
+ if(entry.getArtist() != null) {
+ String title = file.getName();
+ title = title.replace(".complete", "").replace(".partial", "");
+ int index = title.lastIndexOf(".");
+ title = index == -1 ? title : title.substring(0, index);
+ title = title.substring(title.indexOf('-') + 1);
+
+ String query = "artist:\"" + entry.getArtist() + "\"" +
+ " AND title:\"" + title + "\"";
+
+ return query;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ 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);
+ }
+
+ public static boolean isCastProxy(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_CAST_PROXY, false);
+ }
+
+ public static boolean isFirstLevelArtist(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true);
+ }
+ public static void toggleFirstLevelArtist(Context context) {
+ SharedPreferences prefs = Util.getPreferences(context);
+ SharedPreferences.Editor editor = prefs.edit();
+
+ if(prefs.getBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true)) {
+ editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), false);
+ } else {
+ editor.putBoolean(Constants.PREFERENCES_KEY_FIRST_LEVEL_ARTIST + getActiveServer(context), true);
+ }
+
+ editor.commit();
+ }
+
+ public static boolean shouldStartOnHeadphones(Context context) {
+ SharedPreferences prefs = getPreferences(context);
+ return prefs.getBoolean(Constants.PREFERENCES_KEY_START_ON_HEADPHONES, false);
+ }
+
+ /**
+ * 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 renameFile(File from, File to) throws IOException {
+ if(!from.renameTo(to)) {
+ Log.i(TAG, "Failed to rename " + from + " to " + to);
+ }
+ }
+
+ public static void close(Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (Throwable x) {
+ // Ignored
+ }
+ }
+
+ public static boolean delete(File file) {
+ if (file != null && file.exists()) {
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete file " + file);
+ return false;
+ }
+ Log.i(TAG, "Deleted file " + file);
+ }
+ return true;
+ }
+
+ public static void toast(Context context, int messageId) {
+ toast(context, messageId, true);
+ }
+
+ public static void toast(Context context, int messageId, boolean shortDuration) {
+ toast(context, context.getString(messageId), shortDuration);
+ }
+
+ public static void toast(Context context, String message) {
+ toast(context, message, true);
+ }
+
+ public static void toast(Context context, String message, boolean shortDuration) {
+ if (toast == null) {
+ toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ } else {
+ toast.setText(message);
+ toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
+ }
+ toast.show();
+ }
+
+ public static void confirmDialog(Context context, int action, int subject, DialogInterface.OnClickListener onClick) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), context.getResources().getString(subject), onClick, null);
+ }
+ public static void confirmDialog(Context context, int action, int subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), context.getResources().getString(subject), onClick, onCancel);
+ }
+ public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick, null);
+ }
+ public static void confirmDialog(Context context, int action, String subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) {
+ Util.confirmDialog(context, context.getResources().getString(action).toLowerCase(), subject, onClick, onCancel);
+ }
+ public static void confirmDialog(Context context, String action, String subject, DialogInterface.OnClickListener onClick, DialogInterface.OnClickListener onCancel) {
+ new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setTitle(R.string.common_confirm)
+ .setMessage(context.getResources().getString(R.string.common_confirm_message, action, subject))
+ .setPositiveButton(R.string.common_ok, onClick)
+ .setNegativeButton(R.string.common_cancel, onCancel)
+ .show();
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * To get a localized string, please use formatLocalizedBytes instead.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatBytes(long byteCount) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT;
+ return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ NumberFormat megaByteFormat = MEGA_BYTE_FORMAT;
+ return megaByteFormat.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ NumberFormat kiloByteFormat = KILO_BYTE_FORMAT;
+ return kiloByteFormat.format((double) byteCount / 1024);
+ }
+
+ return byteCount + " B";
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ * This version of the method returns a localized string.
+ *
+ * @param byteCount The number of bytes.
+ * @return The formatted string.
+ */
+ public static synchronized String formatLocalizedBytes(long byteCount, Context context) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
+ GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte));
+ }
+
+ return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
+ MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte));
+ }
+
+ return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ if (KILO_BYTE_LOCALIZED_FORMAT == null) {
+ KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte));
+ }
+
+ return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024);
+ }
+
+ if (BYTE_LOCALIZED_FORMAT == null) {
+ BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte));
+ }
+
+ return BYTE_LOCALIZED_FORMAT.format((double) byteCount);
+ }
+
+ public static String formatDuration(Integer seconds) {
+ if (seconds == null) {
+ return null;
+ }
+
+ int hours = seconds / 3600;
+ int minutes = (seconds / 60) % 60;
+ int secs = seconds % 60;
+
+ StringBuilder builder = new StringBuilder(7);
+ if(hours > 0) {
+ builder.append(hours).append(":");
+ if(minutes < 10) {
+ builder.append("0");
+ }
+ }
+ builder.append(minutes).append(":");
+ if (secs < 10) {
+ builder.append("0");
+ }
+ builder.append(secs);
+ return builder.toString();
+ }
+
+ public static String formatDate(Date date) {
+ if(date == null) {
+ return "Never";
+ } else {
+ if(date.getYear() != CURRENT_YEAR) {
+ return DATE_FORMAT_LONG.format(date);
+ } else {
+ return DATE_FORMAT_SHORT.format(date);
+ }
+ }
+ }
+
+ public static boolean equals(Object object1, Object object2) {
+ if (object1 == object2) {
+ return true;
+ }
+ if (object1 == null || object2 == null) {
+ return false;
+ }
+ return object1.equals(object2);
+
+ }
+
+ /**
+ * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to encode.
+ * @return The encoded string.
+ */
+ public static String utf8HexEncode(String s) {
+ if (s == null) {
+ return null;
+ }
+ byte[] utf8;
+ try {
+ utf8 = s.getBytes(Constants.UTF_8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ return hexEncode(utf8);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+ * The returned array will be double the length of the passed array, as it takes two characters to represent any
+ * given byte.
+ *
+ * @param data Bytes to convert to hexadecimal characters.
+ * @return A string containing hexadecimal characters.
+ */
+ public static String hexEncode(byte[] data) {
+ int length = data.length;
+ char[] out = new char[length << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < length; i++) {
+ out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4];
+ out[j++] = HEX_DIGITS[0x0F & data[i]];
+ }
+ return new String(out);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param s Data to digest.
+ * @return MD5 digest as a hex string.
+ */
+ public static String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return hexEncode(md5.digest(s.getBytes(Constants.UTF_8)));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ public static boolean isNullOrWhiteSpace(String string) {
+ return string == null || "".equals(string) || "".equals(string.trim());
+ }
+
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+
+ // Calculate ratios of height and width to requested height and
+ // width
+ final int heightRatio = Math.round((float) height / (float) reqHeight);
+ final int widthRatio = Math.round((float) width / (float) reqWidth);
+
+ // Choose the smallest ratio as inSampleSize value, this will
+ // guarantee
+ // a final image with both dimensions larger than or equal to the
+ // requested height and width.
+ inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
+ }
+
+ return inSampleSize;
+ }
+
+ public static int getScaledHeight(double height, double width, int newWidth) {
+ // Try to keep correct aspect ratio of the original image, do not force a square
+ double aspectRatio = height / width;
+
+ // Assume the size given refers to the width of the image, so calculate the new height using
+ // the previously determined aspect ratio
+ return (int) Math.round(newWidth * aspectRatio);
+ }
+
+ public static int getScaledHeight(Bitmap bitmap, int width) {
+ return Util.getScaledHeight((double) bitmap.getHeight(), (double) bitmap.getWidth(), width);
+ }
+
+ public static int getStringDistance(CharSequence s, CharSequence t) {
+ if (s == null || t == null) {
+ throw new IllegalArgumentException("Strings must not be null");
+ }
+
+ if(t.toString().toLowerCase().indexOf(s.toString().toLowerCase()) != -1) {
+ return 1;
+ }
+
+ int n = s.length();
+ int m = t.length();
+
+ if (n == 0) {
+ return m;
+ } else if (m == 0) {
+ return n;
+ }
+
+ if (n > m) {
+ final CharSequence tmp = s;
+ s = t;
+ t = tmp;
+ n = m;
+ m = t.length();
+ }
+
+ int p[] = new int[n + 1];
+ int d[] = new int[n + 1];
+ int _d[];
+
+ int i;
+ int j;
+ char t_j;
+ int cost;
+
+ for (i = 0; i <= n; i++) {
+ p[i] = i;
+ }
+
+ for (j = 1; j <= m; j++) {
+ t_j = t.charAt(j - 1);
+ d[0] = j;
+
+ for (i = 1; i <= n; i++) {
+ cost = s.charAt(i - 1) == t_j ? 0 : 1;
+ d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
+ }
+
+ _d = p;
+ p = d;
+ d = _d;
+ }
+
+ return p[n];
+ }
+
+ public static boolean isNetworkConnected(Context context) {
+ return isNetworkConnected(context, false);
+ }
+ public static boolean isNetworkConnected(Context context, boolean streaming) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+
+ if(streaming) {
+ boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI;
+ boolean wifiRequired = isWifiRequiredForDownload(context);
+
+ return connected && (!wifiRequired || wifiConnected);
+ } else {
+ return connected;
+ }
+ }
+ public static boolean isWifiConnected(Context context) {
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = manager.getActiveNetworkInfo();
+ boolean connected = networkInfo != null && networkInfo.isConnected();
+ return connected && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
+ }
+ public static String getSSID(Context context) {
+ if (isWifiConnected(context)) {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager.getConnectionInfo() != null && wifiManager.getConnectionInfo().getSSID() != null) {
+ return wifiManager.getConnectionInfo().getSSID().replace("\"", "");
+ }
+ return null;
+ }
+ return null;
+ }
+
+ 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) {
+ info(context, titleId, messageId, true);
+ }
+ public static void info(Context context, int titleId, String message) {
+ info(context, titleId, message, true);
+ }
+ public static void info(Context context, String title, String message) {
+ info(context, title, message, true);
+ }
+ public static void info(Context context, int titleId, int messageId, boolean linkify) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId, linkify);
+ }
+ public static void info(Context context, int titleId, String message, boolean linkify) {
+ showDialog(context, android.R.drawable.ic_dialog_info, titleId, message, linkify);
+ }
+ public static void info(Context context, String title, String message, boolean linkify) {
+ showDialog(context, android.R.drawable.ic_dialog_info, title, message, linkify);
+ }
+
+ private static void showDialog(Context context, int icon, int titleId, int messageId) {
+ showDialog(context, icon, titleId, messageId, true);
+ }
+ private static void showDialog(Context context, int icon, int titleId, String message) {
+ showDialog(context, icon, titleId, message, true);
+ }
+ private static void showDialog(Context context, int icon, String title, String message) {
+ showDialog(context, icon, title, message, true);
+ }
+ private static void showDialog(Context context, int icon, int titleId, int messageId, boolean linkify) {
+ showDialog(context, icon, context.getResources().getString(titleId), context.getResources().getString(messageId), linkify);
+ }
+ private static void showDialog(Context context, int icon, int titleId, String message, boolean linkify) {
+ showDialog(context, icon, context.getResources().getString(titleId), message, linkify);
+ }
+ private static void showDialog(Context context, int icon, String title, String message, boolean linkify) {
+ SpannableString ss = new SpannableString(message);
+ if(linkify) {
+ Linkify.addLinks(ss, Linkify.ALL);
+ }
+
+ AlertDialog dialog = new AlertDialog.Builder(context)
+ .setIcon(icon)
+ .setTitle(title)
+ .setMessage(ss)
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ public static void showHTMLDialog(Context context, int title, int message) {
+ showHTMLDialog(context, title, context.getResources().getString(message));
+ }
+ public static void showHTMLDialog(Context context, int title, String message) {
+ AlertDialog dialog = new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(title)
+ .setMessage(Html.fromHtml(message))
+ .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int i) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ ((TextView)dialog.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+ public static void 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 int getAttribute(Context context, int attr) {
+ int res;
+ int[] attrs = new int[] {attr};
+ TypedArray typedArray = context.obtainStyledAttributes(attrs);
+ res = typedArray.getResourceId(0, 0);
+ typedArray.recycle();
+ return res;
+ }
+
+ public static void registerMediaButtonEventReceiver(Context context) {
+
+ // Only do it if enabled in the settings.
+ SharedPreferences prefs = getPreferences(context);
+ boolean enabled = prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true);
+
+ if (enabled) {
+
+ // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+ }
+
+ public static void unregisterMediaButtonEventReceiver(Context context) {
+ // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2.
+ // Use reflection to maintain compatibility with 1.5.
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName());
+ Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class);
+ method.invoke(audioManager, componentName);
+ } catch (Throwable x) {
+ // Ignored.
+ }
+ }
+
+ @TargetApi(8)
+ public static void requestAudioFocus(final Context context) {
+ if (Build.VERSION.SDK_INT >= 8 && focusListener == null) {
+ final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.requestAudioFocus(focusListener = new OnAudioFocusChangeListener() {
+ public void onAudioFocusChange(int focusChange) {
+ DownloadService downloadService = (DownloadService)context;
+ if((focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) && !downloadService.isRemoteEnabled()) {
+ if(downloadService.getPlayerState() == PlayerState.STARTED) {
+ Log.i(TAG, "Temporary loss of focus");
+ SharedPreferences prefs = getPreferences(context);
+ int lossPref = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_TEMP_LOSS, "1"));
+ if(lossPref == 2 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)) {
+ lowerFocus = true;
+ downloadService.setVolume(0.1f);
+ } else if(lossPref == 0 || (lossPref == 1 && focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)) {
+ pauseFocus = true;
+ downloadService.pause(true);
+ }
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ if(pauseFocus) {
+ pauseFocus = false;
+ downloadService.start();
+ } else if(lowerFocus) {
+ lowerFocus = false;
+ downloadService.setVolume(1.0f);
+ }
+ } else if(focusChange == AudioManager.AUDIOFOCUS_LOSS && !downloadService.isRemoteEnabled()) {
+ Log.i(TAG, "Permanently lost focus");
+ focusListener = null;
+ downloadService.pause();
+ audioManager.abandonAudioFocus(this);
+ }
+ }
+ }, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
+ }
+
+ public static void abandonAudioFocus(Context context) {
+ if(focusListener != null) {
+ final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.abandonAudioFocus(focusListener);
+ focusListener = null;
+ }
+ }
+
+ /**
+ * <p>Broadcasts the given song info as the new song being played.</p>
+ */
+ public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) {
+ DownloadService downloadService = (DownloadService)context;
+ Intent intent = new Intent(EVENT_META_CHANGED);
+ Intent avrcpIntent = new Intent(AVRCP_METADATA_CHANGED);
+
+ if (song != null) {
+ intent.putExtra("title", song.getTitle());
+ intent.putExtra("artist", song.getArtist());
+ intent.putExtra("album", song.getAlbum());
+
+ File albumArtFile = FileUtil.getAlbumArtFile(context, song);
+ intent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ avrcpIntent.putExtra("playing", true);
+ } else {
+ intent.putExtra("title", "");
+ intent.putExtra("artist", "");
+ intent.putExtra("album", "");
+ intent.putExtra("coverart", "");
+ avrcpIntent.putExtra("playing", false);
+ }
+ addTrackInfo(context, song, avrcpIntent);
+
+ context.sendBroadcast(intent);
+ context.sendBroadcast(avrcpIntent);
+ }
+
+ /**
+ * <p>Broadcasts the given player state as the one being set.</p>
+ */
+ public static void broadcastPlaybackStatusChange(Context context, MusicDirectory.Entry song, PlayerState state) {
+ Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED);
+ Intent avrcpIntent = new Intent(AVRCP_PLAYSTATE_CHANGED);
+
+ switch (state) {
+ case STARTED:
+ intent.putExtra("state", "play");
+ avrcpIntent.putExtra("playing", true);
+ break;
+ case STOPPED:
+ intent.putExtra("state", "stop");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PAUSED:
+ intent.putExtra("state", "pause");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ case PREPARED:
+ // Only send quick pause event for samsung devices, causes issues for others
+ if(Build.MANUFACTURER.toLowerCase().indexOf("samsung") != -1) {
+ avrcpIntent.putExtra("playing", false);
+ } else {
+ return; // Don't broadcast anything
+ }
+ break;
+ case COMPLETED:
+ intent.putExtra("state", "complete");
+ avrcpIntent.putExtra("playing", false);
+ break;
+ default:
+ return; // No need to broadcast.
+ }
+ addTrackInfo(context, song, avrcpIntent);
+
+ if(state != PlayerState.PREPARED) {
+ context.sendBroadcast(intent);
+ }
+ context.sendBroadcast(avrcpIntent);
+ }
+
+ private static void addTrackInfo(Context context, MusicDirectory.Entry song, Intent intent) {
+ if (song != null) {
+ DownloadService downloadService = (DownloadService)context;
+ File albumArtFile = FileUtil.getAlbumArtFile(context, song);
+
+ intent.putExtra("track", song.getTitle());
+ intent.putExtra("artist", song.getArtist());
+ intent.putExtra("album", song.getAlbum());
+ intent.putExtra("ListSize", (long) downloadService.getSongs().size());
+ intent.putExtra("id", (long) downloadService.getCurrentPlayingIndex() + 1);
+ intent.putExtra("duration", (long) downloadService.getPlayerDuration());
+ intent.putExtra("position", (long) downloadService.getPlayerPosition());
+ intent.putExtra("coverart", albumArtFile.getAbsolutePath());
+ } else {
+ intent.putExtra("track", "");
+ intent.putExtra("artist", "");
+ intent.putExtra("album", "");
+ intent.putExtra("ListSize", (long) 0);
+ intent.putExtra("id", (long) 0);
+ intent.putExtra("duration", (long) 0);
+ intent.putExtra("position", (long) 0);
+ intent.putExtra("coverart", "");
+ }
+ }
+
+ public static WifiManager.WifiLock createWifiLock(Context context, String tag) {
+ WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ int lockType = WifiManager.WIFI_MODE_FULL;
+ if (Build.VERSION.SDK_INT >= 12) {
+ lockType = 3;
+ }
+ return wm.createWifiLock(lockType, tag);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
new file mode 100644
index 00000000..ab64bca9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java
@@ -0,0 +1,57 @@
+/*
+ This file is part of Subsonic.
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+ Copyright 2014 (C) Scott Jackson
+*/
+
+package github.daneren2005.dsub.util.compat;
+
+import android.support.v7.media.MediaRouter;
+
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.CastMediaControlIntent;
+
+import github.daneren2005.dsub.service.ChromeCastController;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.service.RemoteController;
+
+/**
+ * Created by owner on 2/9/14.
+ */
+public final class CastCompat {
+ public static final String APPLICATION_ID = "5F85EBEB";
+
+ static {
+ try {
+ Class.forName("com.google.android.gms.cast.CastDevice");
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static void checkAvailable() throws Throwable {
+ // Calling here forces class initialization.
+ }
+
+ public static RemoteController getController(DownloadService downloadService, MediaRouter.RouteInfo info) {
+ CastDevice device = CastDevice.getFromBundle(info.getExtras());
+ if(device != null) {
+ return new ChromeCastController(downloadService, device);
+ } else {
+ return null;
+ }
+ }
+
+ public static String getCastControlCategory() {
+ return CastMediaControlIntent.categoryForCast(APPLICATION_ID);
+ }
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
new file mode 100644
index 00000000..320092e9
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientBase.java
@@ -0,0 +1,43 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory.Entry;
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+
+public class RemoteControlClientBase extends RemoteControlClientHelper {
+
+ private static final String TAG = RemoteControlClientBase.class.getSimpleName();
+
+ @Override
+ public void register(Context context, ComponentName mediaButtonReceiverComponent) {
+
+ }
+
+ @Override
+ public void unregister(Context context) {
+
+ }
+
+ @Override
+ public void setPlaybackState(int state) {
+
+ }
+
+ @Override
+ public void updateMetadata(Context context, Entry currentSong) {
+
+ }
+
+ @Override
+ public void registerRoute(MediaRouter router) {
+
+ }
+
+ @Override
+ public void unregisterRoute(MediaRouter router) {
+
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java
new file mode 100644
index 00000000..93075a28
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientHelper.java
@@ -0,0 +1,32 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.media.MediaRouter;
+import android.os.Build;
+
+public abstract class RemoteControlClientHelper {
+
+ public static RemoteControlClientHelper createInstance() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ return new RemoteControlClientBase();
+ } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ return new RemoteControlClientJB();
+ } else {
+ return new RemoteControlClientICS();
+ }
+ }
+
+ protected RemoteControlClientHelper() {
+ // Avoid instantiation
+ }
+
+ public abstract void register(final Context context, final ComponentName mediaButtonReceiverComponent);
+ public abstract void unregister(final Context context);
+ public abstract void setPlaybackState(final int state);
+ public abstract void updateMetadata(final Context context, final MusicDirectory.Entry currentSong);
+ public abstract void registerRoute(MediaRouter router);
+ public abstract void unregisterRoute(MediaRouter router);
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
new file mode 100644
index 00000000..50283da6
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientICS.java
@@ -0,0 +1,104 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.ImageLoader;
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.RemoteControlClient;
+import android.support.v7.media.MediaRouter;
+
+import github.daneren2005.dsub.activity.SubsonicActivity;
+
+@TargetApi(14)
+public class RemoteControlClientICS extends RemoteControlClientHelper {
+ private static String TAG = RemoteControlClientICS.class.getSimpleName();
+
+ protected RemoteControlClient mRemoteControl;
+ protected ImageLoader imageLoader;
+ protected DownloadService downloadService;
+
+ public void register(final Context context, final ComponentName mediaButtonReceiverComponent) {
+ downloadService = (DownloadService) context;
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ // build the PendingIntent for the remote control client
+ Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ mediaButtonIntent.setComponent(mediaButtonReceiverComponent);
+ PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0, mediaButtonIntent, 0);
+
+ // create and register the remote control client
+ mRemoteControl = new RemoteControlClient(mediaPendingIntent);
+ audioManager.registerRemoteControlClient(mRemoteControl);
+
+ mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED);
+ mRemoteControl.setTransportControlFlags(getTransportFlags());
+ imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ public void unregister(final Context context) {
+ if (mRemoteControl != null) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.unregisterRemoteControlClient(mRemoteControl);
+ }
+ }
+
+ public void setPlaybackState(final int state) {
+ mRemoteControl.setPlaybackState(state);
+ }
+
+ public void updateMetadata(final Context context, final MusicDirectory.Entry currentSong) {
+ if(imageLoader == null) {
+ imageLoader = SubsonicActivity.getStaticImageLoader(context);
+ }
+
+ // Update the remote controls
+ RemoteControlClient.MetadataEditor editor = mRemoteControl.editMetadata(true);
+ updateMetadata(currentSong, editor);
+ editor.apply();
+ if (currentSong == null || imageLoader == null) {
+ mRemoteControl.editMetadata(true)
+ .putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, null)
+ .apply();
+ } else {
+ imageLoader.loadImage(context, mRemoteControl, currentSong);
+ }
+ }
+
+ @Override
+ public void registerRoute(MediaRouter router) {
+ router.addRemoteControlClient(mRemoteControl);
+ }
+
+ @Override
+ public void unregisterRoute(MediaRouter router) {
+ router.removeRemoteControlClient(mRemoteControl);
+ }
+
+ protected void updateMetadata(final MusicDirectory.Entry currentSong, final RemoteControlClient.MetadataEditor editor) {
+ editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, (currentSong == null) ? null : currentSong.getArtist())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, (currentSong == null) ? null : currentSong.getAlbum())
+ .putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, (currentSong == null) ? null : currentSong.getArtist())
+ .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, (currentSong) == null ? null : currentSong.getTitle())
+ .putString(MediaMetadataRetriever.METADATA_KEY_GENRE, (currentSong) == null ? null : currentSong.getGenre())
+ .putLong(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, (currentSong == null) ?
+ 0 : ((currentSong.getTrack() == null) ? 0 : currentSong.getTrack()))
+ .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, (currentSong == null) ?
+ 0 : ((currentSong.getDuration() == null) ? 0 : (currentSong.getDuration() * 1000)));
+ }
+
+ protected int getTransportFlags() {
+ return RemoteControlClient.FLAG_KEY_MEDIA_PLAY |
+ RemoteControlClient.FLAG_KEY_MEDIA_PAUSE |
+ RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE |
+ RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS |
+ RemoteControlClient.FLAG_KEY_MEDIA_NEXT |
+ RemoteControlClient.FLAG_KEY_MEDIA_STOP;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
new file mode 100644
index 00000000..c27df2ba
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/compat/RemoteControlClientJB.java
@@ -0,0 +1,58 @@
+package github.daneren2005.dsub.util.compat;
+
+import github.daneren2005.dsub.domain.MusicDirectory;
+import github.daneren2005.dsub.util.ImageLoader;
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.RemoteControlClient;
+import github.daneren2005.dsub.activity.SubsonicActivity;
+import github.daneren2005.dsub.service.DownloadService;
+import github.daneren2005.dsub.util.SilentBackgroundTask;
+
+@TargetApi(18)
+public class RemoteControlClientJB extends RemoteControlClientICS {
+ @Override
+ public void register(final Context context, final ComponentName mediaButtonReceiverComponent) {
+ super.register(context, mediaButtonReceiverComponent);
+
+ mRemoteControl.setOnGetPlaybackPositionListener(new RemoteControlClient.OnGetPlaybackPositionListener() {
+ @Override
+ public long onGetPlaybackPosition() {
+ return downloadService.getPlayerPosition();
+ }
+ });
+ mRemoteControl.setPlaybackPositionUpdateListener(new RemoteControlClient.OnPlaybackPositionUpdateListener() {
+ @Override
+ public void onPlaybackPositionUpdate(final long newPosition) {
+ new SilentBackgroundTask<Void>(context) {
+ @Override
+ protected Void doInBackground() throws Throwable {
+ downloadService.seekTo((int) newPosition);
+ return null;
+ }
+ }.execute();
+ setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING);
+ }
+ });
+ }
+
+ @Override
+ public void setPlaybackState(final int state) {
+ long position = -1;
+ if(state == RemoteControlClient.PLAYSTATE_PLAYING || state == RemoteControlClient.PLAYSTATE_PAUSED) {
+ position = downloadService.getPlayerPosition();
+ }
+ mRemoteControl.setPlaybackState(state, position, 1.0f);
+ }
+
+ @Override
+ protected int getTransportFlags() {
+ return super.getTransportFlags() | RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java b/app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java
new file mode 100644
index 00000000..aa0a2e25
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/Bastp.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.RandomAccessFile;
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class Bastp {
+
+ public Bastp() {
+ }
+
+ public HashMap getTags(String fname) {
+ HashMap tags = new HashMap();
+ try {
+ RandomAccessFile ra = new RandomAccessFile(fname, "r");
+ tags = getTags(ra);
+ ra.close();
+ }
+ catch(Exception e) {
+ /* we dont' care much: SOMETHING went wrong. d'oh! */
+ }
+
+ return tags;
+ }
+
+ public HashMap getTags(RandomAccessFile s) {
+ HashMap tags = new HashMap();
+ byte[] file_ff = new byte[4];
+
+ try {
+ s.read(file_ff);
+ String magic = new String(file_ff);
+ if(magic.equals("fLaC")) {
+ tags = (new FlacFile()).getTags(s);
+ }
+ else if(magic.equals("OggS")) {
+ tags = (new OggFile()).getTags(s);
+ }
+ else if(file_ff[0] == -1 && file_ff[1] == -5) { /* aka 0xfffb in real languages */
+ tags = (new LameHeader()).getTags(s);
+ }
+ else if(magic.substring(0,3).equals("ID3")) {
+ tags = (new ID3v2File()).getTags(s);
+ if(tags.containsKey("_hdrlen")) {
+ Long hlen = Long.parseLong( tags.get("_hdrlen").toString(), 10 );
+ HashMap lameInfo = (new LameHeader()).parseLameHeader(s, hlen);
+ /* add gain tags if not already present */
+ inheritTag("REPLAYGAIN_TRACK_GAIN", lameInfo, tags);
+ inheritTag("REPLAYGAIN_ALBUM_GAIN", lameInfo, tags);
+ }
+ }
+ tags.put("_magic", magic);
+ }
+ catch (IOException e) {
+ }
+ return tags;
+ }
+
+ private void inheritTag(String key, HashMap from, HashMap to) {
+ if(!to.containsKey(key) && from.containsKey(key)) {
+ to.put(key, from.get(key));
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java b/app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java
new file mode 100644
index 00000000..7ff517fd
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/BastpUtil.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import android.support.v4.util.LruCache;
+import java.util.HashMap;
+import java.util.Vector;
+
+public final class BastpUtil {
+ private static final RGLruCache rgCache = new RGLruCache(16);
+
+ /** Returns the ReplayGain values of 'path' as <track,album>
+ */
+ public static float[] getReplayGainValues(String path) {
+ float[] cached = rgCache.get(path);
+
+ if(cached == null) {
+ cached = getReplayGainValuesFromFile(path);
+ rgCache.put(path, cached);
+ }
+ return cached;
+ }
+
+
+
+ /** Parse given file and return track,album replay gain values
+ */
+ private static float[] getReplayGainValuesFromFile(String path) {
+ String[] keys = { "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN" };
+ float[] adjust= { 0f , 0f };
+ HashMap tags = (new Bastp()).getTags(path);
+
+ for (int i=0; i<keys.length; i++) {
+ String curKey = keys[i];
+ if(tags.containsKey(curKey)) {
+ String rg_raw = (String)((Vector)tags.get(curKey)).get(0);
+ String rg_numonly = "";
+ float rg_float = 0f;
+ try {
+ String nums = rg_raw.replaceAll("[^0-9.-]","");
+ rg_float = Float.parseFloat(nums);
+ } catch(Exception e) {}
+ adjust[i] = rg_float;
+ }
+ }
+ return adjust;
+ }
+
+ /** LRU cache for ReplayGain values
+ */
+ private static class RGLruCache extends LruCache<String, float[]> {
+ public RGLruCache(int size) {
+ super(size);
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/Common.java b/app/src/main/java/github/daneren2005/dsub/util/tags/Common.java
new file mode 100644
index 00000000..51344d90
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/Common.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Vector;
+
+public class Common {
+ private static final long MAX_PKT_SIZE = 524288;
+
+ public void xdie(String reason) throws IOException {
+ throw new IOException(reason);
+ }
+
+ /*
+ ** Returns a 32bit int from given byte offset in LE
+ */
+ public int b2le32(byte[] b, int off) {
+ int r = 0;
+ for(int i=0; i<4; i++) {
+ r |= ( b2u(b[off+i]) << (8*i) );
+ }
+ return r;
+ }
+
+ public int b2be32(byte[] b, int off) {
+ return swap32(b2le32(b, off));
+ }
+
+ public int swap32(int i) {
+ return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff);
+ }
+
+ /*
+ ** convert 'byte' value into unsigned int
+ */
+ public int b2u(byte x) {
+ return (x & 0xFF);
+ }
+
+ /*
+ ** Printout debug message to STDOUT
+ */
+ public void debug(String s) {
+ System.out.println("DBUG "+s);
+ }
+
+ public HashMap parse_vorbis_comment(RandomAccessFile s, long offset, long payload_len) throws IOException {
+ HashMap tags = new HashMap();
+ int comments = 0; // number of found comments
+ int xoff = 0; // offset within 'scratch'
+ int can_read = (int)(payload_len > MAX_PKT_SIZE ? MAX_PKT_SIZE : payload_len);
+ byte[] scratch = new byte[can_read];
+
+ // seek to given position and slurp in the payload
+ s.seek(offset);
+ s.read(scratch);
+
+ // skip vendor string in format: [LEN][VENDOR_STRING]
+ xoff += 4 + b2le32(scratch, xoff); // 4 = LEN = 32bit int
+ comments = b2le32(scratch, xoff);
+ xoff += 4;
+
+ // debug("comments count = "+comments);
+ for(int i=0; i<comments; i++) {
+
+ int clen = (int)b2le32(scratch, xoff);
+ xoff += 4+clen;
+
+ if(xoff > scratch.length)
+ xdie("string out of bounds");
+
+ String tag_raw = new String(scratch, xoff-clen, clen);
+ String[] tag_vec = tag_raw.split("=",2);
+ String tag_key = tag_vec[0].toUpperCase();
+
+ addTagEntry(tags, tag_key, tag_vec[1]);
+ }
+ return tags;
+ }
+
+ public void addTagEntry(HashMap tags, String key, String value) {
+ if(tags.containsKey(key)) {
+ ((Vector)tags.get(key)).add(value); // just add to existing vector
+ }
+ else {
+ Vector vx = new Vector();
+ vx.add(value);
+ tags.put(key, vx);
+ }
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java b/app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java
new file mode 100644
index 00000000..de3584d1
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/FlacFile.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+public class FlacFile extends Common {
+ private static final int FLAC_TYPE_COMMENT = 4; // ID of 'VorbisComment's
+
+ public FlacFile() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ int xoff = 4; // skip file magic
+ int retry = 64;
+ int r[];
+ HashMap tags = new HashMap();
+
+ for(; retry > 0; retry--) {
+ r = parse_metadata_block(s, xoff);
+
+ if(r[2] == FLAC_TYPE_COMMENT) {
+ tags = parse_vorbis_comment(s, xoff+r[0], r[1]);
+ break;
+ }
+
+ if(r[3] != 0)
+ break; // eof reached
+
+ // else: calculate next offset
+ xoff += r[0] + r[1];
+ }
+ return tags;
+ }
+
+ /* Parses the metadata block at 'offset' and returns
+ ** [header_size, payload_size, type, stop_after]
+ */
+ private int[] parse_metadata_block(RandomAccessFile s, long offset) throws IOException {
+ int[] result = new int[4];
+ byte[] mb_head = new byte[4];
+ int stop_after = 0;
+ int block_type = 0;
+ int block_size = 0;
+
+ s.seek(offset);
+
+ if( s.read(mb_head) != 4 )
+ xdie("failed to read metadata block header");
+
+ block_size = b2be32(mb_head,0); // read whole header as 32 big endian
+ block_type = (block_size >> 24) & 127; // BIT 1-7 are the type
+ stop_after = (((block_size >> 24) & 128) > 0 ? 1 : 0 ); // BIT 0 indicates the last-block flag
+ block_size = (block_size & 0x00FFFFFF); // byte 1-7 are the size
+
+ // debug("size="+block_size+", type="+block_type+", is_last="+stop_after);
+
+ result[0] = 4; // hardcoded - only returned to be consistent with OGG parser
+ result[1] = block_size;
+ result[2] = block_type;
+ result[3] = stop_after;
+
+ return result;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
new file mode 100644
index 00000000..ea61f36c
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/ID3v2File.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Locale;
+
+
+public class ID3v2File extends Common {
+ private static int ID3_ENC_LATIN = 0x00;
+ private static int ID3_ENC_UTF16LE = 0x01;
+ private static int ID3_ENC_UTF16BE = 0x02;
+ private static int ID3_ENC_UTF8 = 0x03;
+
+ public ID3v2File() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ HashMap tags = new HashMap();
+
+ final int v2hdr_len = 10;
+ byte[] v2hdr = new byte[v2hdr_len];
+
+ // read the whole 10 byte header into memory
+ s.seek(0);
+ s.read(v2hdr);
+
+ int id3v = ((b2be32(v2hdr,0))) & 0xFF; // swapped ID3\04 -> ver. ist the first byte
+ int v3len = ((b2be32(v2hdr,6))); // total size EXCLUDING the this 10 byte header
+ v3len = ((v3len & 0x7f000000) >> 3) | // for some funky reason, this is encoded as 7*4 bits
+ ((v3len & 0x007f0000) >> 2) |
+ ((v3len & 0x00007f00) >> 1) |
+ ((v3len & 0x0000007f) >> 0) ;
+
+ // debug(">> tag version ID3v2."+id3v);
+ // debug(">> LEN= "+v3len+" // "+v3len);
+
+ // we should already be at the first frame
+ // so we can start the parsing right now
+ tags = parse_v3_frames(s, v3len);
+ tags.put("_hdrlen", v3len+v2hdr_len);
+ return tags;
+ }
+
+ /* Parses all ID3v2 frames at the current position up until payload_len
+ ** bytes were read
+ */
+ public HashMap parse_v3_frames(RandomAccessFile s, long payload_len) throws IOException {
+ HashMap tags = new HashMap();
+ byte[] frame = new byte[10]; // a frame header is always 10 bytes
+ long bread = 0; // total amount of read bytes
+
+ while(bread < payload_len) {
+ bread += s.read(frame);
+ String framename = new String(frame, 0, 4);
+ int slen = b2be32(frame, 4);
+
+ /* Abort on silly sizes */
+ if(slen < 1 || slen > 524288)
+ break;
+
+ byte[] xpl = new byte[slen];
+ bread += s.read(xpl);
+
+ if(framename.substring(0,1).equals("T")) {
+ String[] nmzInfo = normalizeTaginfo(framename, xpl);
+
+ for(int i = 0; i < nmzInfo.length; i += 2) {
+ String oggKey = nmzInfo[i];
+ String decPld = nmzInfo[i + 1];
+
+ if (oggKey.length() > 0 && !tags.containsKey(oggKey)) {
+ addTagEntry(tags, oggKey, decPld);
+ }
+ }
+ }
+ else if(framename.equals("RVA2")) {
+ //
+ }
+
+ }
+ return tags;
+ }
+
+ /* Converts ID3v2 sillyframes to OggNames */
+ private String[] normalizeTaginfo(String k, byte[] v) {
+ String[] rv = new String[] {"",""};
+ HashMap lu = new HashMap<String, String>();
+ lu.put("TIT2", "TITLE");
+ lu.put("TALB", "ALBUM");
+ lu.put("TPE1", "ARTIST");
+
+ if(lu.containsKey(k)) {
+ /* A normal, known key: translate into Ogg-Frame name */
+ rv[0] = (String)lu.get(k);
+ rv[1] = getDecodedString(v);
+ }
+ else if(k.equals("TXXX")) {
+ /* A freestyle field, ieks! */
+ String txData[] = getDecodedString(v).split(Character.toString('\0'), 2);
+ /* Check if we got replaygain info in key\0value style */
+ if(txData.length == 2) {
+ if(txData[0].matches("^(?i)REPLAYGAIN_(ALBUM|TRACK)_GAIN$")) {
+ rv[0] = txData[0].toUpperCase(); /* some tagwriters use lowercase for this */
+ rv[1] = txData[1];
+ } else {
+ // Check for replaygain tags just thrown randomly in field
+ int nextStartIndex = 1;
+ int startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_");
+ ArrayList<String> parts = new ArrayList<String>();
+ while(startName != -1) {
+ int endName = txData[1].indexOf((char) 0, startName);
+ if(endName != -1) {
+ parts.add(txData[1].substring(startName, endName).toUpperCase());
+ int endValue = txData[1].indexOf((char) 0, endName + 1);
+ if(endValue != -1) {
+ parts.add(txData[1].substring(endName + 1, endValue));
+ nextStartIndex = endValue + 1;
+ }
+ }
+
+ startName = txData[1].toLowerCase(Locale.US).indexOf("replaygain_", nextStartIndex);
+ }
+
+ if(parts.size() > 0) {
+ rv = new String[parts.size()];
+ rv = parts.toArray(rv);
+ }
+ }
+ }
+ }
+
+ return rv;
+ }
+
+ /* Converts a raw byte-stream text into a java String */
+ private String getDecodedString(byte[] raw) {
+ int encid = raw[0] & 0xFF;
+ int len = raw.length;
+ String v = "";
+ try {
+ if(encid == ID3_ENC_LATIN) {
+ v = new String(raw, 1, len-1, "ISO-8859-1");
+ }
+ else if (encid == ID3_ENC_UTF8) {
+ v = new String(raw, 1, len-1, "UTF-8");
+ }
+ else if (encid == ID3_ENC_UTF16LE) {
+ v = new String(raw, 3, len-3, "UTF-16LE");
+ }
+ else if (encid == ID3_ENC_UTF16BE) {
+ v = new String(raw, 3, len-3, "UTF-16BE");
+ }
+ } catch(Exception e) {}
+ return v;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java b/app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java
new file mode 100644
index 00000000..720ee87f
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/LameHeader.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Enumeration;
+
+
+public class LameHeader extends Common {
+
+ public LameHeader() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ return parseLameHeader(s, 0);
+ }
+
+ public HashMap parseLameHeader(RandomAccessFile s, long offset) throws IOException {
+ HashMap tags = new HashMap();
+ byte[] chunk = new byte[4];
+
+ s.seek(offset + 0x24);
+ s.read(chunk);
+
+ String lameMark = new String(chunk, 0, chunk.length, "ISO-8859-1");
+
+ if(lameMark.equals("Info") || lameMark.equals("Xing")) {
+ s.seek(offset+0xAB);
+ s.read(chunk);
+
+ int raw = b2be32(chunk, 0);
+ int gtrk_raw = raw >> 16; /* first 16 bits are the raw track gain value */
+ int galb_raw = raw & 0xFFFF; /* the rest is for the album gain value */
+
+ float gtrk_val = (float)(gtrk_raw & 0x01FF)/10;
+ float galb_val = (float)(galb_raw & 0x01FF)/10;
+
+ gtrk_val = ((gtrk_raw&0x0200)!=0 ? -1*gtrk_val : gtrk_val);
+ galb_val = ((galb_raw&0x0200)!=0 ? -1*galb_val : galb_val);
+
+ if( (gtrk_raw&0xE000) == 0x2000 ) {
+ addTagEntry(tags, "REPLAYGAIN_TRACK_GAIN", gtrk_val+" dB");
+ }
+ if( (gtrk_raw&0xE000) == 0x4000 ) {
+ addTagEntry(tags, "REPLAYGAIN_ALBUM_GAIN", galb_val+" dB");
+ }
+
+ }
+
+ return tags;
+ }
+
+}
diff --git a/app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java b/app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java
new file mode 100644
index 00000000..d0b31671
--- /dev/null
+++ b/app/src/main/java/github/daneren2005/dsub/util/tags/OggFile.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 Adrian Ulrich <adrian@blinkenlights.ch>
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package github.daneren2005.dsub.util.tags;
+
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+
+
+public class OggFile extends Common {
+
+ private static final int OGG_PAGE_SIZE = 27; // Static size of an OGG Page
+ private static final int OGG_TYPE_COMMENT = 3; // ID of 'VorbisComment's
+
+ public OggFile() {
+ }
+
+ public HashMap getTags(RandomAccessFile s) throws IOException {
+ long offset = 0;
+ int retry = 64;
+ HashMap tags = new HashMap();
+
+ for( ; retry > 0 ; retry-- ) {
+ long res[] = parse_ogg_page(s, offset);
+ if(res[2] == OGG_TYPE_COMMENT) {
+ tags = parse_ogg_vorbis_comment(s, offset+res[0], res[1]);
+ break;
+ }
+ offset += res[0] + res[1];
+ }
+ return tags;
+ }
+
+
+ /* Parses the ogg page at offset 'offset' and returns
+ ** [header_size, payload_size, type]
+ */
+ private long[] parse_ogg_page(RandomAccessFile s, long offset) throws IOException {
+ long[] result = new long[3]; // [header_size, payload_size]
+ byte[] p_header = new byte[OGG_PAGE_SIZE]; // buffer for the page header
+ byte[] scratch;
+ int bread = 0; // number of bytes read
+ int psize = 0; // payload-size
+ int nsegs = 0; // Number of segments
+
+ s.seek(offset);
+ bread = s.read(p_header);
+ if(bread != OGG_PAGE_SIZE)
+ xdie("Unable to read() OGG_PAGE_HEADER");
+ if((new String(p_header, 0, 5)).equals("OggS\0") != true)
+ xdie("Invalid magic - not an ogg file?");
+
+ nsegs = b2u(p_header[26]);
+ // debug("> file seg: "+nsegs);
+ if(nsegs > 0) {
+ scratch = new byte[nsegs];
+ bread = s.read(scratch);
+ if(bread != nsegs)
+ xdie("Failed to read segtable");
+
+ for(int i=0; i<nsegs; i++) {
+ psize += b2u(scratch[i]);
+ }
+ }
+
+ // populate result array
+ result[0] = (s.getFilePointer() - offset);
+ result[1] = psize;
+ result[2] = -1;
+
+ /* next byte is most likely the type -> pre-read */
+ if(psize >= 1 && s.read(p_header, 0, 1) == 1) {
+ result[2] = b2u(p_header[0]);
+ }
+
+ return result;
+ }
+
+ /* In 'vorbiscomment' field is prefixed with \3vorbis in OGG files
+ ** we check that this marker is present and call the generic comment
+ ** parset with the correct offset (+7) */
+ private HashMap parse_ogg_vorbis_comment(RandomAccessFile s, long offset, long pl_len) throws IOException {
+ final int pfx_len = 7;
+ byte[] pfx = new byte[pfx_len];
+
+ if(pl_len < pfx_len)
+ xdie("ogg vorbis comment field is too short!");
+
+ s.seek(offset);
+ s.read(pfx);
+
+ if( (new String(pfx, 0, pfx_len)).equals("\3vorbis") == false )
+ xdie("Damaged packet found!");
+
+ return parse_vorbis_comment(s, offset+pfx_len, pl_len-pfx_len);
+ }
+
+};