From 32024ed1afa53b78829235a1b1a4c92bb23fed87 Mon Sep 17 00:00:00 2001 From: Joshua Bahnsen Date: Fri, 8 Feb 2013 02:09:55 -0700 Subject: [PATCH] Fix notification sounds causing playback to start, fixed some CacheCleaner issues --- AndroidManifest.xml | 252 +- .../androidapp/activity/HelpActivity.java | 253 +- .../receiver/MediaButtonIntentReceiver.java | 115 +- .../service/DownloadServiceImpl.java | 2133 +++++++++-------- .../service/OfflineMusicService.java | 520 ++-- .../subsonic/androidapp/util/AlbumView.java | 188 +- .../androidapp/util/CacheCleaner.java | 77 +- .../subsonic/androidapp/util/FileUtil.java | 605 ++--- .../subsonic/androidapp/util/ImageLoader.java | 496 ++-- .../subsonic/androidapp/util/SongView.java | 448 ++-- .../subsonic/androidapp/util/Util.java | 1756 +++++++------- 11 files changed, 3442 insertions(+), 3401 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3394514d..db2c3e28 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,126 +1,126 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java b/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java index 652069d8..1fe37a63 100644 --- a/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java +++ b/src/net/sourceforge/subsonic/androidapp/activity/HelpActivity.java @@ -1,126 +1,127 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ - -package net.sourceforge.subsonic.androidapp.activity; - -import android.app.Activity; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.view.Window; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Button; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.util.Util; - -/** - * An HTML-based help screen with Back and Done buttons at the bottom. - * - * @author Sindre Mehus - */ -public final class HelpActivity extends Activity { - - private WebView webView; - private Button backButton; - - @Override - protected void onCreate(Bundle bundle) { - super.onCreate(bundle); - getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - - setContentView(R.layout.help); - - webView = (WebView) findViewById(R.id.help_contents); - webView.getSettings().setJavaScriptEnabled(true); - webView.setWebViewClient(new HelpClient()); - if (bundle != null) { - webView.restoreState(bundle); - } else { - webView.loadUrl(getResources().getString(R.string.help_url)); - } - - backButton = (Button) findViewById(R.id.help_back); - backButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - webView.goBack(); - } - }); - - Button doneButton = (Button) findViewById(R.id.help_close); - doneButton.setOnClickListener(new Button.OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - } - - @Override - public void onResume() { - super.onResume(); - } - - @Override - protected void onSaveInstanceState(Bundle state) { - webView.saveState(state); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (webView.canGoBack()) { - webView.goBack(); - return true; - } - } - return super.onKeyDown(keyCode, event); - } - - private final class HelpClient extends WebViewClient { - @Override - public void onLoadResource(WebView webView, String url) { - setProgressBarIndeterminateVisibility(true); - setTitle(getResources().getString(R.string.help_loading)); - super.onLoadResource(webView, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - setProgressBarIndeterminateVisibility(false); - String versionName = null; - - try { - versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - - setTitle(view.getTitle() + " (" + versionName + ")"); - backButton.setEnabled(view.canGoBack()); - } - - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - Util.toast(HelpActivity.this, description); - } - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.androidapp.activity; + +import android.app.Activity; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Button; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.util.Util; + +/** + * An HTML-based help screen with Back and Done buttons at the bottom. + * + * @author Sindre Mehus + */ +public final class HelpActivity extends Activity { + private static final String TAG = HelpActivity.class.getSimpleName(); + private WebView webView; + private Button backButton; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.help); + + webView = (WebView) findViewById(R.id.help_contents); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new HelpClient()); + if (bundle != null) { + webView.restoreState(bundle); + } else { + webView.loadUrl(getResources().getString(R.string.help_url)); + } + + backButton = (Button) findViewById(R.id.help_back); + backButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + webView.goBack(); + } + }); + + Button doneButton = (Button) findViewById(R.id.help_close); + doneButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + protected void onSaveInstanceState(Bundle state) { + webView.saveState(state); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (webView.canGoBack()) { + webView.goBack(); + return true; + } + } + return super.onKeyDown(keyCode, event); + } + + private final class HelpClient extends WebViewClient { + @Override + public void onLoadResource(WebView webView, String url) { + setProgressBarIndeterminateVisibility(true); + setTitle(getResources().getString(R.string.help_loading)); + super.onLoadResource(webView, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + setProgressBarIndeterminateVisibility(false); + String versionName = null; + + try { + versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + } catch (NameNotFoundException e) { + Log.e(TAG, e.getMessage(), e); + } + + setTitle(view.getTitle() + " (" + versionName + ")"); + backButton.setEnabled(view.canGoBack()); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + Util.toast(HelpActivity.this, description); + } + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/receiver/MediaButtonIntentReceiver.java b/src/net/sourceforge/subsonic/androidapp/receiver/MediaButtonIntentReceiver.java index 67ac3eba..197f27a9 100644 --- a/src/net/sourceforge/subsonic/androidapp/receiver/MediaButtonIntentReceiver.java +++ b/src/net/sourceforge/subsonic/androidapp/receiver/MediaButtonIntentReceiver.java @@ -1,55 +1,60 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2010 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import android.view.KeyEvent; -import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; -import net.sourceforge.subsonic.androidapp.util.Util; - -/** - * @author Sindre Mehus - */ -public class MediaButtonIntentReceiver extends BroadcastReceiver { - - private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); - - @Override - public void onReceive(Context context, Intent intent) { - if (Util.getMediaButtonsPreference(context)) { - KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); - Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); - - Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); - serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - context.startService(serviceIntent); - - try { - if (isOrderedBroadcast()) { - abortBroadcast(); - } - } catch (Exception x) { - // Ignored. - } - } - } -} +/* + 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 . + + Copyright 2010 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; +import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; +import net.sourceforge.subsonic.androidapp.util.Util; + +/** + * @author Sindre Mehus + */ +public class MediaButtonIntentReceiver extends BroadcastReceiver { + + private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if (Util.getMediaButtonsPreference(context)) { + String intentAction = intent.getAction(); + + if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) + return; + + KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); + Log.i(TAG, "Got MEDIA_BUTTON key event: " + event); + + Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); + serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); + context.startService(serviceIntent); + + try { + if (isOrderedBroadcast()) { + abortBroadcast(); + } + } catch (Exception x) { + // Ignored. + } + } + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java index a63c7a16..5c236256 100644 --- a/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java +++ b/src/net/sourceforge/subsonic/androidapp/service/DownloadServiceImpl.java @@ -1,1062 +1,1071 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.service; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.media.AudioManager; -import android.media.AudioManager.OnAudioFocusChangeListener; -import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.os.Handler; -import android.os.IBinder; -import android.os.PowerManager; -import android.util.DisplayMetrics; -import android.util.Log; -import android.widget.RemoteViews; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; -import net.sourceforge.subsonic.androidapp.audiofx.EqualizerController; -import net.sourceforge.subsonic.androidapp.audiofx.VisualizerController; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.domain.PlayerState; -import net.sourceforge.subsonic.androidapp.domain.RepeatMode; -import net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver; -import net.sourceforge.subsonic.androidapp.util.CancellableTask; -import net.sourceforge.subsonic.androidapp.util.LRUCache; -import net.sourceforge.subsonic.androidapp.util.ShufflePlayBuffer; -import net.sourceforge.subsonic.androidapp.util.SimpleServiceBinder; -import net.sourceforge.subsonic.androidapp.util.Util; -import net.sourceforge.subsonic.androidapp.util.RemoteControlHelper; -import net.sourceforge.subsonic.androidapp.util.RemoteControlClientCompat; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import static net.sourceforge.subsonic.androidapp.domain.PlayerState.*; - -/** - * @author Sindre Mehus, Joshua Bahnsen - * @version $Id$ - */ -public class DownloadServiceImpl extends Service implements DownloadService { - - private static final String TAG = DownloadServiceImpl.class.getSimpleName(); - - public static final String CMD_PLAY = "net.sourceforge.subsonic.androidapp.CMD_PLAY"; - public static final String CMD_TOGGLEPAUSE = "net.sourceforge.subsonic.androidapp.CMD_TOGGLEPAUSE"; - public static final String CMD_PAUSE = "net.sourceforge.subsonic.androidapp.CMD_PAUSE"; - public static final String CMD_STOP = "net.sourceforge.subsonic.androidapp.CMD_STOP"; - public static final String CMD_PREVIOUS = "net.sourceforge.subsonic.androidapp.CMD_PREVIOUS"; - public static final String CMD_NEXT = "net.sourceforge.subsonic.androidapp.CMD_NEXT"; - - private final IBinder binder = new SimpleServiceBinder(this); - private MediaPlayer mediaPlayer; - private final List downloadList = new ArrayList(); - private final Handler handler = new Handler(); - private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); - private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); - - private final LRUCache downloadFileCache = new LRUCache(100); - private final List cleanupCandidates = new ArrayList(); - private final Scrobbler scrobbler = new Scrobbler(); - private final JukeboxService jukeboxService = new JukeboxService(this); - private Notification notification = new Notification(R.drawable.ic_stat_subsonic, null, System.currentTimeMillis()); - - private DownloadFile currentPlaying; - private DownloadFile currentDownloading; - private CancellableTask bufferTask; - private PlayerState playerState = IDLE; - private boolean shufflePlay; - private long revision; - private static DownloadService instance; - private String suggestedPlaylistName; - private PowerManager.WakeLock wakeLock; - private boolean keepScreenOn = false; - - private static boolean equalizerAvailable; - private static boolean visualizerAvailable; - private EqualizerController equalizerController; - private VisualizerController visualizerController; - private boolean showVisualization; - private boolean jukeboxEnabled; - - RemoteControlClientCompat remoteControlClientCompat; - - static { - try { - EqualizerController.checkAvailable(); - equalizerAvailable = true; - } catch (Throwable t) { - equalizerAvailable = false; - } - } - static { - try { - VisualizerController.checkAvailable(); - visualizerAvailable = true; - } catch (Throwable t) { - visualizerAvailable = false; - } - } - - private OnAudioFocusChangeListener _afChangeListener = new OnAudioFocusChangeListener() { - public void onAudioFocusChange(int focusChange) { - if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - start(); - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - stop(); - } - } - }; - - @Override - public void onCreate() { - super.onCreate(); - - mediaPlayer = new MediaPlayer(); - mediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK); - - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { - @Override - public boolean onError(MediaPlayer mediaPlayer, int what, int more) { - handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")")); - return false; - } - }); - - notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; - notification.contentView = new RemoteViews(this.getPackageName(), R.layout.notification); - Intent notificationIntent = new Intent(this, DownloadActivity.class); - notification.contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); - Util.linkButtons(this, notification.contentView, false); - - if (equalizerAvailable) { - equalizerController = new EqualizerController(this, mediaPlayer); - if (!equalizerController.isAvailable()) { - equalizerController = null; - } else { - equalizerController.loadSettings(); - } - } - if (visualizerAvailable) { - visualizerController = new VisualizerController(this, mediaPlayer); - if (!visualizerController.isAvailable()) { - visualizerController = null; - } - } - - PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); - wakeLock.setReferenceCounted(false); - - instance = this; - lifecycleSupport.onCreate(); - } - - @Override - public void onStart(Intent intent, int startId) { - super.onStart(intent, startId); - lifecycleSupport.onStart(intent); - } - - @Override - public void onDestroy() { - super.onDestroy(); - lifecycleSupport.onDestroy(); - mediaPlayer.release(); - shufflePlayBuffer.shutdown(); - if (equalizerController != null) { - equalizerController.release(); - } - if (visualizerController != null) { - visualizerController.release(); - } - - AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - RemoteControlHelper.unregisterRemoteControlClient(audioManager, remoteControlClientCompat); - notification = null; - instance = null; - } - - public static DownloadService getInstance() { - return instance; - } - - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - @Override - public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext) { - shufflePlay = false; - int offset = 1; - - if (songs.isEmpty()) { - return; - } - if (playNext) { - if (autoplay && getCurrentPlayingIndex() >= 0) { - offset = 0; - } - for (MusicDirectory.Entry song : songs) { - DownloadFile downloadFile = new DownloadFile(this, song, save); - downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); - offset++; - } - revision++; - } else { - for (MusicDirectory.Entry song : songs) { - DownloadFile downloadFile = new DownloadFile(this, song, save); - downloadList.add(downloadFile); - } - revision++; - } - updateJukeboxPlaylist(); - - if (autoplay) { - play(0); - } else { - if (currentPlaying == null) { - currentPlaying = downloadList.get(0); - } - checkDownloads(); - } - lifecycleSupport.serializeDownloadQueue(); - } - - private void updateJukeboxPlaylist() { - if (jukeboxEnabled) { - jukeboxService.updatePlaylist(); - } - } - - public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition) { - download(songs, false, false, false); - if (currentPlayingIndex != -1) { - play(currentPlayingIndex, false); - if (currentPlaying.isCompleteFileAvailable()) { - doPlay(currentPlaying, currentPlayingPosition, false); - } - } - } - - @Override - public synchronized void setShufflePlayEnabled(boolean enabled) { - if (shufflePlay == enabled) { - return; - } - - shufflePlay = enabled; - if (shufflePlay) { - clear(); - checkDownloads(); - } - } - - @Override - public synchronized boolean isShufflePlayEnabled() { - return shufflePlay; - } - - @Override - public synchronized void shuffle() { - Collections.shuffle(downloadList); - if (currentPlaying != null) { - downloadList.remove(getCurrentPlayingIndex()); - downloadList.add(0, currentPlaying); - } - revision++; - lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - } - - @Override - public RepeatMode getRepeatMode() { - return Util.getRepeatMode(this); - } - - @Override - public void setRepeatMode(RepeatMode repeatMode) { - Util.setRepeatMode(this, repeatMode); - } - - @Override - public boolean getKeepScreenOn() { - return keepScreenOn; - } - - @Override - public void setKeepScreenOn(boolean keepScreenOn) { - this.keepScreenOn = keepScreenOn; - } - - @Override - public boolean getShowVisualization() { - return showVisualization; - } - - @Override - public void setShowVisualization(boolean showVisualization) { - this.showVisualization = showVisualization; - } - - @Override - public synchronized DownloadFile forSong(MusicDirectory.Entry song) { - for (DownloadFile downloadFile : downloadList) { - if (downloadFile.getSong().equals(song)) { - return downloadFile; - } - } - - DownloadFile downloadFile = downloadFileCache.get(song); - if (downloadFile == null) { - downloadFile = new DownloadFile(this, song, false); - downloadFileCache.put(song, downloadFile); - } - return downloadFile; - } - - @Override - public synchronized void clear() { - clear(true); - } - - @Override - public synchronized void clearIncomplete() { - reset(); - Iterator iterator = downloadList.iterator(); - while (iterator.hasNext()) { - DownloadFile downloadFile = iterator.next(); - if (!downloadFile.isCompleteFileAvailable()) { - iterator.remove(); - } - } - lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - } - - @Override - public synchronized int size() { - return downloadList.size(); - } - - public synchronized void clear(boolean serialize) { - reset(); - downloadList.clear(); - revision++; - if (currentDownloading != null) { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - setCurrentPlaying(null, false); - - if (serialize) { - lifecycleSupport.serializeDownloadQueue(); - } - updateJukeboxPlaylist(); - } - - @Override - public synchronized void remove(DownloadFile downloadFile) { - if (downloadFile == currentDownloading) { - currentDownloading.cancelDownload(); - currentDownloading = null; - } - if (downloadFile == currentPlaying) { - reset(); - setCurrentPlaying(null, false); - } - downloadList.remove(downloadFile); - revision++; - lifecycleSupport.serializeDownloadQueue(); - updateJukeboxPlaylist(); - } - - @Override - public synchronized void delete(List songs) { - for (MusicDirectory.Entry song : songs) { - forSong(song).delete(); - } - } - - @Override - public synchronized void unpin(List songs) { - for (MusicDirectory.Entry song : songs) { - forSong(song).unpin(); - } - } - - synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) { - try { - setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification); - } catch (IndexOutOfBoundsException x) { - // Ignored - } - } - - synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) { - this.currentPlaying = currentPlaying; - - if (currentPlaying != null) { - Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); - } else { - Util.broadcastNewTrackInfo(this, null); - } - - setRemoteControl(); - - if (Util.isNotificationEnabled(this) && currentPlaying != null && showNotification) { - Util.showPlayingNotification(this, this, handler, currentPlaying.getSong(), this.notification, this.playerState); - } else { - Util.hidePlayingNotification(this, this, handler); - } - } - - @Override - public synchronized int getCurrentPlayingIndex() { - return downloadList.indexOf(currentPlaying); - } - - @Override - public DownloadFile getCurrentPlaying() { - return currentPlaying; - } - - @Override - public DownloadFile getCurrentDownloading() { - return currentDownloading; - } - - @Override - public synchronized List getDownloads() { - return new ArrayList(downloadList); - } - - /** Plays either the current song (resume) or the first/next one in queue. */ - public synchronized void play() - { - int current = getCurrentPlayingIndex(); - if (current == -1) { - play(0); - } else { - play(current); - } - } - - @Override - public synchronized void play(int index) { - play(index, true); - } - - private synchronized void play(int index, boolean start) { - if (index < 0 || index >= size()) { - reset(); - setCurrentPlaying(null, false); - } else { - setCurrentPlaying(index, start); - checkDownloads(); - if (start) { - if (jukeboxEnabled) { - jukeboxService.skip(getCurrentPlayingIndex(), 0); - setPlayerState(STARTED); - } else { - bufferAndPlay(); - } - } - } - } - - /** Plays or resumes the playback, depending on the current player state. */ - public synchronized void togglePlayPause() - { - if (playerState == PAUSED || playerState == COMPLETED) { - start(); - } else if (playerState == STOPPED || playerState == IDLE) { - play(); - } else if (playerState == STARTED) { - pause(); - } - } - - @Override - public synchronized void seekTo(int position) { - try { - if (jukeboxEnabled) { - jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); - } else { - mediaPlayer.seekTo(position); - } - } catch (Exception x) { - handleError(x); - } - } - - @Override - public synchronized void previous() { - int index = getCurrentPlayingIndex(); - if (index == -1) { - return; - } - - // Restart song if played more than five seconds. - if (getPlayerPosition() > 5000 || index == 0) { - play(index); - } else { - play(index - 1); - } - } - - @Override - public synchronized void next() { - int index = getCurrentPlayingIndex(); - if (index != -1) { - play(index + 1); - } - } - - private void onSongCompleted() { - int index = getCurrentPlayingIndex(); - if (index != -1) { - switch (getRepeatMode()) { - case OFF: - play(index + 1); - break; - case ALL: - play((index + 1) % size()); - break; - case SINGLE: - play(index); - break; - default: - break; - } - } - } - - @Override - public synchronized void pause() { - try { - if (playerState == STARTED) { - if (jukeboxEnabled) { - jukeboxService.stop(); - } else { - mediaPlayer.pause(); - } - setPlayerState(PAUSED); - } - } catch (Exception x) { - handleError(x); - } - } - - @Override - public synchronized void stop() { - AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - audioManager.abandonAudioFocus(_afChangeListener); - - try { - if (playerState == STARTED) { - if (jukeboxEnabled) { - jukeboxService.stop(); - } else { - mediaPlayer.pause(); - } - setPlayerState(PAUSED); - } - } catch (Exception x) { - handleError(x); - } - - //seekTo(0); - } - - @Override - public synchronized void start() { - try { - if (jukeboxEnabled) { - jukeboxService.start(); - } else { - mediaPlayer.start(); - } - setPlayerState(STARTED); - } catch (Exception x) { - handleError(x); - } - } - - @Override - public synchronized void reset() { - if (bufferTask != null) { - bufferTask.cancel(); - } - try { - mediaPlayer.reset(); - setPlayerState(IDLE); - } catch (Exception x) { - handleError(x); - } - } - - @Override - public synchronized int getPlayerPosition() { - try { - if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { - return 0; - } - if (jukeboxEnabled) { - return jukeboxService.getPositionSeconds() * 1000; - } else { - return mediaPlayer.getCurrentPosition(); - } - } catch (Exception x) { - handleError(x); - return 0; - } - } - - @Override - public synchronized int getPlayerDuration() { - if (currentPlaying != null) { - Integer duration = currentPlaying.getSong().getDuration(); - if (duration != null) { - return duration * 1000; - } - } - if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { - try { - return mediaPlayer.getDuration(); - } catch (Exception x) { - handleError(x); - } - } - return 0; - } - - @Override - public PlayerState getPlayerState() { - return playerState; - } - - synchronized void setPlayerState(PlayerState playerState) { - Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); - - if (playerState == PAUSED) { - lifecycleSupport.serializeDownloadQueue(); - } - - boolean show = playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED; - boolean hide = playerState == PlayerState.IDLE || playerState == PlayerState.STOPPED; - Util.broadcastPlaybackStatusChange(this, playerState); - - this.playerState = playerState; - - setRemoteControl(); - - if (Util.isNotificationEnabled(this)) { - if (show) { - Util.showPlayingNotification(this, this, handler, currentPlaying.getSong(), this.notification, this.playerState); - } else if (hide) { - Util.hidePlayingNotification(this, this, handler); - } - } else { - Util.hidePlayingNotification(this, this, handler); - } - - if (playerState == STARTED) { - scrobbler.scrobble(this, currentPlaying, false); - } else if (playerState == COMPLETED) { - scrobbler.scrobble(this, currentPlaying, true); - } - } - - @Override - public void setSuggestedPlaylistName(String name) { - this.suggestedPlaylistName = name; - } - - @Override - public String getSuggestedPlaylistName() { - return suggestedPlaylistName; - } - - @Override - public EqualizerController getEqualizerController() { - return equalizerController; - } - - @Override - public VisualizerController getVisualizerController() { - return visualizerController; - } - - @Override - public boolean isJukeboxEnabled() { - return jukeboxEnabled; - } - - @Override - public void setJukeboxEnabled(boolean jukeboxEnabled) { - this.jukeboxEnabled = jukeboxEnabled; - jukeboxService.setEnabled(jukeboxEnabled); - if (jukeboxEnabled) { - reset(); - - // Cancel current download, if necessary. - if (currentDownloading != null) { - currentDownloading.cancelDownload(); - } - } - } - - @Override - public void adjustJukeboxVolume(boolean up) { - jukeboxService.adjustVolume(up); - } - - private void setRemoteControl() { - if (Util.isLockScreenEnabled(this)) { - AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - audioManager.requestAudioFocus(_afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); - - if (remoteControlClientCompat == null) { - audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); - intent.setComponent(new ComponentName(this.getPackageName(), MediaButtonIntentReceiver.class.getName())); - remoteControlClientCompat = new RemoteControlClientCompat(PendingIntent.getBroadcast(this, 0, intent, 0)); - RemoteControlHelper.registerRemoteControlClient(audioManager, remoteControlClientCompat); - } - - switch (playerState) - { - case STARTED: - remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - break; - case PAUSED: - remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - break; - case IDLE: - case STOPPED: - remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - break; - } - - remoteControlClientCompat.setTransportControlFlags( - RemoteControlClient.FLAG_KEY_MEDIA_PLAY | - RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_NEXT | - RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | - RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | - RemoteControlClient.FLAG_KEY_MEDIA_STOP); - - try { - - //String artist = currentPlaying.getSong().getArtist(); - //String album = currentPlaying.getSong().getAlbum(); - String album = currentPlaying.getSong().getAlbum(); - String title = currentPlaying.getSong().getArtist() + " - " + currentPlaying.getSong().getTitle(); - Integer duration = currentPlaying.getSong().getDuration(); - - MusicService musicService = MusicServiceFactory.getMusicService(this); - DisplayMetrics metrics = this.getResources().getDisplayMetrics(); - int size = Math.min(metrics.widthPixels, metrics.heightPixels); - Bitmap bitmap = musicService.getCoverArt(this, currentPlaying.getSong(), size, true, null); - - // Update the remote controls - remoteControlClientCompat.editMetadata(true) - //.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, artist) - .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) - .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) - .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) - .putBitmap(RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, bitmap) - .apply(); - } - catch (Exception e) { - Log.e(TAG, "Exception in setRemoteControl"); - } - } - } - - private synchronized void bufferAndPlay() { - reset(); - - bufferTask = new BufferTask(currentPlaying, 0); - bufferTask.start(); - } - - private synchronized void doPlay(final DownloadFile downloadFile, int position, boolean start) { - try { - final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); - downloadFile.updateModificationDate(); - mediaPlayer.setOnCompletionListener(null); - mediaPlayer.reset(); - setPlayerState(IDLE); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setDataSource(file.getPath()); - setPlayerState(PREPARING); - mediaPlayer.prepare(); - setPlayerState(PREPARED); - - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(MediaPlayer mediaPlayer) { - - // Acquire a temporary wakelock, since when we return from - // this callback the MediaPlayer will release its wakelock - // and allow the device to go to sleep. - wakeLock.acquire(60000); - - setPlayerState(COMPLETED); - - // If COMPLETED and not playing partial file, we are *really" finished - // with the song and can move on to the next. - if (!file.equals(downloadFile.getPartialFile())) { - onSongCompleted(); - return; - } - - // If file is not completely downloaded, restart the playback from the current position. - int pos = mediaPlayer.getCurrentPosition(); - synchronized (DownloadServiceImpl.this) { - - // Work-around for apparent bug on certain phones: If close (less than ten seconds) to the end - // of the song, skip to the next rather than restarting it. - Integer duration = downloadFile.getSong().getDuration() == null ? null : downloadFile.getSong().getDuration() * 1000; - if (duration != null) { - if (Math.abs(duration - pos) < 10000) { - Log.i(TAG, "Skipping restart from " + pos + " of " + duration); - onSongCompleted(); - return; - } - } - - Log.i(TAG, "Requesting restart from " + pos + " of " + duration); - reset(); - bufferTask = new BufferTask(downloadFile, pos); - bufferTask.start(); - } - } - }); - - if (position != 0) { - Log.i(TAG, "Restarting player from position " + position); - mediaPlayer.seekTo(position); - } - - if (start) { - mediaPlayer.start(); - setPlayerState(STARTED); - } else { - setPlayerState(PAUSED); - } - lifecycleSupport.serializeDownloadQueue(); - - } catch (Exception x) { - handleError(x); - } - } - - private void handleError(Exception x) { - Log.w(TAG, "Media player error: " + x, x); - mediaPlayer.reset(); - setPlayerState(IDLE); - } - - protected synchronized void checkDownloads() { - - if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { - return; - } - - if (shufflePlay) { - checkShufflePlay(); - } - - if (jukeboxEnabled || !Util.isNetworkConnected(this)) { - return; - } - - if (downloadList.isEmpty()) { - return; - } - - // Need to download current playing? - if (currentPlaying != null && - currentPlaying != currentDownloading && - !currentPlaying.isCompleteFileAvailable()) { - - // Cancel current download, if necessary. - if (currentDownloading != null) { - currentDownloading.cancelDownload(); - } - - currentDownloading = currentPlaying; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - } - - // Find a suitable target for download. - else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed()) { - - int n = size(); - if (n == 0) { - return; - } - - int preloaded = 0; - - int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); - int i = start; - do { - DownloadFile downloadFile = downloadList.get(i); - if (!downloadFile.isWorkDone()) { - if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) { - currentDownloading = downloadFile; - currentDownloading.download(); - cleanupCandidates.add(currentDownloading); - break; - } - } else if (currentPlaying != downloadFile) { - preloaded++; - } - - i = (i + 1) % n; - } while (i != start); - } - - // Delete obsolete .partial and .complete files. - cleanup(); - } - - private synchronized void checkShufflePlay() { - - final int listSize = 20; - boolean wasEmpty = downloadList.isEmpty(); - - long revisionBefore = revision; - - // First, ensure that list is at least 20 songs long. - int size = size(); - if (size < listSize) { - for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) { - DownloadFile downloadFile = new DownloadFile(this, song, false); - downloadList.add(downloadFile); - revision++; - } - } - - int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex(); - - // Only shift playlist if playing song #5 or later. - if (currIndex > 4) { - int songsToShift = currIndex - 2; - for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) { - downloadList.add(new DownloadFile(this, song, false)); - downloadList.get(0).cancelDownload(); - downloadList.remove(0); - revision++; - } - } - - if (revisionBefore != revision) { - updateJukeboxPlaylist(); - } - - if (wasEmpty && !downloadList.isEmpty()) { - play(0); - } - } - - public long getDownloadListUpdateRevision() { - return revision; - } - - private synchronized void cleanup() { - Iterator iterator = cleanupCandidates.iterator(); - while (iterator.hasNext()) { - DownloadFile downloadFile = iterator.next(); - if (downloadFile != currentPlaying && downloadFile != currentDownloading) { - if (downloadFile.cleanup()) { - iterator.remove(); - } - } - } - } - - private class BufferTask extends CancellableTask { - - private final DownloadFile downloadFile; - private final int position; - private final long expectedFileSize; - private final File partialFile; - - public BufferTask(DownloadFile downloadFile, int position) { - this.downloadFile = downloadFile; - this.position = position; - partialFile = downloadFile.getPartialFile(); - - // Calculate roughly how many bytes buffer length corresponds to. - int bitRate = downloadFile.getBitRate(); - long byteCount = Math.max(100000, bitRate * 1024 / 8 * downloadFile.getBufferLength()); - - // Find out how large the file should grow before resuming playback. - if (position == 0) { - expectedFileSize = byteCount; - } else { - expectedFileSize = partialFile.length() + byteCount; - } - } - - @Override - public void execute() { - setPlayerState(DOWNLOADING); - - while (!bufferComplete()) { - Util.sleepQuietly(1000L); - if (isCancelled()) { - return; - } - } - doPlay(downloadFile, position, true); - } - - private boolean bufferComplete() { - boolean completeFileAvailable = downloadFile.isCompleteFileAvailable(); - long size = partialFile.length(); - - Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")"); - return completeFileAvailable || size >= expectedFileSize; - } - - @Override - public String toString() { - return "BufferTask (" + downloadFile + ")"; - } - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.service; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.os.Handler; +import android.os.IBinder; +import android.os.PowerManager; +import android.util.DisplayMetrics; +import android.util.Log; +import android.widget.RemoteViews; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; +import net.sourceforge.subsonic.androidapp.audiofx.EqualizerController; +import net.sourceforge.subsonic.androidapp.audiofx.VisualizerController; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.domain.PlayerState; +import net.sourceforge.subsonic.androidapp.domain.RepeatMode; +import net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver; +import net.sourceforge.subsonic.androidapp.util.CancellableTask; +import net.sourceforge.subsonic.androidapp.util.LRUCache; +import net.sourceforge.subsonic.androidapp.util.ShufflePlayBuffer; +import net.sourceforge.subsonic.androidapp.util.SimpleServiceBinder; +import net.sourceforge.subsonic.androidapp.util.Util; +import net.sourceforge.subsonic.androidapp.util.RemoteControlHelper; +import net.sourceforge.subsonic.androidapp.util.RemoteControlClientCompat; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static net.sourceforge.subsonic.androidapp.domain.PlayerState.*; + +/** + * @author Sindre Mehus, Joshua Bahnsen + * @version $Id$ + */ +public class DownloadServiceImpl extends Service implements DownloadService { + + private static final String TAG = DownloadServiceImpl.class.getSimpleName(); + + public static final String CMD_PLAY = "net.sourceforge.subsonic.androidapp.CMD_PLAY"; + public static final String CMD_TOGGLEPAUSE = "net.sourceforge.subsonic.androidapp.CMD_TOGGLEPAUSE"; + public static final String CMD_PAUSE = "net.sourceforge.subsonic.androidapp.CMD_PAUSE"; + public static final String CMD_STOP = "net.sourceforge.subsonic.androidapp.CMD_STOP"; + public static final String CMD_PREVIOUS = "net.sourceforge.subsonic.androidapp.CMD_PREVIOUS"; + public static final String CMD_NEXT = "net.sourceforge.subsonic.androidapp.CMD_NEXT"; + + private final IBinder binder = new SimpleServiceBinder(this); + private MediaPlayer mediaPlayer; + private final List downloadList = new ArrayList(); + private final Handler handler = new Handler(); + private final DownloadServiceLifecycleSupport lifecycleSupport = new DownloadServiceLifecycleSupport(this); + private final ShufflePlayBuffer shufflePlayBuffer = new ShufflePlayBuffer(this); + + private final LRUCache downloadFileCache = new LRUCache(100); + private final List cleanupCandidates = new ArrayList(); + private final Scrobbler scrobbler = new Scrobbler(); + private final JukeboxService jukeboxService = new JukeboxService(this); + private Notification notification = new Notification(R.drawable.ic_stat_subsonic, null, System.currentTimeMillis()); + + private DownloadFile currentPlaying; + private DownloadFile currentDownloading; + private CancellableTask bufferTask; + private PlayerState playerState = IDLE; + private boolean shufflePlay; + private long revision; + private static DownloadService instance; + private String suggestedPlaylistName; + private PowerManager.WakeLock wakeLock; + private boolean keepScreenOn = false; + + private static boolean equalizerAvailable; + private static boolean visualizerAvailable; + private EqualizerController equalizerController; + private VisualizerController visualizerController; + private boolean showVisualization; + private boolean jukeboxEnabled; + + private static MusicDirectory.Entry currentSong; + + RemoteControlClientCompat remoteControlClientCompat; + + static { + try { + EqualizerController.checkAvailable(); + equalizerAvailable = true; + } catch (Throwable t) { + equalizerAvailable = false; + } + } + static { + try { + VisualizerController.checkAvailable(); + visualizerAvailable = true; + } catch (Throwable t) { + visualizerAvailable = false; + } + } + + private OnAudioFocusChangeListener _afChangeListener = new OnAudioFocusChangeListener() { + public void onAudioFocusChange(int focusChange) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + pause(); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + if (playerState == PlayerState.STARTED) { + start(); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { + stop(); + } + } + }; + + @Override + public void onCreate() { + super.onCreate(); + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK); + + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mediaPlayer, int what, int more) { + handleError(new Exception("MediaPlayer error: " + what + " (" + more + ")")); + return false; + } + }); + + notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; + notification.contentView = new RemoteViews(this.getPackageName(), R.layout.notification); + Intent notificationIntent = new Intent(this, DownloadActivity.class); + notification.contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); + Util.linkButtons(this, notification.contentView, false); + + if (equalizerAvailable) { + equalizerController = new EqualizerController(this, mediaPlayer); + if (!equalizerController.isAvailable()) { + equalizerController = null; + } else { + equalizerController.loadSettings(); + } + } + if (visualizerAvailable) { + visualizerController = new VisualizerController(this, mediaPlayer); + if (!visualizerController.isAvailable()) { + visualizerController = null; + } + } + + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName()); + wakeLock.setReferenceCounted(false); + + instance = this; + lifecycleSupport.onCreate(); + } + + @Override + public void onStart(Intent intent, int startId) { + super.onStart(intent, startId); + lifecycleSupport.onStart(intent); + } + + @Override + public void onDestroy() { + super.onDestroy(); + lifecycleSupport.onDestroy(); + mediaPlayer.release(); + shufflePlayBuffer.shutdown(); + if (equalizerController != null) { + equalizerController.release(); + } + if (visualizerController != null) { + visualizerController.release(); + } + + AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + RemoteControlHelper.unregisterRemoteControlClient(audioManager, remoteControlClientCompat); + notification = null; + instance = null; + } + + public static DownloadService getInstance() { + return instance; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public synchronized void download(List songs, boolean save, boolean autoplay, boolean playNext) { + shufflePlay = false; + int offset = 1; + + if (songs.isEmpty()) { + return; + } + if (playNext) { + if (autoplay && getCurrentPlayingIndex() >= 0) { + offset = 0; + } + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + downloadList.add(getCurrentPlayingIndex() + offset, downloadFile); + offset++; + } + revision++; + } else { + for (MusicDirectory.Entry song : songs) { + DownloadFile downloadFile = new DownloadFile(this, song, save); + downloadList.add(downloadFile); + } + revision++; + } + updateJukeboxPlaylist(); + + if (autoplay) { + play(0); + } else { + if (currentPlaying == null) { + currentPlaying = downloadList.get(0); + } + checkDownloads(); + } + lifecycleSupport.serializeDownloadQueue(); + } + + private void updateJukeboxPlaylist() { + if (jukeboxEnabled) { + jukeboxService.updatePlaylist(); + } + } + + public void restore(List songs, int currentPlayingIndex, int currentPlayingPosition) { + download(songs, false, false, false); + if (currentPlayingIndex != -1) { + play(currentPlayingIndex, false); + if (currentPlaying.isCompleteFileAvailable()) { + doPlay(currentPlaying, currentPlayingPosition, false); + } + } + } + + @Override + public synchronized void setShufflePlayEnabled(boolean enabled) { + if (shufflePlay == enabled) { + return; + } + + shufflePlay = enabled; + if (shufflePlay) { + clear(); + checkDownloads(); + } + } + + @Override + public synchronized boolean isShufflePlayEnabled() { + return shufflePlay; + } + + @Override + public synchronized void shuffle() { + Collections.shuffle(downloadList); + if (currentPlaying != null) { + downloadList.remove(getCurrentPlayingIndex()); + downloadList.add(0, currentPlaying); + } + revision++; + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + } + + @Override + public RepeatMode getRepeatMode() { + return Util.getRepeatMode(this); + } + + @Override + public void setRepeatMode(RepeatMode repeatMode) { + Util.setRepeatMode(this, repeatMode); + } + + @Override + public boolean getKeepScreenOn() { + return keepScreenOn; + } + + @Override + public void setKeepScreenOn(boolean keepScreenOn) { + this.keepScreenOn = keepScreenOn; + } + + @Override + public boolean getShowVisualization() { + return showVisualization; + } + + @Override + public void setShowVisualization(boolean showVisualization) { + this.showVisualization = showVisualization; + } + + @Override + public synchronized DownloadFile forSong(MusicDirectory.Entry song) { + for (DownloadFile downloadFile : downloadList) { + if (downloadFile.getSong().equals(song)) { + return downloadFile; + } + } + + DownloadFile downloadFile = downloadFileCache.get(song); + if (downloadFile == null) { + downloadFile = new DownloadFile(this, song, false); + downloadFileCache.put(song, downloadFile); + } + return downloadFile; + } + + @Override + public synchronized void clear() { + clear(true); + } + + @Override + public synchronized void clearIncomplete() { + reset(); + Iterator iterator = downloadList.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (!downloadFile.isCompleteFileAvailable()) { + iterator.remove(); + } + } + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + } + + @Override + public synchronized int size() { + return downloadList.size(); + } + + public synchronized void clear(boolean serialize) { + reset(); + downloadList.clear(); + revision++; + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + setCurrentPlaying(null, false); + + if (serialize) { + lifecycleSupport.serializeDownloadQueue(); + } + updateJukeboxPlaylist(); + } + + @Override + public synchronized void remove(DownloadFile downloadFile) { + if (downloadFile == currentDownloading) { + currentDownloading.cancelDownload(); + currentDownloading = null; + } + if (downloadFile == currentPlaying) { + reset(); + setCurrentPlaying(null, false); + } + downloadList.remove(downloadFile); + revision++; + lifecycleSupport.serializeDownloadQueue(); + updateJukeboxPlaylist(); + } + + @Override + public synchronized void delete(List songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).delete(); + } + } + + @Override + public synchronized void unpin(List songs) { + for (MusicDirectory.Entry song : songs) { + forSong(song).unpin(); + } + } + + synchronized void setCurrentPlaying(int currentPlayingIndex, boolean showNotification) { + try { + setCurrentPlaying(downloadList.get(currentPlayingIndex), showNotification); + } catch (IndexOutOfBoundsException x) { + // Ignored + } + } + + synchronized void setCurrentPlaying(DownloadFile currentPlaying, boolean showNotification) { + this.currentPlaying = currentPlaying; + + if (currentPlaying != null) { + Util.broadcastNewTrackInfo(this, currentPlaying.getSong()); + } else { + Util.broadcastNewTrackInfo(this, null); + } + + setRemoteControl(); + + if (Util.isNotificationEnabled(this) && currentPlaying != null && showNotification) { + Util.showPlayingNotification(this, this, handler, currentPlaying.getSong(), this.notification, this.playerState); + } else { + Util.hidePlayingNotification(this, this, handler); + } + } + + @Override + public synchronized int getCurrentPlayingIndex() { + return downloadList.indexOf(currentPlaying); + } + + @Override + public DownloadFile getCurrentPlaying() { + return currentPlaying; + } + + @Override + public DownloadFile getCurrentDownloading() { + return currentDownloading; + } + + @Override + public synchronized List getDownloads() { + return new ArrayList(downloadList); + } + + /** Plays either the current song (resume) or the first/next one in queue. */ + public synchronized void play() + { + int current = getCurrentPlayingIndex(); + if (current == -1) { + play(0); + } else { + play(current); + } + } + + @Override + public synchronized void play(int index) { + play(index, true); + } + + private synchronized void play(int index, boolean start) { + if (index < 0 || index >= size()) { + reset(); + setCurrentPlaying(null, false); + } else { + setCurrentPlaying(index, start); + checkDownloads(); + if (start) { + if (jukeboxEnabled) { + jukeboxService.skip(getCurrentPlayingIndex(), 0); + setPlayerState(STARTED); + } else { + bufferAndPlay(); + } + } + } + } + + /** Plays or resumes the playback, depending on the current player state. */ + public synchronized void togglePlayPause() + { + if (playerState == PAUSED || playerState == COMPLETED) { + start(); + } else if (playerState == STOPPED || playerState == IDLE) { + play(); + } else if (playerState == STARTED) { + pause(); + } + } + + @Override + public synchronized void seekTo(int position) { + try { + if (jukeboxEnabled) { + jukeboxService.skip(getCurrentPlayingIndex(), position / 1000); + } else { + mediaPlayer.seekTo(position); + } + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void previous() { + int index = getCurrentPlayingIndex(); + if (index == -1) { + return; + } + + // Restart song if played more than five seconds. + if (getPlayerPosition() > 5000 || index == 0) { + play(index); + } else { + play(index - 1); + } + } + + @Override + public synchronized void next() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + play(index + 1); + } + } + + private void onSongCompleted() { + int index = getCurrentPlayingIndex(); + if (index != -1) { + switch (getRepeatMode()) { + case OFF: + play(index + 1); + break; + case ALL: + play((index + 1) % size()); + break; + case SINGLE: + play(index); + break; + default: + break; + } + } + } + + @Override + public synchronized void pause() { + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(PAUSED); + } + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void stop() { + AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + audioManager.abandonAudioFocus(_afChangeListener); + + try { + if (playerState == STARTED) { + if (jukeboxEnabled) { + jukeboxService.stop(); + } else { + mediaPlayer.pause(); + } + setPlayerState(PAUSED); + } + } catch (Exception x) { + handleError(x); + } + + //seekTo(0); + } + + @Override + public synchronized void start() { + try { + if (jukeboxEnabled) { + jukeboxService.start(); + } else { + mediaPlayer.start(); + } + setPlayerState(STARTED); + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized void reset() { + if (bufferTask != null) { + bufferTask.cancel(); + } + try { + mediaPlayer.reset(); + setPlayerState(IDLE); + } catch (Exception x) { + handleError(x); + } + } + + @Override + public synchronized int getPlayerPosition() { + try { + if (playerState == IDLE || playerState == DOWNLOADING || playerState == PREPARING) { + return 0; + } + if (jukeboxEnabled) { + return jukeboxService.getPositionSeconds() * 1000; + } else { + return mediaPlayer.getCurrentPosition(); + } + } catch (Exception x) { + handleError(x); + return 0; + } + } + + @Override + public synchronized int getPlayerDuration() { + if (currentPlaying != null) { + Integer duration = currentPlaying.getSong().getDuration(); + if (duration != null) { + return duration * 1000; + } + } + if (playerState != IDLE && playerState != DOWNLOADING && playerState != PlayerState.PREPARING) { + try { + return mediaPlayer.getDuration(); + } catch (Exception x) { + handleError(x); + } + } + return 0; + } + + @Override + public PlayerState getPlayerState() { + return playerState; + } + + synchronized void setPlayerState(PlayerState playerState) { + Log.i(TAG, this.playerState.name() + " -> " + playerState.name() + " (" + currentPlaying + ")"); + + if (playerState == PAUSED) { + lifecycleSupport.serializeDownloadQueue(); + } + + boolean show = playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED; + boolean hide = playerState == PlayerState.IDLE || playerState == PlayerState.STOPPED; + Util.broadcastPlaybackStatusChange(this, playerState); + + this.playerState = playerState; + + setRemoteControl(); + + if (Util.isNotificationEnabled(this)) { + if (show) { + Util.showPlayingNotification(this, this, handler, currentPlaying.getSong(), this.notification, this.playerState); + } else if (hide) { + Util.hidePlayingNotification(this, this, handler); + } + } else { + Util.hidePlayingNotification(this, this, handler); + } + + if (playerState == STARTED) { + scrobbler.scrobble(this, currentPlaying, false); + } else if (playerState == COMPLETED) { + scrobbler.scrobble(this, currentPlaying, true); + } + } + + @Override + public void setSuggestedPlaylistName(String name) { + this.suggestedPlaylistName = name; + } + + @Override + public String getSuggestedPlaylistName() { + return suggestedPlaylistName; + } + + @Override + public EqualizerController getEqualizerController() { + return equalizerController; + } + + @Override + public VisualizerController getVisualizerController() { + return visualizerController; + } + + @Override + public boolean isJukeboxEnabled() { + return jukeboxEnabled; + } + + @Override + public void setJukeboxEnabled(boolean jukeboxEnabled) { + this.jukeboxEnabled = jukeboxEnabled; + jukeboxService.setEnabled(jukeboxEnabled); + if (jukeboxEnabled) { + reset(); + + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + } + } + + @Override + public void adjustJukeboxVolume(boolean up) { + jukeboxService.adjustVolume(up); + } + + private void setRemoteControl() { + if (Util.isLockScreenEnabled(this)) { + AudioManager audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + audioManager.requestAudioFocus(_afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + + if (remoteControlClientCompat == null) { + audioManager = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setComponent(new ComponentName(this.getPackageName(), MediaButtonIntentReceiver.class.getName())); + remoteControlClientCompat = new RemoteControlClientCompat(PendingIntent.getBroadcast(this, 0, intent, 0)); + RemoteControlHelper.registerRemoteControlClient(audioManager, remoteControlClientCompat); + } + + switch (playerState) + { + case STARTED: + remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case IDLE: + case STOPPED: + remoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + } + + remoteControlClientCompat.setTransportControlFlags( + RemoteControlClient.FLAG_KEY_MEDIA_PLAY | + RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_NEXT | + RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | + RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE | + RemoteControlClient.FLAG_KEY_MEDIA_STOP); + + try { + if (currentPlaying != null) { + if (currentSong != currentPlaying.getSong()) { + currentSong = currentPlaying.getSong(); + + String album = currentPlaying.getSong().getAlbum(); + String title = currentPlaying.getSong().getArtist() + " - " + currentPlaying.getSong().getTitle(); + Integer duration = currentPlaying.getSong().getDuration(); + + MusicService musicService = MusicServiceFactory.getMusicService(this); + DisplayMetrics metrics = this.getResources().getDisplayMetrics(); + int size = Math.min(metrics.widthPixels, metrics.heightPixels); + Bitmap bitmap = musicService.getCoverArt(this, currentPlaying.getSong(), size, true, null); + + // Update the remote controls + remoteControlClientCompat + .editMetadata(true) + .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title) + .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, album) + .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) + .putBitmap(RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, bitmap) + .apply(); + } + } + } + catch (Exception e) { + Log.e(TAG, "Exception in setRemoteControl", e); + } + } + } + + private synchronized void bufferAndPlay() { + reset(); + + bufferTask = new BufferTask(currentPlaying, 0); + bufferTask.start(); + } + + private synchronized void doPlay(final DownloadFile downloadFile, int position, boolean start) { + try { + final File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile(); + downloadFile.updateModificationDate(); + mediaPlayer.setOnCompletionListener(null); + mediaPlayer.reset(); + setPlayerState(IDLE); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setDataSource(file.getPath()); + setPlayerState(PREPARING); + mediaPlayer.prepare(); + setPlayerState(PREPARED); + + mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + + // Acquire a temporary wakelock, since when we return from + // this callback the MediaPlayer will release its wakelock + // and allow the device to go to sleep. + wakeLock.acquire(60000); + + setPlayerState(COMPLETED); + + // If COMPLETED and not playing partial file, we are *really" finished + // with the song and can move on to the next. + if (!file.equals(downloadFile.getPartialFile())) { + onSongCompleted(); + return; + } + + // If file is not completely downloaded, restart the playback from the current position. + int pos = mediaPlayer.getCurrentPosition(); + synchronized (DownloadServiceImpl.this) { + + // Work-around for apparent bug on certain phones: If close (less than ten seconds) to the end + // of the song, skip to the next rather than restarting it. + Integer duration = downloadFile.getSong().getDuration() == null ? null : downloadFile.getSong().getDuration() * 1000; + if (duration != null) { + if (Math.abs(duration - pos) < 10000) { + Log.i(TAG, "Skipping restart from " + pos + " of " + duration); + onSongCompleted(); + return; + } + } + + Log.i(TAG, "Requesting restart from " + pos + " of " + duration); + reset(); + bufferTask = new BufferTask(downloadFile, pos); + bufferTask.start(); + } + } + }); + + if (position != 0) { + Log.i(TAG, "Restarting player from position " + position); + mediaPlayer.seekTo(position); + } + + if (start) { + mediaPlayer.start(); + setPlayerState(STARTED); + } else { + setPlayerState(PAUSED); + } + lifecycleSupport.serializeDownloadQueue(); + + } catch (Exception x) { + handleError(x); + } + } + + private void handleError(Exception x) { + Log.w(TAG, "Media player error: " + x, x); + mediaPlayer.reset(); + setPlayerState(IDLE); + } + + protected synchronized void checkDownloads() { + + if (!Util.isExternalStoragePresent() || !lifecycleSupport.isExternalStorageAvailable()) { + return; + } + + if (shufflePlay) { + checkShufflePlay(); + } + + if (jukeboxEnabled || !Util.isNetworkConnected(this)) { + return; + } + + if (downloadList.isEmpty()) { + return; + } + + // Need to download current playing? + if (currentPlaying != null && + currentPlaying != currentDownloading && + !currentPlaying.isCompleteFileAvailable()) { + + // Cancel current download, if necessary. + if (currentDownloading != null) { + currentDownloading.cancelDownload(); + } + + currentDownloading = currentPlaying; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + } + + // Find a suitable target for download. + else if (currentDownloading == null || currentDownloading.isWorkDone() || currentDownloading.isFailed()) { + + int n = size(); + if (n == 0) { + return; + } + + int preloaded = 0; + + int start = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + int i = start; + do { + DownloadFile downloadFile = downloadList.get(i); + if (!downloadFile.isWorkDone()) { + if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount(this)) { + currentDownloading = downloadFile; + currentDownloading.download(); + cleanupCandidates.add(currentDownloading); + break; + } + } else if (currentPlaying != downloadFile) { + preloaded++; + } + + i = (i + 1) % n; + } while (i != start); + } + + // Delete obsolete .partial and .complete files. + cleanup(); + } + + private synchronized void checkShufflePlay() { + + final int listSize = 20; + boolean wasEmpty = downloadList.isEmpty(); + + long revisionBefore = revision; + + // First, ensure that list is at least 20 songs long. + int size = size(); + if (size < listSize) { + for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size)) { + DownloadFile downloadFile = new DownloadFile(this, song, false); + downloadList.add(downloadFile); + revision++; + } + } + + int currIndex = currentPlaying == null ? 0 : getCurrentPlayingIndex(); + + // Only shift playlist if playing song #5 or later. + if (currIndex > 4) { + int songsToShift = currIndex - 2; + for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift)) { + downloadList.add(new DownloadFile(this, song, false)); + downloadList.get(0).cancelDownload(); + downloadList.remove(0); + revision++; + } + } + + if (revisionBefore != revision) { + updateJukeboxPlaylist(); + } + + if (wasEmpty && !downloadList.isEmpty()) { + play(0); + } + } + + public long getDownloadListUpdateRevision() { + return revision; + } + + private synchronized void cleanup() { + Iterator iterator = cleanupCandidates.iterator(); + while (iterator.hasNext()) { + DownloadFile downloadFile = iterator.next(); + if (downloadFile != currentPlaying && downloadFile != currentDownloading) { + if (downloadFile.cleanup()) { + iterator.remove(); + } + } + } + } + + private class BufferTask extends CancellableTask { + + private final DownloadFile downloadFile; + private final int position; + private final long expectedFileSize; + private final File partialFile; + + public BufferTask(DownloadFile downloadFile, int position) { + this.downloadFile = downloadFile; + this.position = position; + partialFile = downloadFile.getPartialFile(); + + // Calculate roughly how many bytes buffer length corresponds to. + int bitRate = downloadFile.getBitRate(); + long byteCount = Math.max(100000, bitRate * 1024 / 8 * downloadFile.getBufferLength()); + + // Find out how large the file should grow before resuming playback. + if (position == 0) { + expectedFileSize = byteCount; + } else { + expectedFileSize = partialFile.length() + byteCount; + } + } + + @Override + public void execute() { + setPlayerState(DOWNLOADING); + + while (!bufferComplete()) { + Util.sleepQuietly(1000L); + if (isCancelled()) { + return; + } + } + doPlay(downloadFile, position, true); + } + + private boolean bufferComplete() { + boolean completeFileAvailable = downloadFile.isCompleteFileAvailable(); + long size = partialFile.length(); + + Log.i(TAG, "Buffering " + partialFile + " (" + size + "/" + expectedFileSize + ", " + completeFileAvailable + ")"); + return completeFileAvailable || size >= expectedFileSize; + } + + @Override + public String toString() { + return "BufferTask (" + downloadFile + ")"; + } + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java b/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java index 989c40ad..11893d5a 100644 --- a/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java +++ b/src/net/sourceforge/subsonic/androidapp/service/OfflineMusicService.java @@ -1,259 +1,261 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.service; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; -import java.util.Set; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import net.sourceforge.subsonic.androidapp.domain.Artist; -import net.sourceforge.subsonic.androidapp.domain.Indexes; -import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus; -import net.sourceforge.subsonic.androidapp.domain.Lyrics; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.domain.MusicFolder; -import net.sourceforge.subsonic.androidapp.domain.Playlist; -import net.sourceforge.subsonic.androidapp.domain.SearchCritera; -import net.sourceforge.subsonic.androidapp.domain.SearchResult; -import net.sourceforge.subsonic.androidapp.util.Constants; -import net.sourceforge.subsonic.androidapp.util.FileUtil; -import net.sourceforge.subsonic.androidapp.util.ProgressListener; -import net.sourceforge.subsonic.androidapp.util.Util; - -/** - * @author Sindre Mehus - */ -public class OfflineMusicService extends RESTMusicService { - - @Override - public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { - return true; - } - - @Override - public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { - List artists = new ArrayList(); - File root = FileUtil.getMusicDirectory(context); - for (File file : FileUtil.listFiles(root)) { - if (file.isDirectory()) { - Artist artist = new Artist(); - artist.setId(file.getPath()); - artist.setIndex(file.getName().substring(0, 1)); - artist.setName(file.getName()); - artists.add(artist); - } - } - return new Indexes(0L, Collections.emptyList(), artists); - } - - @Override - public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { - File dir = new File(id); - MusicDirectory result = new MusicDirectory(); - result.setName(dir.getName()); - - Set names = new HashSet(); - - for (File file : FileUtil.listMusicFiles(dir)) { - String name = getName(file); - if (name != null & !names.contains(name)) { - names.add(name); - result.addChild(createEntry(context, file, name)); - } - } - return result; - } - - private String getName(File file) { - String name = file.getName(); - if (file.isDirectory()) { - return name; - } - - if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) { - return null; - } - - name = name.replace(".complete", ""); - return FileUtil.getBaseName(name); - } - - private MusicDirectory.Entry createEntry(Context context, File file, String name) { - MusicDirectory.Entry entry = new MusicDirectory.Entry(); - entry.setDirectory(file.isDirectory()); - entry.setId(file.getPath()); - entry.setParent(file.getParent()); - entry.setSize(file.length()); - String root = FileUtil.getMusicDirectory(context).getPath(); - entry.setPath(file.getPath().replaceFirst("^" + root + "/" , "")); - if (file.isFile()) { - entry.setArtist(file.getParentFile().getParentFile().getName()); - entry.setAlbum(file.getParentFile().getName()); - } - entry.setTitle(name); - entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", ""))); - - File albumArt = FileUtil.getAlbumArtFile(context, entry); - if (albumArt.exists()) { - entry.setCoverArt(albumArt.getPath()); - } - return entry; - } - - @Override - public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception { - InputStream in = new FileInputStream(entry.getCoverArt()); - try { - byte[] bytes = Util.toByteArray(in); - Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); - return Bitmap.createScaledBitmap(bitmap, size, size, true); - } finally { - Util.close(in); - } - } - - @Override - public void star(String id, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Star not available in offline mode"); - } - - @Override - public void unstar(String id, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("UnStar not available in offline mode"); - } - - @Override - public List getMusicFolders(Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Music folders not available in offline mode"); - } - - @Override - public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Search not available in offline mode"); - } - - @Override - public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Playlists not available in offline mode"); - } - - @Override - public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Playlists not available in offline mode"); - } - - @Override - public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Playlists not available in offline mode"); - } - - @Override - public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Lyrics not available in offline mode"); - } - - @Override - public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Scrobbling not available in offline mode"); - } - - @Override - public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Album lists not available in offline mode"); - } - - @Override - public String getVideoUrl(Context context, String id) { - return null; - } - - @Override - public JukeboxStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Jukebox not available in offline mode"); - } - - @Override - public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Jukebox not available in offline mode"); - } - - @Override - public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Jukebox not available in offline mode"); - } - - @Override - public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Jukebox not available in offline mode"); - } - - @Override - public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Jukebox not available in offline mode"); - } - - @Override - public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Jukebox not available in offline mode"); - } - - @Override - public SearchResult getStarred(Context context, ProgressListener progressListener) throws Exception { - throw new OfflineException("Starred not available in offline mode"); - } - - @Override - public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { - File root = FileUtil.getMusicDirectory(context); - List children = new LinkedList(); - listFilesRecursively(root, children); - MusicDirectory result = new MusicDirectory(); - - if (children.isEmpty()) { - return result; - } - Random random = new Random(); - for (int i = 0; i < size; i++) { - File file = children.get(random.nextInt(children.size())); - result.addChild(createEntry(context, file, getName(file))); - } - - return result; - } - - private void listFilesRecursively(File parent, List children) { - for (File file : FileUtil.listMusicFiles(parent)) { - if (file.isFile()) { - children.add(file); - } else { - listFilesRecursively(file, children); - } - } - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import net.sourceforge.subsonic.androidapp.domain.Artist; +import net.sourceforge.subsonic.androidapp.domain.Indexes; +import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus; +import net.sourceforge.subsonic.androidapp.domain.Lyrics; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.domain.MusicFolder; +import net.sourceforge.subsonic.androidapp.domain.Playlist; +import net.sourceforge.subsonic.androidapp.domain.SearchCritera; +import net.sourceforge.subsonic.androidapp.domain.SearchResult; +import net.sourceforge.subsonic.androidapp.util.Constants; +import net.sourceforge.subsonic.androidapp.util.FileUtil; +import net.sourceforge.subsonic.androidapp.util.ProgressListener; +import net.sourceforge.subsonic.androidapp.util.Util; + +/** + * @author Sindre Mehus + */ +public class OfflineMusicService extends RESTMusicService { + + @Override + public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { + return true; + } + + @Override + public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + List artists = new ArrayList(); + File root = FileUtil.getMusicDirectory(context); + for (File file : FileUtil.listFiles(root)) { + if (file.isDirectory()) { + Artist artist = new Artist(); + artist.setId(file.getPath()); + artist.setIndex(file.getName().substring(0, 1)); + artist.setName(file.getName()); + artists.add(artist); + } + } + return new Indexes(0L, Collections.emptyList(), artists); + } + + @Override + public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { + File dir = new File(id); + MusicDirectory result = new MusicDirectory(); + result.setName(dir.getName()); + + Set names = new HashSet(); + + for (File file : FileUtil.listMusicFiles(dir)) { + String name = getName(file); + if (name != null & !names.contains(name)) { + names.add(name); + result.addChild(createEntry(context, file, name)); + } + } + return result; + } + + private String getName(File file) { + String name = file.getName(); + if (file.isDirectory()) { + return name; + } + + if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) { + return null; + } + + name = name.replace(".complete", ""); + return FileUtil.getBaseName(name); + } + + private MusicDirectory.Entry createEntry(Context context, File file, String name) { + MusicDirectory.Entry entry = new MusicDirectory.Entry(); + entry.setDirectory(file.isDirectory()); + entry.setId(file.getPath()); + entry.setParent(file.getParent()); + entry.setSize(file.length()); + String root = FileUtil.getMusicDirectory(context).getPath(); + entry.setPath(file.getPath().replaceFirst("^" + root + "/" , "")); + if (file.isFile()) { + entry.setArtist(file.getParentFile().getParentFile().getName()); + entry.setAlbum(file.getParentFile().getName()); + } + entry.setTitle(name); + entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", ""))); + + File albumArt = FileUtil.getAlbumArtFile(context, entry); + if (albumArt.exists()) { + entry.setCoverArt(albumArt.getPath()); + } + return entry; + } + + @Override + public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception { + InputStream in = new FileInputStream(entry.getCoverArt()); + try { + byte[] bytes = Util.toByteArray(in); + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + Log.i("getCoverArt", "getCoverArt"); + return Bitmap.createScaledBitmap(bitmap, size, size, true); + } finally { + Util.close(in); + } + } + + @Override + public void star(String id, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Star not available in offline mode"); + } + + @Override + public void unstar(String id, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("UnStar not available in offline mode"); + } + + @Override + public List getMusicFolders(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Music folders not available in offline mode"); + } + + @Override + public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Search not available in offline mode"); + } + + @Override + public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Playlists not available in offline mode"); + } + + @Override + public MusicDirectory getPlaylist(String id, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Playlists not available in offline mode"); + } + + @Override + public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Playlists not available in offline mode"); + } + + @Override + public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Lyrics not available in offline mode"); + } + + @Override + public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Scrobbling not available in offline mode"); + } + + @Override + public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Album lists not available in offline mode"); + } + + @Override + public String getVideoUrl(Context context, String id) { + return null; + } + + @Override + public JukeboxStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Jukebox not available in offline mode"); + } + + @Override + public SearchResult getStarred(Context context, ProgressListener progressListener) throws Exception { + throw new OfflineException("Starred not available in offline mode"); + } + + @Override + public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception { + File root = FileUtil.getMusicDirectory(context); + List children = new LinkedList(); + listFilesRecursively(root, children); + MusicDirectory result = new MusicDirectory(); + + if (children.isEmpty()) { + return result; + } + Random random = new Random(); + for (int i = 0; i < size; i++) { + File file = children.get(random.nextInt(children.size())); + result.addChild(createEntry(context, file, getName(file))); + } + + return result; + } + + private void listFilesRecursively(File parent, List children) { + for (File file : FileUtil.listMusicFiles(parent)) { + if (file.isFile()) { + children.add(file); + } else { + listFilesRecursively(file, children); + } + } + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/util/AlbumView.java b/src/net/sourceforge/subsonic/androidapp/util/AlbumView.java index b7eaf158..ebd2223a 100644 --- a/src/net/sourceforge/subsonic/androidapp/util/AlbumView.java +++ b/src/net/sourceforge/subsonic/androidapp/util/AlbumView.java @@ -1,93 +1,95 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.util; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.service.MusicService; -import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; - -/** - * Used to display albums in a {@code ListView}. - * - * @author Sindre Mehus - */ -public class AlbumView extends LinearLayout { - - private TextView titleView; - private TextView artistView; - private View coverArtView; - private ImageView starImageView; - - public AlbumView(Context context) { - super(context); - LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true); - - titleView = (TextView) findViewById(R.id.album_title); - artistView = (TextView) findViewById(R.id.album_artist); - coverArtView = findViewById(R.id.album_coverart); - starImageView = (ImageView) findViewById(R.id.album_star); - } - - public void setAlbum(final MusicDirectory.Entry album, ImageLoader imageLoader) { - titleView.setText(album.getTitle()); - artistView.setText(album.getArtist()); - artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE); - starImageView.setImageDrawable(album.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow)); - imageLoader.loadImage(coverArtView, album, false, true); - - starImageView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - final boolean isStarred = album.getStarred(); - final String id = album.getId(); - - if (!isStarred) { - starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); - album.setStarred(true); - } else { - starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); - album.setStarred(false); - } - - new Thread(new Runnable() { - public void run() { - MusicService musicService = MusicServiceFactory.getMusicService(null); - - try { - if (!isStarred) { - musicService.star(id, getContext(), null); - } else { - musicService.unstar(id, getContext(), null); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - }).start(); - } - }); - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.util; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.service.MusicService; +import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; + +/** + * Used to display albums in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class AlbumView extends LinearLayout { + + private static final String TAG = AlbumView.class.getSimpleName(); + private TextView titleView; + private TextView artistView; + private View coverArtView; + private ImageView starImageView; + + public AlbumView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true); + + titleView = (TextView) findViewById(R.id.album_title); + artistView = (TextView) findViewById(R.id.album_artist); + coverArtView = findViewById(R.id.album_coverart); + starImageView = (ImageView) findViewById(R.id.album_star); + } + + public void setAlbum(final MusicDirectory.Entry album, ImageLoader imageLoader) { + titleView.setText(album.getTitle()); + artistView.setText(album.getArtist()); + artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE); + starImageView.setImageDrawable(album.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow)); + imageLoader.loadImage(coverArtView, album, false, true); + + starImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final boolean isStarred = album.getStarred(); + final String id = album.getId(); + + if (!isStarred) { + starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); + album.setStarred(true); + } else { + starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); + album.setStarred(false); + } + + new Thread(new Runnable() { + public void run() { + MusicService musicService = MusicServiceFactory.getMusicService(null); + + try { + if (!isStarred) { + musicService.star(id, getContext(), null); + } else { + musicService.unstar(id, getContext(), null); + } + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } + }).start(); + } + }); + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/util/CacheCleaner.java b/src/net/sourceforge/subsonic/androidapp/util/CacheCleaner.java index 46459571..11e5e8de 100644 --- a/src/net/sourceforge/subsonic/androidapp/util/CacheCleaner.java +++ b/src/net/sourceforge/subsonic/androidapp/util/CacheCleaner.java @@ -1,6 +1,9 @@ package net.sourceforge.subsonic.androidapp.util; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -32,31 +35,33 @@ public class CacheCleaner { } public void clean() { + new Thread(new Runnable() { + public void run() { + Log.i(TAG, "Starting cache cleaning."); - Log.i(TAG, "Starting cache cleaning."); + if (downloadService == null) { + Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); + return; + } - if (downloadService == null) { - Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); - return; - } + try { + List files = new ArrayList(); + List dirs = new ArrayList(); - try { + findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs); + sortByAscendingModificationTime(files); - List files = new ArrayList(); - List dirs = new ArrayList(); + Set undeletable = findUndeletableFiles(); - findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs); - sortByAscendingModificationTime(files); + deleteFiles(files, undeletable); + deleteEmptyDirs(dirs, undeletable); + Log.i(TAG, "Completed cache cleaning."); - Set undeletable = findUndeletableFiles(); - - deleteFiles(files, undeletable); - deleteEmptyDirs(dirs, undeletable); - Log.i(TAG, "Completed cache cleaning."); - - } catch (RuntimeException x) { - Log.e(TAG, "Error in cache cleaning.", x); - } + } catch (RuntimeException x) { + Log.e(TAG, "Error in cache cleaning.", x); + } + } + }).start(); } private void deleteEmptyDirs(List dirs, Set undeletable) { @@ -68,7 +73,7 @@ public class CacheCleaner { File[] children = dir.listFiles(); // Delete empty directory and associated album artwork. - if (children.length == 0) { + if (children != null && children.length == 0) { Util.delete(dir); Util.delete(FileUtil.getAlbumArtFile(dir)); } @@ -88,25 +93,31 @@ public class CacheCleaner { bytesUsedBySubsonic += file.length(); } + long bytesToDelete = 0; + // Ensure that file system is not more than 95% full. - StatFs stat = new StatFs(files.get(0).getPath()); - long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); - long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); - long bytesUsedFs = bytesTotalFs - bytesAvailableFs; - long minFsAvailability = Math.round(MAX_FILE_SYSTEM_USAGE * (double) bytesTotalFs); + try + { + StatFs stat = new StatFs(files.get(0).getPath()); + long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); + long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); + long bytesUsedFs = bytesTotalFs - bytesAvailableFs; + long minFsAvailability = Math.round(MAX_FILE_SYSTEM_USAGE * (double) bytesTotalFs); - long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L); - long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L); - long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit); + long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L); + long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L); + 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)); + 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)); + } catch (Exception x) { + // + } long bytesDeleted = 0L; for (File file : files) { - if (file.getName().equals(Constants.ALBUM_ART_FILE)) { // Move artwork to new folder. file.renameTo(FileUtil.getAlbumArtFile(file.getParentFile())); diff --git a/src/net/sourceforge/subsonic/androidapp/util/FileUtil.java b/src/net/sourceforge/subsonic/androidapp/util/FileUtil.java index 65e69767..5934d3d9 100644 --- a/src/net/sourceforge/subsonic/androidapp/util/FileUtil.java +++ b/src/net/sourceforge/subsonic/androidapp/util/FileUtil.java @@ -1,302 +1,303 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.Arrays; -import java.util.Locale; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.Iterator; -import java.util.List; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Environment; -import android.util.Log; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; - -/** - * @author Sindre Mehus - */ -public class FileUtil { - - private static final String TAG = FileUtil.class.getSimpleName(); - private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">"}; - private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">"}; - private static final List MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma"); - private static final File DEFAULT_MUSIC_DIR = createDirectory("music"); - - public static File getSongFile(Context context, MusicDirectory.Entry song) { - File dir = getAlbumDirectory(context, song); - - StringBuilder fileName = new StringBuilder(); - Integer track = song.getTrack(); - if (track != null) { - if (track < 10) { - fileName.append("0"); - } - fileName.append(track).append("-"); - } - - fileName.append(fileSystemSafe(song.getTitle())).append("."); - - if (song.getTranscodedSuffix() != null) { - fileName.append(song.getTranscodedSuffix()); - } else { - fileName.append(song.getSuffix()); - } - - return new File(dir, fileName.toString()); - } - - public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) { - File albumDir = getAlbumDirectory(context, entry); - return getAlbumArtFile(albumDir); - } - - public static File getAlbumArtFile(File albumDir) { - File albumArtDir = getAlbumArtDirectory(); - return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg"); - } - - public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) { - File albumArtFile = getAlbumArtFile(context, entry); - if (albumArtFile.exists()) { - Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath()); - return bitmap == null ? null : Bitmap.createScaledBitmap(bitmap, size, size, true); - } - return null; - } - - public static File getAlbumArtDirectory() { - File albumArtDir = new File(getSubsonicDirectory(), "artwork"); - ensureDirectoryExistsAndIsReadWritable(albumArtDir); - ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia")); - return albumArtDir; - } - - private static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) { - File dir; - if (entry.getPath() != null) { - File f = new File(fileSystemSafeDir(entry.getPath())); - dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent())); - } else { - String artist = fileSystemSafe(entry.getArtist()); - String album = fileSystemSafe(entry.getAlbum()); - dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album); - } - return dir; - } - - public static void createDirectoryForParent(File file) { - File dir = file.getParentFile(); - if (!dir.exists()) { - if (!dir.mkdirs()) { - Log.e(TAG, "Failed to create directory " + dir); - } - } - } - - private static File createDirectory(String name) { - File dir = new File(getSubsonicDirectory(), name); - if (!dir.exists() && !dir.mkdirs()) { - Log.e(TAG, "Failed to create " + name); - } - return dir; - } - - public static File getSubsonicDirectory() { - return new File(Environment.getExternalStorageDirectory(), "subsonic"); - } - - public static File getDefaultMusicDirectory() { - return DEFAULT_MUSIC_DIR; - } - - public static File getMusicDirectory(Context context) { - String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath()); - File dir = new File(path); - return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory(); - } - - public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) { - if (dir == null) { - return false; - } - - if (dir.exists()) { - if (!dir.isDirectory()) { - Log.w(TAG, dir + " exists but is not a directory."); - return false; - } - } else { - if (dir.mkdirs()) { - Log.i(TAG, "Created directory " + dir); - } else { - Log.w(TAG, "Failed to create directory " + dir); - return false; - } - } - - if (!dir.canRead()) { - Log.w(TAG, "No read permission for directory " + dir); - return false; - } - - if (!dir.canWrite()) { - Log.w(TAG, "No write permission for directory " + dir); - return false; - } - return true; - } - - /** - * Makes a given filename safe by replacing special characters like slashes ("/" and "\") - * with dashes ("-"). - * - * @param filename The filename in question. - * @return The filename with special characters replaced by hyphens. - */ - private static String fileSystemSafe(String filename) { - if (filename == null || filename.trim().length() == 0) { - return "unnamed"; - } - - for (String s : FILE_SYSTEM_UNSAFE) { - filename = filename.replace(s, "-"); - } - return filename; - } - - /** - * Makes a given filename safe by replacing special characters like colons (":") - * with dashes ("-"). - * - * @param path The path of the directory in question. - * @return The the directory name with special characters replaced by hyphens. - */ - private static String fileSystemSafeDir(String path) { - if (path == null || path.trim().length() == 0) { - return ""; - } - - for (String s : FILE_SYSTEM_UNSAFE_DIR) { - path = path.replace(s, "-"); - } - return path; - } - - /** - * Similar to {@link File#listFiles()}, but returns a sorted set. - * Never returns {@code null}, instead a warning is logged, and an empty set is returned. - */ - public static SortedSet listFiles(File dir) { - File[] files = dir.listFiles(); - if (files == null) { - Log.w(TAG, "Failed to list children for " + dir.getPath()); - return new TreeSet(); - } - - return new TreeSet(Arrays.asList(files)); - } - - public static SortedSet listMusicFiles(File dir) { - SortedSet files = listFiles(dir); - Iterator iterator = files.iterator(); - while (iterator.hasNext()) { - File file = iterator.next(); - if (!file.isDirectory() && !isMusicFile(file)) { - iterator.remove(); - } - } - return files; - } - - private static boolean isMusicFile(File file) { - String extension = getExtension(file.getName()); - return MUSIC_FILE_EXTENSIONS.contains(extension); - } - - /** - * Returns the extension (the substring after the last dot) of the given file. The dot - * is not included in the returned extension. - * - * @param name The filename in question. - * @return The extension, or an empty string if no extension is found. - */ - public static String getExtension(String name) { - int index = name.lastIndexOf('.'); - return index == -1 ? "" : name.substring(index + 1).toLowerCase(Locale.getDefault()); - } - - /** - * 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 boolean serialize(Context context, T obj, String fileName) { - File file = new File(context.getCacheDir(), fileName); - ObjectOutputStream out = null; - try { - out = new ObjectOutputStream(new FileOutputStream(file)); - out.writeObject(obj); - Log.i(TAG, "Serialized object to " + file); - return true; - } catch (Throwable x) { - Log.w(TAG, "Failed to serialize object to " + file); - return false; - } finally { - Util.close(out); - } - } - - public static T deserialize(Context context, String fileName) { - File file = new File(context.getCacheDir(), fileName); - if (!file.exists() || !file.isFile()) { - return null; - } - - ObjectInputStream in = null; - try { - in = new ObjectInputStream(new FileInputStream(file)); - T result = (T)in.readObject(); - Log.i(TAG, "Deserialized object from " + file); - return result; - } catch (Throwable x) { - Log.w(TAG, "Failed to deserialize object from " + file, x); - return null; - } finally { - Util.close(in); - } - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Locale; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.Iterator; +import java.util.List; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.util.Log; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; + +/** + * @author Sindre Mehus + */ +public class FileUtil { + + private static final String TAG = FileUtil.class.getSimpleName(); + private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "<", ">"}; + private static final String[] FILE_SYSTEM_UNSAFE_DIR = {"\\", "..", ":", "\"", "?", "*", "<", ">"}; + private static final List MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma"); + private static final File DEFAULT_MUSIC_DIR = createDirectory("music"); + + public static File getSongFile(Context context, MusicDirectory.Entry song) { + File dir = getAlbumDirectory(context, song); + + StringBuilder fileName = new StringBuilder(); + Integer track = song.getTrack(); + if (track != null) { + if (track < 10) { + fileName.append("0"); + } + fileName.append(track).append("-"); + } + + fileName.append(fileSystemSafe(song.getTitle())).append("."); + + if (song.getTranscodedSuffix() != null) { + fileName.append(song.getTranscodedSuffix()); + } else { + fileName.append(song.getSuffix()); + } + + return new File(dir, fileName.toString()); + } + + public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) { + File albumDir = getAlbumDirectory(context, entry); + return getAlbumArtFile(albumDir); + } + + public static File getAlbumArtFile(File albumDir) { + File albumArtDir = getAlbumArtDirectory(); + return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg"); + } + + public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) { + File albumArtFile = getAlbumArtFile(context, entry); + if (albumArtFile.exists()) { + Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath()); + Log.i("getAlbumArtBitmap", String.valueOf(size)); + return bitmap == null ? null : Bitmap.createScaledBitmap(bitmap, size, size, true); + } + return null; + } + + public static File getAlbumArtDirectory() { + File albumArtDir = new File(getSubsonicDirectory(), "artwork"); + ensureDirectoryExistsAndIsReadWritable(albumArtDir); + ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia")); + return albumArtDir; + } + + private static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) { + File dir; + if (entry.getPath() != null) { + File f = new File(fileSystemSafeDir(entry.getPath())); + dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent())); + } else { + String artist = fileSystemSafe(entry.getArtist()); + String album = fileSystemSafe(entry.getAlbum()); + dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album); + } + return dir; + } + + public static void createDirectoryForParent(File file) { + File dir = file.getParentFile(); + if (!dir.exists()) { + if (!dir.mkdirs()) { + Log.e(TAG, "Failed to create directory " + dir); + } + } + } + + private static File createDirectory(String name) { + File dir = new File(getSubsonicDirectory(), name); + if (!dir.exists() && !dir.mkdirs()) { + Log.e(TAG, "Failed to create " + name); + } + return dir; + } + + public static File getSubsonicDirectory() { + return new File(Environment.getExternalStorageDirectory(), "subsonic"); + } + + public static File getDefaultMusicDirectory() { + return DEFAULT_MUSIC_DIR; + } + + public static File getMusicDirectory(Context context) { + String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath()); + File dir = new File(path); + return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory(); + } + + public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) { + if (dir == null) { + return false; + } + + if (dir.exists()) { + if (!dir.isDirectory()) { + Log.w(TAG, dir + " exists but is not a directory."); + return false; + } + } else { + if (dir.mkdirs()) { + Log.i(TAG, "Created directory " + dir); + } else { + Log.w(TAG, "Failed to create directory " + dir); + return false; + } + } + + if (!dir.canRead()) { + Log.w(TAG, "No read permission for directory " + dir); + return false; + } + + if (!dir.canWrite()) { + Log.w(TAG, "No write permission for directory " + dir); + return false; + } + return true; + } + + /** + * Makes a given filename safe by replacing special characters like slashes ("/" and "\") + * with dashes ("-"). + * + * @param filename The filename in question. + * @return The filename with special characters replaced by hyphens. + */ + private static String fileSystemSafe(String filename) { + if (filename == null || filename.trim().length() == 0) { + return "unnamed"; + } + + for (String s : FILE_SYSTEM_UNSAFE) { + filename = filename.replace(s, "-"); + } + return filename; + } + + /** + * Makes a given filename safe by replacing special characters like colons (":") + * with dashes ("-"). + * + * @param path The path of the directory in question. + * @return The the directory name with special characters replaced by hyphens. + */ + private static String fileSystemSafeDir(String path) { + if (path == null || path.trim().length() == 0) { + return ""; + } + + for (String s : FILE_SYSTEM_UNSAFE_DIR) { + path = path.replace(s, "-"); + } + return path; + } + + /** + * Similar to {@link File#listFiles()}, but returns a sorted set. + * Never returns {@code null}, instead a warning is logged, and an empty set is returned. + */ + public static SortedSet listFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + Log.w(TAG, "Failed to list children for " + dir.getPath()); + return new TreeSet(); + } + + return new TreeSet(Arrays.asList(files)); + } + + public static SortedSet listMusicFiles(File dir) { + SortedSet files = listFiles(dir); + Iterator iterator = files.iterator(); + while (iterator.hasNext()) { + File file = iterator.next(); + if (!file.isDirectory() && !isMusicFile(file)) { + iterator.remove(); + } + } + return files; + } + + private static boolean isMusicFile(File file) { + String extension = getExtension(file.getName()); + return MUSIC_FILE_EXTENSIONS.contains(extension); + } + + /** + * Returns the extension (the substring after the last dot) of the given file. The dot + * is not included in the returned extension. + * + * @param name The filename in question. + * @return The extension, or an empty string if no extension is found. + */ + public static String getExtension(String name) { + int index = name.lastIndexOf('.'); + return index == -1 ? "" : name.substring(index + 1).toLowerCase(Locale.getDefault()); + } + + /** + * 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 boolean serialize(Context context, T obj, String fileName) { + File file = new File(context.getCacheDir(), fileName); + ObjectOutputStream out = null; + try { + out = new ObjectOutputStream(new FileOutputStream(file)); + out.writeObject(obj); + Log.i(TAG, "Serialized object to " + file); + return true; + } catch (Throwable x) { + Log.w(TAG, "Failed to serialize object to " + file); + return false; + } finally { + Util.close(out); + } + } + + public static T deserialize(Context context, String fileName) { + File file = new File(context.getCacheDir(), fileName); + if (!file.exists() || !file.isFile()) { + return null; + } + + ObjectInputStream in = null; + try { + in = new ObjectInputStream(new FileInputStream(file)); + T result = (T)in.readObject(); + Log.i(TAG, "Deserialized object from " + file); + return result; + } catch (Throwable x) { + Log.w(TAG, "Failed to deserialize object from " + file, x); + return null; + } finally { + Util.close(in); + } + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/util/ImageLoader.java b/src/net/sourceforge/subsonic/androidapp/util/ImageLoader.java index 302b5033..9af71a2d 100644 --- a/src/net/sourceforge/subsonic/androidapp/util/ImageLoader.java +++ b/src/net/sourceforge/subsonic/androidapp/util/ImageLoader.java @@ -1,247 +1,249 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.util; - -import android.app.ActionBar; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.LinearGradient; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Shader; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.TransitionDrawable; -import android.os.Handler; -import android.os.Message; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.service.MusicService; -import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -/** - * Asynchronous loading of images, with caching. - *

- * There should normally be only one instance of this class. - * - * @author Sindre Mehus - */ -public class ImageLoader implements Runnable { - - private static final String TAG = ImageLoader.class.getSimpleName(); - private static final int CONCURRENCY = 5; - - private final LRUCache cache = new LRUCache(100); - private final BlockingQueue queue; - private final int imageSizeDefault; - private final int imageSizeLarge; - private Drawable largeUnknownImage; - private Drawable drawable; - - public ImageLoader(Context context) { - queue = new LinkedBlockingQueue(500); - - // Determine the density-dependent image sizes. - imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); - - for (int i = 0; i < CONCURRENCY; i++) { - new Thread(this, "ImageLoader").start(); - } - - createLargeUnknownImage(context); - } - - private void createLargeUnknownImage(Context context) { - BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); - Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true); - //bitmap = createReflection(bitmap); - largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap); - } - - public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) { - if (entry == null || entry.getCoverArt() == null) { - setUnknownImage(view, large); - return; - } - - int size = large ? imageSizeLarge : imageSizeDefault; - Drawable drawable = cache.get(getKey(entry.getCoverArt(), size)); - if (drawable != null) { - setImage(view, drawable, large); - return; - } - - if (!large) { - setUnknownImage(view, large); - } - queue.offer(new Task(view, entry, size, large, large, crossfade)); - } - - public void setActionBarArtwork(final View view, final MusicDirectory.Entry entry, final ActionBar ab) { - if (entry == null || entry.getCoverArt() == null) { - ab.setLogo(largeUnknownImage); - } - - final int size = imageSizeLarge; - drawable = cache.get(getKey(entry.getCoverArt(), size)); - - if (drawable != null) { - ab.setLogo(drawable); - } - - final Handler handler = new Handler(){ - @Override - public void handleMessage(Message msg) { - drawable = (Drawable) msg.obj; - ab.setLogo(drawable); - } - }; - - new Thread(new Runnable() { - public void run() { - MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); - - try - { - Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, true, null); - drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap); - Message msg = Message.obtain(); - msg.obj = drawable; - handler.sendMessage(msg); - cache.put(getKey(entry.getCoverArt(), size), drawable); - } catch (Throwable x) { - Log.e(TAG, "Failed to download album art.", x); - } - } - }).start(); - } - - private String getKey(String coverArtId, int size) { - return coverArtId + size; - } - - private void setImage(View view, Drawable drawable, boolean crossfade) { - if (view instanceof TextView) { - // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though. - TextView textView = (TextView) view; - textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - } else if (view instanceof ImageView) { - ImageView imageView = (ImageView) view; - if (crossfade) { - - Drawable existingDrawable = imageView.getDrawable(); - if (existingDrawable == null) { - Bitmap emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - existingDrawable = new BitmapDrawable(emptyImage); - } - - Drawable[] layers = new Drawable[]{existingDrawable, drawable}; - - TransitionDrawable transitionDrawable = new TransitionDrawable(layers); - imageView.setImageDrawable(transitionDrawable); - transitionDrawable.startTransition(250); - } else { - imageView.setImageDrawable(drawable); - } - } - } - - private void setUnknownImage(View view, boolean large) { - if (large) { - setImage(view, largeUnknownImage, false); - } else { - if (view instanceof TextView) { - ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); - } else if (view instanceof ImageView) { - ((ImageView) view).setImageResource(R.drawable.unknown_album); - } - } - } - - public void clear() { - queue.clear(); - } - - @Override - public void run() { - while (true) { - try { - Task task = queue.take(); - task.execute(); - } catch (Throwable x) { - Log.e(TAG, "Unexpected exception in ImageLoader.", x); - } - } - } - - private class Task { - private final View view; - private final MusicDirectory.Entry entry; - private final Handler handler; - private final int size; - private final boolean reflection; - private final boolean saveToFile; - private final boolean crossfade; - - public Task(View view, MusicDirectory.Entry entry, int size, boolean reflection, boolean saveToFile, boolean crossfade) { - this.view = view; - this.entry = entry; - this.size = size; - this.reflection = reflection; - this.saveToFile = saveToFile; - this.crossfade = crossfade; - handler = new Handler(); - } - - public void execute() { - try { - MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); - Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, null); - - if (reflection) { - //bitmap = createReflection(bitmap); - } - - final Drawable drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap); - cache.put(getKey(entry.getCoverArt(), size), drawable); - - handler.post(new Runnable() { - @Override - public void run() { - setImage(view, drawable, crossfade); - } - }); - } catch (Throwable x) { - Log.e(TAG, "Failed to download album art.", x); - } - } - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.util; + +import android.app.ActionBar; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Handler; +import android.os.Message; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.service.MusicService; +import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Asynchronous loading of images, with caching. + *

+ * There should normally be only one instance of this class. + * + * @author Sindre Mehus + */ +public class ImageLoader implements Runnable { + + private static final String TAG = ImageLoader.class.getSimpleName(); + private static final int CONCURRENCY = 5; + + private final LRUCache cache = new LRUCache(100); + private final BlockingQueue queue; + private final int imageSizeDefault; + private final int imageSizeLarge; + private Drawable largeUnknownImage; + private Drawable drawable; + + public ImageLoader(Context context) { + queue = new LinkedBlockingQueue(500); + + // Determine the density-dependent image sizes. + imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); + + for (int i = 0; i < CONCURRENCY; i++) { + new Thread(this, "ImageLoader").start(); + } + + createLargeUnknownImage(context); + } + + private void createLargeUnknownImage(Context context) { + BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); + Log.i(TAG, "createLargeUnknownImage"); + Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true); + + //bitmap = createReflection(bitmap); + largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap); + } + + public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) { + if (entry == null || entry.getCoverArt() == null) { + setUnknownImage(view, large); + return; + } + + int size = large ? imageSizeLarge : imageSizeDefault; + Drawable drawable = cache.get(getKey(entry.getCoverArt(), size)); + if (drawable != null) { + setImage(view, drawable, large); + return; + } + + if (!large) { + setUnknownImage(view, large); + } + queue.offer(new Task(view, entry, size, large, large, crossfade)); + } + + public void setActionBarArtwork(final View view, final MusicDirectory.Entry entry, final ActionBar ab) { + if (entry == null || entry.getCoverArt() == null) { + ab.setLogo(largeUnknownImage); + } + + final int size = imageSizeLarge; + drawable = cache.get(getKey(entry.getCoverArt(), size)); + + if (drawable != null) { + ab.setLogo(drawable); + } + + final Handler handler = new Handler(){ + @Override + public void handleMessage(Message msg) { + drawable = (Drawable) msg.obj; + ab.setLogo(drawable); + } + }; + + new Thread(new Runnable() { + public void run() { + MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); + + try + { + Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, true, null); + drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap); + Message msg = Message.obtain(); + msg.obj = drawable; + handler.sendMessage(msg); + cache.put(getKey(entry.getCoverArt(), size), drawable); + } catch (Throwable x) { + Log.e(TAG, "Failed to download album art.", x); + } + } + }).start(); + } + + private String getKey(String coverArtId, int size) { + return coverArtId + size; + } + + private void setImage(View view, Drawable drawable, boolean crossfade) { + if (view instanceof TextView) { + // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though. + TextView textView = (TextView) view; + textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + } else if (view instanceof ImageView) { + ImageView imageView = (ImageView) view; + if (crossfade) { + + Drawable existingDrawable = imageView.getDrawable(); + if (existingDrawable == null) { + Bitmap emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + existingDrawable = new BitmapDrawable(emptyImage); + } + + Drawable[] layers = new Drawable[]{existingDrawable, drawable}; + + TransitionDrawable transitionDrawable = new TransitionDrawable(layers); + imageView.setImageDrawable(transitionDrawable); + transitionDrawable.startTransition(250); + } else { + imageView.setImageDrawable(drawable); + } + } + } + + private void setUnknownImage(View view, boolean large) { + if (large) { + setImage(view, largeUnknownImage, false); + } else { + if (view instanceof TextView) { + ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); + } else if (view instanceof ImageView) { + ((ImageView) view).setImageResource(R.drawable.unknown_album); + } + } + } + + public void clear() { + queue.clear(); + } + + @Override + public void run() { + while (true) { + try { + Task task = queue.take(); + task.execute(); + } catch (Throwable x) { + Log.e(TAG, "Unexpected exception in ImageLoader.", x); + } + } + } + + private class Task { + private final View view; + private final MusicDirectory.Entry entry; + private final Handler handler; + private final int size; + private final boolean reflection; + private final boolean saveToFile; + private final boolean crossfade; + + public Task(View view, MusicDirectory.Entry entry, int size, boolean reflection, boolean saveToFile, boolean crossfade) { + this.view = view; + this.entry = entry; + this.size = size; + this.reflection = reflection; + this.saveToFile = saveToFile; + this.crossfade = crossfade; + handler = new Handler(); + } + + public void execute() { + try { + MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); + Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, null); + + if (reflection) { + //bitmap = createReflection(bitmap); + } + + final Drawable drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap); + cache.put(getKey(entry.getCoverArt(), size), drawable); + + handler.post(new Runnable() { + @Override + public void run() { + setImage(view, drawable, crossfade); + } + }); + } catch (Throwable x) { + Log.e(TAG, "Failed to download album art.", x); + } + } + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/util/SongView.java b/src/net/sourceforge/subsonic/androidapp/util/SongView.java index e5e79147..b638fc7e 100644 --- a/src/net/sourceforge/subsonic/androidapp/util/SongView.java +++ b/src/net/sourceforge/subsonic/androidapp/util/SongView.java @@ -1,224 +1,224 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.util; - -import android.content.Context; -import android.os.Handler; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Checkable; -import android.widget.CheckedTextView; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.service.DownloadService; -import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; -import net.sourceforge.subsonic.androidapp.service.DownloadFile; -import net.sourceforge.subsonic.androidapp.service.MusicService; -import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; - -import java.io.File; -import java.util.WeakHashMap; - -/** - * Used to display songs in a {@code ListView}. - * - * @author Sindre Mehus - */ -public class SongView extends LinearLayout implements Checkable { - - private static final String TAG = SongView.class.getSimpleName(); - private static final WeakHashMap INSTANCES = new WeakHashMap(); - private static Handler handler; - - private CheckedTextView checkedTextView; - private ImageView starImageView; - private TextView titleTextView; - private TextView artistTextView; - private TextView durationTextView; - private TextView statusTextView; - private MusicDirectory.Entry song; - - public SongView(Context context) { - super(context); - LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); - - checkedTextView = (CheckedTextView) findViewById(R.id.song_check); - starImageView = (ImageView) findViewById(R.id.song_star); - titleTextView = (TextView) findViewById(R.id.song_title); - artistTextView = (TextView) findViewById(R.id.song_artist); - durationTextView = (TextView) findViewById(R.id.song_duration); - statusTextView = (TextView) findViewById(R.id.song_status); - - INSTANCES.put(this, null); - int instanceCount = INSTANCES.size(); - - if (instanceCount > 50) { - Log.w(TAG, instanceCount + " live SongView instances"); - } - - startUpdater(); - } - - public void setSong(final MusicDirectory.Entry song, boolean checkable) { - this.song = song; - StringBuilder artist = new StringBuilder(40); - - String bitRate = null; - if (song.getBitRate() != null) { - bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate()); - } - - String fileFormat = null; - if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) { - fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix()); - } else { - fileFormat = song.getSuffix(); - } - - artist.append(song.getArtist()).append(" (") - .append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat)) - .append(")"); - - titleTextView.setText(song.getTitle()); - artistTextView.setText(artist); - durationTextView.setText(Util.formatDuration(song.getDuration())); - starImageView.setImageDrawable(song.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow)); - checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE); - - starImageView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - final boolean isStarred = song.getStarred(); - final String id = song.getId(); - - if (!isStarred) { - starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); - song.setStarred(true); - } else { - starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); - song.setStarred(false); - } - - new Thread(new Runnable() { - public void run() { - MusicService musicService = MusicServiceFactory.getMusicService(null); - - try { - if (!isStarred) { - musicService.star(id, getContext(), null); - } else { - musicService.unstar(id, getContext(), null); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - }).start(); - } - }); - - update(); - } - - private void update() { - DownloadService downloadService = DownloadServiceImpl.getInstance(); - if (downloadService == null) { - return; - } - - DownloadFile downloadFile = downloadService.forSong(song); - File completeFile = downloadFile.getCompleteFile(); - File partialFile = downloadFile.getPartialFile(); - - int leftImage = 0; - int rightImage = 0; - - if (completeFile.exists()) { - leftImage = downloadFile.isSaved() ? R.drawable.ic_stat_saved : R.drawable.ic_stat_downloaded; - } - - if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) { - statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext())); - rightImage = R.drawable.ic_stat_downloading; - } else { - statusTextView.setText(null); - } - statusTextView.setCompoundDrawablesWithIntrinsicBounds(leftImage, 0, rightImage, 0); - - if (!song.getStarred()) { - starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); - } else { - starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); - } - - boolean playing = downloadService.getCurrentPlaying() == downloadFile; - if (playing) { - titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_stat_play, 0, 0, 0); - } else { - titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); - } - } - - private static synchronized void startUpdater() { - if (handler != null) { - return; - } - - handler = new Handler(); - Runnable runnable = new Runnable() { - @Override - public void run() { - updateAll(); - handler.postDelayed(this, 1000L); - } - }; - handler.postDelayed(runnable, 1000L); - } - - private static void updateAll() { - try { - for (SongView view : INSTANCES.keySet()) { - if (view.isShown()) { - view.update(); - } - } - } catch (Throwable x) { - Log.w(TAG, "Error when updating song views.", x); - } - } - - @Override - public void setChecked(boolean b) { - checkedTextView.setChecked(b); - } - - @Override - public boolean isChecked() { - return checkedTextView.isChecked(); - } - - @Override - public void toggle() { - checkedTextView.toggle(); - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.util; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Checkable; +import android.widget.CheckedTextView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.service.DownloadService; +import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; +import net.sourceforge.subsonic.androidapp.service.DownloadFile; +import net.sourceforge.subsonic.androidapp.service.MusicService; +import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; + +import java.io.File; +import java.util.WeakHashMap; + +/** + * Used to display songs in a {@code ListView}. + * + * @author Sindre Mehus + */ +public class SongView extends LinearLayout implements Checkable { + + private static final String TAG = SongView.class.getSimpleName(); + private static final WeakHashMap INSTANCES = new WeakHashMap(); + private static Handler handler; + + private CheckedTextView checkedTextView; + private ImageView starImageView; + private TextView titleTextView; + private TextView artistTextView; + private TextView durationTextView; + private TextView statusTextView; + private MusicDirectory.Entry song; + + public SongView(Context context) { + super(context); + LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); + + checkedTextView = (CheckedTextView) findViewById(R.id.song_check); + starImageView = (ImageView) findViewById(R.id.song_star); + titleTextView = (TextView) findViewById(R.id.song_title); + artistTextView = (TextView) findViewById(R.id.song_artist); + durationTextView = (TextView) findViewById(R.id.song_duration); + statusTextView = (TextView) findViewById(R.id.song_status); + + INSTANCES.put(this, null); + int instanceCount = INSTANCES.size(); + + if (instanceCount > 50) { + Log.w(TAG, instanceCount + " live SongView instances"); + } + + startUpdater(); + } + + public void setSong(final MusicDirectory.Entry song, boolean checkable) { + this.song = song; + StringBuilder artist = new StringBuilder(40); + + String bitRate = null; + if (song.getBitRate() != null) { + bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate()); + } + + String fileFormat = null; + if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) { + fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix()); + } else { + fileFormat = song.getSuffix(); + } + + artist.append(song.getArtist()).append(" (") + .append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat)) + .append(")"); + + titleTextView.setText(song.getTitle()); + artistTextView.setText(artist); + durationTextView.setText(Util.formatDuration(song.getDuration())); + starImageView.setImageDrawable(song.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow)); + checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE); + + starImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final boolean isStarred = song.getStarred(); + final String id = song.getId(); + + if (!isStarred) { + starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); + song.setStarred(true); + } else { + starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); + song.setStarred(false); + } + + new Thread(new Runnable() { + public void run() { + MusicService musicService = MusicServiceFactory.getMusicService(null); + + try { + if (!isStarred) { + musicService.star(id, getContext(), null); + } else { + musicService.unstar(id, getContext(), null); + } + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } + }).start(); + } + }); + + update(); + } + + private void update() { + DownloadService downloadService = DownloadServiceImpl.getInstance(); + if (downloadService == null) { + return; + } + + DownloadFile downloadFile = downloadService.forSong(song); + File completeFile = downloadFile.getCompleteFile(); + File partialFile = downloadFile.getPartialFile(); + + int leftImage = 0; + int rightImage = 0; + + if (completeFile.exists()) { + leftImage = downloadFile.isSaved() ? R.drawable.ic_stat_saved : R.drawable.ic_stat_downloaded; + } + + if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) { + statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext())); + rightImage = R.drawable.ic_stat_downloading; + } else { + statusTextView.setText(null); + } + statusTextView.setCompoundDrawablesWithIntrinsicBounds(leftImage, 0, rightImage, 0); + + if (!song.getStarred()) { + starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); + } else { + starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); + } + + boolean playing = downloadService.getCurrentPlaying() == downloadFile; + if (playing) { + titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_stat_play, 0, 0, 0); + } else { + titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + + private static synchronized void startUpdater() { + if (handler != null) { + return; + } + + handler = new Handler(); + Runnable runnable = new Runnable() { + @Override + public void run() { + updateAll(); + handler.postDelayed(this, 1000L); + } + }; + handler.postDelayed(runnable, 1000L); + } + + private static void updateAll() { + try { + for (SongView view : INSTANCES.keySet()) { + if (view.isShown()) { + view.update(); + } + } + } catch (Throwable x) { + Log.w(TAG, "Error when updating song views.", x); + } + } + + @Override + public void setChecked(boolean b) { + checkedTextView.setChecked(b); + } + + @Override + public boolean isChecked() { + return checkedTextView.isChecked(); + } + + @Override + public void toggle() { + checkedTextView.toggle(); + } +} diff --git a/src/net/sourceforge/subsonic/androidapp/util/Util.java b/src/net/sourceforge/subsonic/androidapp/util/Util.java index 40313823..b43b927f 100644 --- a/src/net/sourceforge/subsonic/androidapp/util/Util.java +++ b/src/net/sourceforge/subsonic/androidapp/util/Util.java @@ -1,874 +1,882 @@ -/* - 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 . - - Copyright 2009 (C) Sindre Mehus - */ -package net.sourceforge.subsonic.androidapp.util; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.media.AudioManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Environment; -import android.os.Handler; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.widget.RemoteViews; -import android.widget.Toast; -import net.sourceforge.subsonic.androidapp.R; -import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; -import net.sourceforge.subsonic.androidapp.activity.MainActivity; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; -import net.sourceforge.subsonic.androidapp.domain.PlayerState; -import net.sourceforge.subsonic.androidapp.domain.RepeatMode; -import net.sourceforge.subsonic.androidapp.domain.SearchResult; -import net.sourceforge.subsonic.androidapp.domain.Version; -import net.sourceforge.subsonic.androidapp.domain.MusicDirectory.Entry; -import net.sourceforge.subsonic.androidapp.provider.SubsonicAppWidgetProvider4x1; -import net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver; -import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; -import org.apache.http.HttpEntity; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.security.MessageDigest; -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author Sindre Mehus - * @version $Id$ - */ -public class Util extends DownloadActivity { - - private static final String TAG = Util.class.getSimpleName(); - - private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB"); - private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB"); - private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB"); - - private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null; - private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null; - private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null; - private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; - - public static final String EVENT_META_CHANGED = "net.sourceforge.subsonic.androidapp.EVENT_META_CHANGED"; - public static final String EVENT_PLAYSTATE_CHANGED = "net.sourceforge.subsonic.androidapp.EVENT_PLAYSTATE_CHANGED"; - - private static final Map SERVER_REST_VERSIONS = new ConcurrentHashMap(); - - // 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) { - if (context == null) - return false; - else - return getActiveServer(context) == 0; - } - - public static boolean isScreenLitOnDownload(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false); - } - - public static RepeatMode getRepeatMode(Context context) { - SharedPreferences prefs = getPreferences(context); - return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name())); - } - - public static void setRepeatMode(Context context, RepeatMode repeatMode) { - SharedPreferences prefs = getPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name()); - editor.commit(); - } - - public static boolean isScrobblingEnabled(Context context) { - if (isOffline(context)) { - return false; - } - SharedPreferences prefs = getPreferences(context); - return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false); - } - - public static boolean isNotificationEnabled(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getBoolean(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION, false); - } - - public static boolean isLockScreenEnabled(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false); - } - - public static void setActiveServer(Context context, int instance) { - SharedPreferences prefs = getPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); - editor.commit(); - } - - public static int getActiveServer(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); - } - - public static String getServerName(Context context, int instance) { - if (instance == 0) { - return context.getResources().getString(R.string.main_offline); - } - SharedPreferences prefs = getPreferences(context); - return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); - } - - public static void setServerRestVersion(Context context, Version version) { - SERVER_REST_VERSIONS.put(getActiveServer(context), version); - } - - public static Version getServerRestVersion(Context context) { - return SERVER_REST_VERSIONS.get(getActiveServer(context)); - } - - public static void setSelectedMusicFolderId(Context context, String musicFolderId) { - int instance = getActiveServer(context); - SharedPreferences prefs = getPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId); - editor.commit(); - } - - public static String getSelectedMusicFolderId(Context context) { - SharedPreferences prefs = getPreferences(context); - int instance = getActiveServer(context); - return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); - } - - public static String getTheme(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getString(Constants.PREFERENCES_KEY_THEME, "dark"); - } - - public static int getMaxBitrate(Context context) { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - if (networkInfo == null) { - return 0; - } - - boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0")); - } - - public static int getPreloadCount(Context context) { - SharedPreferences prefs = getPreferences(context); - int preloadCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_PRELOAD_COUNT, "-1")); - return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount; - } - - public static int getCacheSizeMB(Context context) { - SharedPreferences prefs = getPreferences(context); - int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); - return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; - } - - public static String getRestUrl(Context context, String method) { - StringBuilder builder = new StringBuilder(); - - SharedPreferences prefs = getPreferences(context); - - int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); - String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); - String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); - String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); - - // Slightly obfuscate password - password = "enc:" + Util.utf8HexEncode(password); - - builder.append(serverUrl); - if (builder.charAt(builder.length() - 1) != '/') { - builder.append("/"); - } - builder.append("rest/").append(method).append(".view"); - builder.append("?u=").append(username); - builder.append("&p=").append(password); - builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION); - builder.append("&c=").append(Constants.REST_CLIENT_ID); - - return builder.toString(); - } - - public static SharedPreferences getPreferences(Context context) { - return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0); - } - - public static String getContentType(HttpEntity entity) { - if (entity == null || entity.getContentType() == null) { - return null; - } - return entity.getContentType().getValue(); - } - - public static int getRemainingTrialDays(Context context) { - SharedPreferences prefs = getPreferences(context); - long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L); - - if (installTime == 0L) { - installTime = System.currentTimeMillis(); - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime); - editor.commit(); - } - - long now = System.currentTimeMillis(); - long millisPerDay = 24L * 60L * 60L * 1000L; - int daysSinceInstall = (int) ((now - installTime) / millisPerDay); - return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall); - } - - /** - * Get the contents of an InputStream as a byte[]. - *

- * This method buffers the input internally, so there is no need to use a - * BufferedInputStream. - * - * @param input the InputStream to read from - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - */ - public static byte[] toByteArray(InputStream input) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - copy(input, output); - return output.toByteArray(); - } - - public static long copy(InputStream input, OutputStream output) - throws IOException { - byte[] buffer = new byte[1024 * 4]; - long count = 0; - int n; - while (-1 != (n = input.read(buffer))) { - output.write(buffer, 0, n); - count += n; - } - return count; - } - - public static void atomicCopy(File from, File to) throws IOException { - FileInputStream in = null; - FileOutputStream out = null; - File tmp = null; - try { - tmp = new File(to.getPath() + ".tmp"); - in = new FileInputStream(from); - out = new FileOutputStream(tmp); - in.getChannel().transferTo(0, from.length(), out.getChannel()); - out.close(); - if (!tmp.renameTo(to)) { - throw new IOException("Failed to rename " + tmp + " to " + to); - } - Log.i(TAG, "Copied " + from + " to " + to); - } catch (IOException x) { - close(out); - delete(to); - throw x; - } finally { - close(in); - close(out); - delete(tmp); - } - } - - public static void close(Closeable closeable) { - try { - if (closeable != null) { - closeable.close(); - } - } catch (Throwable x) { - // Ignored - } - } - - public static boolean delete(File file) { - if (file != null && file.exists()) { - if (!file.delete()) { - Log.w(TAG, "Failed to delete file " + file); - return false; - } - Log.i(TAG, "Deleted file " + file); - } - return true; - } - - public static void toast(Context context, int messageId) { - toast(context, messageId, true); - } - - public static void toast(Context context, int messageId, boolean shortDuration) { - toast(context, context.getString(messageId), shortDuration); - } - - public static void toast(Context context, String message) { - toast(context, message, true); - } - - public static void toast(Context context, String message, boolean shortDuration) { - if (toast == null) { - toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - } else { - toast.setText(message); - toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); - } - toast.show(); - } - - /** - * Converts a byte-count to a formatted string suitable for display to the user. - * For instance: - *

    - *
  • format(918) returns "918 B".
  • - *
  • format(98765) returns "96 KB".
  • - *
  • format(1238476) returns "1.2 MB".
  • - *
- * 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: - *
    - *
  • format(918) returns "918 B".
  • - *
  • format(98765) returns "96 KB".
  • - *
  • format(1238476) returns "1.2 MB".
  • - *
- * This method assumes that 1 KB is 1024 bytes. - * This version of the method returns a localized string. - * - * @param byteCount The number of bytes. - * @return The formatted string. - */ - public static synchronized String formatLocalizedBytes(long byteCount, Context context) { - - // More than 1 GB? - if (byteCount >= 1024 * 1024 * 1024) { - if (GIGA_BYTE_LOCALIZED_FORMAT == null) { - GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte)); - } - - return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024)); - } - - // More than 1 MB? - if (byteCount >= 1024 * 1024) { - if (MEGA_BYTE_LOCALIZED_FORMAT == null) { - MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte)); - } - - return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024)); - } - - // More than 1 KB? - if (byteCount >= 1024) { - if (KILO_BYTE_LOCALIZED_FORMAT == null) { - KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte)); - } - - return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024); - } - - if (BYTE_LOCALIZED_FORMAT == null) { - BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte)); - } - - return BYTE_LOCALIZED_FORMAT.format((double) byteCount); - } - - public static String formatDuration(Integer seconds) { - if (seconds == null) { - return null; - } - - int minutes = seconds / 60; - int secs = seconds % 60; - - StringBuilder builder = new StringBuilder(6); - builder.append(minutes).append(":"); - if (secs < 10) { - builder.append("0"); - } - builder.append(secs); - return builder.toString(); - } - - public static boolean equals(Object object1, Object object2) { - if (object1 == object2) { - return true; - } - if (object1 == null || object2 == null) { - return false; - } - return object1.equals(object2); - - } - - /** - * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes. - * - * @param s The string to encode. - * @return The encoded string. - */ - public static String utf8HexEncode(String s) { - if (s == null) { - return null; - } - byte[] utf8; - try { - utf8 = s.getBytes(Constants.UTF_8); - } catch (UnsupportedEncodingException x) { - throw new RuntimeException(x); - } - return hexEncode(utf8); - } - - /** - * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. - * The returned array will be double the length of the passed array, as it takes two characters to represent any - * given byte. - * - * @param data Bytes to convert to hexadecimal characters. - * @return A string containing hexadecimal characters. - */ - public static String hexEncode(byte[] data) { - int length = data.length; - char[] out = new char[length << 1]; - // two characters form the hex value. - for (int i = 0, j = 0; i < length; i++) { - out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4]; - out[j++] = HEX_DIGITS[0x0F & data[i]]; - } - return new String(out); - } - - /** - * Calculates the MD5 digest and returns the value as a 32 character hex string. - * - * @param s Data to digest. - * @return MD5 digest as a hex string. - */ - public static String md5Hex(String s) { - if (s == null) { - return null; - } - - try { - MessageDigest md5 = MessageDigest.getInstance("MD5"); - return hexEncode(md5.digest(s.getBytes(Constants.UTF_8))); - } catch (Exception x) { - throw new RuntimeException(x.getMessage(), x); - } - } - - public static boolean isNetworkConnected(Context context) { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - boolean connected = networkInfo != null && networkInfo.isConnected(); - - boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI; - boolean wifiRequired = isWifiRequiredForDownload(context); - - return connected && (!wifiRequired || wifiConnected); - } - - public static boolean isExternalStoragePresent() { - return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); - } - - private static boolean isWifiRequiredForDownload(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false); - } - - public static void info(Context context, int titleId, int messageId) { - showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId); - } - - private static void showDialog(Context context, int icon, int titleId, int messageId) { - new AlertDialog.Builder(context) - .setIcon(icon) - .setTitle(titleId) - .setMessage(messageId) - .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int i) { - dialog.dismiss(); - } - }) - .show(); - } - - public static void showPlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler, MusicDirectory.Entry song, final Notification notification, PlayerState playerState) { - - // Use the same text for the ticker and the expanded notification - String title = song.getTitle(); - String text = song.getArtist(); - String album = song.getAlbum(); - - // Set the album art. - try { - int size = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); - Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size); - if (bitmap == null) { - // set default album art - notification.contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } else { - notification.contentView.setImageViewBitmap(R.id.notification_image, bitmap); - } - } catch (Exception x) { - Log.w(TAG, "Failed to get notification cover art", x); - notification.contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); - } - - // set the text for the notifications - notification.contentView.setTextViewText(R.id.trackname, title); - notification.contentView.setTextViewText(R.id.artist, text); - notification.contentView.setTextViewText(R.id.album, album); - - if (playerState == PlayerState.PAUSED) { - notification.contentView.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play); - } - else if (playerState == PlayerState.STARTED) { - notification.contentView.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause); - } - - // Send the notification and put the service in the foreground. - handler.post(new Runnable() { - @Override - public void run() { - startForeground(downloadService, Constants.NOTIFICATION_ID_PLAYING, notification); - } - }); - - // Update widget - SubsonicAppWidgetProvider4x1.getInstance().notifyChange(context, downloadService, true); - } - - public static void hidePlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler) { - - // Remove notification and remove the service from the foreground - handler.post(new Runnable() { - @Override - public void run() { - stopForeground(downloadService, true); - } - }); - - // Update widget - SubsonicAppWidgetProvider4x1.getInstance().notifyChange(context, downloadService, false); - } - - public static void sleepQuietly(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException x) { - Log.w(TAG, "Interrupted from sleep.", x); - } - } - - public static void startActivityWithoutTransition(Activity currentActivity, Class 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 constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class); - return constructor.newInstance(context.getResources(), bitmap); - } catch (Throwable x) { - return new BitmapDrawable(bitmap); - } - } - - public static void registerMediaButtonEventReceiver(Context context) { - - if (getMediaButtonsPreference(context)) { - // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2. - // Use reflection to maintain compatibility with 1.5. - try { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); - Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class); - method.invoke(audioManager, componentName); - } catch (Throwable x) { - // Ignored. - } - } - } - - public static void unregisterMediaButtonEventReceiver(Context context) { - // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2. - // Use reflection to maintain compatibility with 1.5. - try { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); - Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class); - method.invoke(audioManager, componentName); - } catch (Throwable x) { - // Ignored. - } - } - - private static void startForeground(Service service, int notificationId, Notification notification) { - // Service.startForeground() was introduced in Android 2.0. - // Use reflection to maintain compatibility with 1.5. - try { - Method method = Service.class.getMethod("startForeground", int.class, Notification.class); - method.invoke(service, notificationId, notification); - Log.i(TAG, "Successfully invoked Service.startForeground()"); - } catch (Throwable x) { - NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(Constants.NOTIFICATION_ID_PLAYING, notification); - Log.i(TAG, "Service.startForeground() not available. Using work-around."); - } - } - - private static void stopForeground(Service service, boolean removeNotification) { - // Service.stopForeground() was introduced in Android 2.0. - // Use reflection to maintain compatibility with 1.5. - try { - Method method = Service.class.getMethod("stopForeground", boolean.class); - method.invoke(service, removeNotification); - Log.i(TAG, "Successfully invoked Service.stopForeground()"); - } catch (Throwable x) { - NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(Constants.NOTIFICATION_ID_PLAYING); - Log.i(TAG, "Service.stopForeground() not available. Using work-around."); - } - } - - public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult) { - MusicDirectory musicDirectory = new MusicDirectory(); - - for (Entry entry : searchResult.getSongs()) { - musicDirectory.addChild(entry); - } - - return musicDirectory; - } - - /** - *

Broadcasts the given song info as the new song being played.

- */ - public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { - Intent intent = new Intent(EVENT_META_CHANGED); - - if (song != null) { - intent.putExtra("title", song.getTitle()); - intent.putExtra("artist", song.getArtist()); - intent.putExtra("album", song.getAlbum()); - - File albumArtFile = FileUtil.getAlbumArtFile(context, song); - intent.putExtra("coverart", albumArtFile.getAbsolutePath()); - } else { - intent.putExtra("title", ""); - intent.putExtra("artist", ""); - intent.putExtra("album", ""); - intent.putExtra("coverart", ""); - } - - context.sendBroadcast(intent); - } - - /** - *

Broadcasts the given player state as the one being set.

- */ - public static void broadcastPlaybackStatusChange(Context context, PlayerState state) { - Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); - - switch (state) { - case STARTED: - intent.putExtra("state", "play"); - break; - case STOPPED: - intent.putExtra("state", "stop"); - break; - case PAUSED: - intent.putExtra("state", "pause"); - break; - case COMPLETED: - intent.putExtra("state", "complete"); - break; - default: - return; // No need to broadcast. - } - - context.sendBroadcast(intent); - } - - public static void linkButtons(Context context, RemoteViews views, boolean playerActive) { - - Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); - views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); - views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); - - // Emulate media button clicks. - intent = new Intent("1"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); - views.setOnClickPendingIntent(R.id.control_play, pendingIntent); - - intent = new Intent("2"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); - views.setOnClickPendingIntent(R.id.control_next, pendingIntent); - - intent = new Intent("3"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); - views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); - - intent = new Intent("4"); - intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); - intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); - pendingIntent = PendingIntent.getService(context, 0, intent, 0); - views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); - } - - public static int getNetworkTimeout(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000")); - } - - public static int getDefaultAlbums(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS, "5")); - } - - public static int getMaxAlbums(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_MAX_ALBUMS, "20")); - } - - public static int getDefaultSongs(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_DEFAULT_SONGS, "10")); - } - - public static int getMaxSongs(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_MAX_SONGS, "25")); - } - - public static int getMaxArtists(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_MAX_ARTISTS, "10")); - } - - public static int getDefaultArtists(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS, "3")); - } - - public static int getBufferLength(Context context) { - SharedPreferences prefs = getPreferences(context); - return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_BUFFER_LENGTH, "5")); - } - - public static boolean getMediaButtonsPreference(Context context) { - SharedPreferences prefs = getPreferences(context); - return prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true); - } -} +/* + 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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.androidapp.util; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Environment; +import android.os.Handler; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.widget.RemoteViews; +import android.widget.Toast; +import net.sourceforge.subsonic.androidapp.R; +import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; +import net.sourceforge.subsonic.androidapp.activity.MainActivity; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; +import net.sourceforge.subsonic.androidapp.domain.PlayerState; +import net.sourceforge.subsonic.androidapp.domain.RepeatMode; +import net.sourceforge.subsonic.androidapp.domain.SearchResult; +import net.sourceforge.subsonic.androidapp.domain.Version; +import net.sourceforge.subsonic.androidapp.domain.MusicDirectory.Entry; +import net.sourceforge.subsonic.androidapp.provider.SubsonicAppWidgetProvider4x1; +import net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver; +import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; +import org.apache.http.HttpEntity; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class Util extends DownloadActivity { + + private static final String TAG = Util.class.getSimpleName(); + + private static final DecimalFormat GIGA_BYTE_FORMAT = new DecimalFormat("0.00 GB"); + private static final DecimalFormat MEGA_BYTE_FORMAT = new DecimalFormat("0.00 MB"); + private static final DecimalFormat KILO_BYTE_FORMAT = new DecimalFormat("0 KB"); + + private static DecimalFormat GIGA_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat MEGA_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat KILO_BYTE_LOCALIZED_FORMAT = null; + private static DecimalFormat BYTE_LOCALIZED_FORMAT = null; + + public static final String EVENT_META_CHANGED = "net.sourceforge.subsonic.androidapp.EVENT_META_CHANGED"; + public static final String EVENT_PLAYSTATE_CHANGED = "net.sourceforge.subsonic.androidapp.EVENT_PLAYSTATE_CHANGED"; + + private static final Map SERVER_REST_VERSIONS = new ConcurrentHashMap(); + + // 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 static MusicDirectory.Entry currentSong; + + private Util() { + } + + public static boolean isOffline(Context context) { + if (context == null) + return false; + else + return getActiveServer(context) == 0; + } + + public static boolean isScreenLitOnDownload(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SCREEN_LIT_ON_DOWNLOAD, false); + } + + public static RepeatMode getRepeatMode(Context context) { + SharedPreferences prefs = getPreferences(context); + return RepeatMode.valueOf(prefs.getString(Constants.PREFERENCES_KEY_REPEAT_MODE, RepeatMode.OFF.name())); + } + + public static void setRepeatMode(Context context, RepeatMode repeatMode) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name()); + editor.commit(); + } + + public static boolean isScrobblingEnabled(Context context) { + if (isOffline(context)) { + return false; + } + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false); + } + + public static boolean isNotificationEnabled(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION, false); + } + + public static boolean isLockScreenEnabled(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false); + } + + public static void setActiveServer(Context context, int instance) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance); + editor.commit(); + } + + public static int getActiveServer(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + } + + public static String getServerName(Context context, int instance) { + if (instance == 0) { + return context.getResources().getString(R.string.main_offline); + } + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_SERVER_NAME + instance, null); + } + + public static void setServerRestVersion(Context context, Version version) { + SERVER_REST_VERSIONS.put(getActiveServer(context), version); + } + + public static Version getServerRestVersion(Context context) { + return SERVER_REST_VERSIONS.get(getActiveServer(context)); + } + + public static void setSelectedMusicFolderId(Context context, String musicFolderId) { + int instance = getActiveServer(context); + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, musicFolderId); + editor.commit(); + } + + public static String getSelectedMusicFolderId(Context context) { + SharedPreferences prefs = getPreferences(context); + int instance = getActiveServer(context); + return prefs.getString(Constants.PREFERENCES_KEY_MUSIC_FOLDER_ID + instance, null); + } + + public static String getTheme(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getString(Constants.PREFERENCES_KEY_THEME, "dark"); + } + + public static int getMaxBitrate(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo == null) { + return 0; + } + + boolean wifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(wifi ? Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI : Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0")); + } + + public static int getPreloadCount(Context context) { + SharedPreferences prefs = getPreferences(context); + int preloadCount = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_PRELOAD_COUNT, "-1")); + return preloadCount == -1 ? Integer.MAX_VALUE : preloadCount; + } + + public static int getCacheSizeMB(Context context) { + SharedPreferences prefs = getPreferences(context); + int cacheSize = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_CACHE_SIZE, "-1")); + return cacheSize == -1 ? Integer.MAX_VALUE : cacheSize; + } + + public static String getRestUrl(Context context, String method) { + StringBuilder builder = new StringBuilder(); + + SharedPreferences prefs = getPreferences(context); + + int instance = prefs.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1); + String serverUrl = prefs.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null); + String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); + String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); + + // Slightly obfuscate password + password = "enc:" + Util.utf8HexEncode(password); + + builder.append(serverUrl); + if (builder.charAt(builder.length() - 1) != '/') { + builder.append("/"); + } + builder.append("rest/").append(method).append(".view"); + builder.append("?u=").append(username); + builder.append("&p=").append(password); + builder.append("&v=").append(Constants.REST_PROTOCOL_VERSION); + builder.append("&c=").append(Constants.REST_CLIENT_ID); + + return builder.toString(); + } + + public static SharedPreferences getPreferences(Context context) { + return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0); + } + + public static String getContentType(HttpEntity entity) { + if (entity == null || entity.getContentType() == null) { + return null; + } + return entity.getContentType().getValue(); + } + + public static int getRemainingTrialDays(Context context) { + SharedPreferences prefs = getPreferences(context); + long installTime = prefs.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L); + + if (installTime == 0L) { + installTime = System.currentTimeMillis(); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime); + editor.commit(); + } + + long now = System.currentTimeMillis(); + long millisPerDay = 24L * 60L * 60L * 1000L; + int daysSinceInstall = (int) ((now - installTime) / millisPerDay); + return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall); + } + + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + public static long copy(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[1024 * 4]; + long count = 0; + int n; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + public static void atomicCopy(File from, File to) throws IOException { + FileInputStream in = null; + FileOutputStream out = null; + File tmp = null; + try { + tmp = new File(to.getPath() + ".tmp"); + in = new FileInputStream(from); + out = new FileOutputStream(tmp); + in.getChannel().transferTo(0, from.length(), out.getChannel()); + out.close(); + if (!tmp.renameTo(to)) { + throw new IOException("Failed to rename " + tmp + " to " + to); + } + Log.i(TAG, "Copied " + from + " to " + to); + } catch (IOException x) { + close(out); + delete(to); + throw x; + } finally { + close(in); + close(out); + delete(tmp); + } + } + + public static void close(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (Throwable x) { + // Ignored + } + } + + public static boolean delete(File file) { + if (file != null && file.exists()) { + if (!file.delete()) { + Log.w(TAG, "Failed to delete file " + file); + return false; + } + Log.i(TAG, "Deleted file " + file); + } + return true; + } + + public static void toast(Context context, int messageId) { + toast(context, messageId, true); + } + + public static void toast(Context context, int messageId, boolean shortDuration) { + toast(context, context.getString(messageId), shortDuration); + } + + public static void toast(Context context, String message) { + toast(context, message, true); + } + + public static void toast(Context context, String message, boolean shortDuration) { + if (toast == null) { + toast = Toast.makeText(context, message, shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + } else { + toast.setText(message); + toast.setDuration(shortDuration ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + } + toast.show(); + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + *

    + *
  • format(918) returns "918 B".
  • + *
  • format(98765) returns "96 KB".
  • + *
  • format(1238476) returns "1.2 MB".
  • + *
+ * 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: + *
    + *
  • format(918) returns "918 B".
  • + *
  • format(98765) returns "96 KB".
  • + *
  • format(1238476) returns "1.2 MB".
  • + *
+ * This method assumes that 1 KB is 1024 bytes. + * This version of the method returns a localized string. + * + * @param byteCount The number of bytes. + * @return The formatted string. + */ + public static synchronized String formatLocalizedBytes(long byteCount, Context context) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + if (GIGA_BYTE_LOCALIZED_FORMAT == null) { + GIGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_gigabyte)); + } + + return GIGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + if (MEGA_BYTE_LOCALIZED_FORMAT == null) { + MEGA_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_megabyte)); + } + + return MEGA_BYTE_LOCALIZED_FORMAT.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + if (KILO_BYTE_LOCALIZED_FORMAT == null) { + KILO_BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_kilobyte)); + } + + return KILO_BYTE_LOCALIZED_FORMAT.format((double) byteCount / 1024); + } + + if (BYTE_LOCALIZED_FORMAT == null) { + BYTE_LOCALIZED_FORMAT = new DecimalFormat(context.getResources().getString(R.string.util_bytes_format_byte)); + } + + return BYTE_LOCALIZED_FORMAT.format((double) byteCount); + } + + public static String formatDuration(Integer seconds) { + if (seconds == null) { + return null; + } + + int minutes = seconds / 60; + int secs = seconds % 60; + + StringBuilder builder = new StringBuilder(6); + builder.append(minutes).append(":"); + if (secs < 10) { + builder.append("0"); + } + builder.append(secs); + return builder.toString(); + } + + public static boolean equals(Object object1, Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + return object1.equals(object2); + + } + + /** + * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes. + * + * @param s The string to encode. + * @return The encoded string. + */ + public static String utf8HexEncode(String s) { + if (s == null) { + return null; + } + byte[] utf8; + try { + utf8 = s.getBytes(Constants.UTF_8); + } catch (UnsupportedEncodingException x) { + throw new RuntimeException(x); + } + return hexEncode(utf8); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data Bytes to convert to hexadecimal characters. + * @return A string containing hexadecimal characters. + */ + public static String hexEncode(byte[] data) { + int length = data.length; + char[] out = new char[length << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < length; i++) { + out[j++] = HEX_DIGITS[(0xF0 & data[i]) >>> 4]; + out[j++] = HEX_DIGITS[0x0F & data[i]]; + } + return new String(out); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param s Data to digest. + * @return MD5 digest as a hex string. + */ + public static String md5Hex(String s) { + if (s == null) { + return null; + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return hexEncode(md5.digest(s.getBytes(Constants.UTF_8))); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + + public static boolean isNetworkConnected(Context context) { + ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + boolean connected = networkInfo != null && networkInfo.isConnected(); + + boolean wifiConnected = connected && networkInfo.getType() == ConnectivityManager.TYPE_WIFI; + boolean wifiRequired = isWifiRequiredForDownload(context); + + return connected && (!wifiRequired || wifiConnected); + } + + public static boolean isExternalStoragePresent() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + private static boolean isWifiRequiredForDownload(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_WIFI_REQUIRED_FOR_DOWNLOAD, false); + } + + public static void info(Context context, int titleId, int messageId) { + showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId); + } + + private static void showDialog(Context context, int icon, int titleId, int messageId) { + new AlertDialog.Builder(context) + .setIcon(icon) + .setTitle(titleId) + .setMessage(messageId) + .setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .show(); + } + + public static void showPlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler, MusicDirectory.Entry song, final Notification notification, PlayerState playerState) { + + if (currentSong != song) { + currentSong = song; + + // Use the same text for the ticker and the expanded notification + String title = song.getTitle(); + String text = song.getArtist(); + String album = song.getAlbum(); + + // Set the album art. + try { + int size = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); + Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, song, size); + if (bitmap == null) { + // set default album art + notification.contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } else { + notification.contentView.setImageViewBitmap(R.id.notification_image, bitmap); + } + } catch (Exception x) { + Log.w(TAG, "Failed to get notification cover art", x); + notification.contentView.setImageViewResource(R.id.notification_image, R.drawable.unknown_album); + } + + // set the text for the notifications + notification.contentView.setTextViewText(R.id.trackname, title); + notification.contentView.setTextViewText(R.id.artist, text); + notification.contentView.setTextViewText(R.id.album, album); + } + + if (playerState == PlayerState.PAUSED) { + notification.contentView.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_play); + } + else if (playerState == PlayerState.STARTED) { + notification.contentView.setImageViewResource(R.id.control_play, R.drawable.ic_appwidget_music_pause); + } + + // Send the notification and put the service in the foreground. + handler.post(new Runnable() { + @Override + public void run() { + startForeground(downloadService, Constants.NOTIFICATION_ID_PLAYING, notification); + } + }); + + // Update widget + SubsonicAppWidgetProvider4x1.getInstance().notifyChange(context, downloadService, true); + } + + public static void hidePlayingNotification(final Context context, final DownloadServiceImpl downloadService, Handler handler) { + + currentSong = null; + + // Remove notification and remove the service from the foreground + handler.post(new Runnable(){ + @Override + public void run() { + stopForeground(downloadService, true); + } + }); + + // Update widget + SubsonicAppWidgetProvider4x1.getInstance().notifyChange(context, downloadService, false); + } + + public static void sleepQuietly(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException x) { + Log.w(TAG, "Interrupted from sleep.", x); + } + } + + public static void startActivityWithoutTransition(Activity currentActivity, Class 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 constructor = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class); + return constructor.newInstance(context.getResources(), bitmap); + } catch (Throwable x) { + return new BitmapDrawable(bitmap); + } + } + + public static void registerMediaButtonEventReceiver(Context context) { + + if (getMediaButtonsPreference(context)) { + // AudioManager.registerMediaButtonEventReceiver() was introduced in Android 2.2. + // Use reflection to maintain compatibility with 1.5. + try { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Method method = AudioManager.class.getMethod("registerMediaButtonEventReceiver", ComponentName.class); + method.invoke(audioManager, componentName); + } catch (Throwable x) { + // Ignored. + } + } + } + + public static void unregisterMediaButtonEventReceiver(Context context) { + // AudioManager.unregisterMediaButtonEventReceiver() was introduced in Android 2.2. + // Use reflection to maintain compatibility with 1.5. + try { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName componentName = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Method method = AudioManager.class.getMethod("unregisterMediaButtonEventReceiver", ComponentName.class); + method.invoke(audioManager, componentName); + } catch (Throwable x) { + // Ignored. + } + } + + private static void startForeground(Service service, int notificationId, Notification notification) { + // Service.startForeground() was introduced in Android 2.0. + // Use reflection to maintain compatibility with 1.5. + try { + Method method = Service.class.getMethod("startForeground", int.class, Notification.class); + method.invoke(service, notificationId, notification); + Log.i(TAG, "Successfully invoked Service.startForeground()"); + } catch (Throwable x) { + NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(Constants.NOTIFICATION_ID_PLAYING, notification); + Log.i(TAG, "Service.startForeground() not available. Using work-around."); + } + } + + private static void stopForeground(Service service, boolean removeNotification) { + // Service.stopForeground() was introduced in Android 2.0. + // Use reflection to maintain compatibility with 1.5. + try { + Method method = Service.class.getMethod("stopForeground", boolean.class); + method.invoke(service, removeNotification); + Log.i(TAG, "Successfully invoked Service.stopForeground()"); + } catch (Throwable x) { + NotificationManager notificationManager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(Constants.NOTIFICATION_ID_PLAYING); + Log.i(TAG, "Service.stopForeground() not available. Using work-around."); + } + } + + public static MusicDirectory getSongsFromSearchResult(SearchResult searchResult) { + MusicDirectory musicDirectory = new MusicDirectory(); + + for (Entry entry : searchResult.getSongs()) { + musicDirectory.addChild(entry); + } + + return musicDirectory; + } + + /** + *

Broadcasts the given song info as the new song being played.

+ */ + public static void broadcastNewTrackInfo(Context context, MusicDirectory.Entry song) { + Intent intent = new Intent(EVENT_META_CHANGED); + + if (song != null) { + intent.putExtra("title", song.getTitle()); + intent.putExtra("artist", song.getArtist()); + intent.putExtra("album", song.getAlbum()); + + File albumArtFile = FileUtil.getAlbumArtFile(context, song); + intent.putExtra("coverart", albumArtFile.getAbsolutePath()); + } else { + intent.putExtra("title", ""); + intent.putExtra("artist", ""); + intent.putExtra("album", ""); + intent.putExtra("coverart", ""); + } + + context.sendBroadcast(intent); + } + + /** + *

Broadcasts the given player state as the one being set.

+ */ + public static void broadcastPlaybackStatusChange(Context context, PlayerState state) { + Intent intent = new Intent(EVENT_PLAYSTATE_CHANGED); + + switch (state) { + case STARTED: + intent.putExtra("state", "play"); + break; + case STOPPED: + intent.putExtra("state", "stop"); + break; + case PAUSED: + intent.putExtra("state", "pause"); + break; + case COMPLETED: + intent.putExtra("state", "complete"); + break; + default: + return; // No need to broadcast. + } + + context.sendBroadcast(intent); + } + + public static void linkButtons(Context context, RemoteViews views, boolean playerActive) { + + Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent); + views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent); + + // Emulate media button clicks. + intent = new Intent("1"); + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_play, pendingIntent); + + intent = new Intent("2"); + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_next, pendingIntent); + + intent = new Intent("3"); + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_previous, pendingIntent); + + intent = new Intent("4"); + intent.setComponent(new ComponentName(context, DownloadServiceImpl.class)); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_STOP)); + pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_stop, pendingIntent); + } + + public static int getNetworkTimeout(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, "15000")); + } + + public static int getDefaultAlbums(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS, "5")); + } + + public static int getMaxAlbums(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_MAX_ALBUMS, "20")); + } + + public static int getDefaultSongs(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_DEFAULT_SONGS, "10")); + } + + public static int getMaxSongs(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_MAX_SONGS, "25")); + } + + public static int getMaxArtists(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_MAX_ARTISTS, "10")); + } + + public static int getDefaultArtists(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS, "3")); + } + + public static int getBufferLength(Context context) { + SharedPreferences prefs = getPreferences(context); + return Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_BUFFER_LENGTH, "5")); + } + + public static boolean getMediaButtonsPreference(Context context) { + SharedPreferences prefs = getPreferences(context); + return prefs.getBoolean(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true); + } +}