Fix notification sounds causing playback to start, fixed some CacheCleaner issues

This commit is contained in:
Joshua Bahnsen 2013-02-08 02:09:55 -07:00
parent 6429559026
commit 32024ed1af
11 changed files with 3442 additions and 3401 deletions

View File

@ -1,126 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:a="http://schemas.android.com/apk/res/android"
package="net.sourceforge.subsonic.androidapp"
a:versionCode="56"
a:versionName="3.9.9.15" a:installLocation="auto">
<uses-permission a:name="android.permission.INTERNET"/>
<uses-permission a:name="android.permission.READ_PHONE_STATE"/>
<uses-permission a:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission a:name="android.permission.WAKE_LOCK"/>
<uses-permission a:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission a:name="android.permission.RECORD_AUDIO"/>
<uses-permission a:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-sdk a:minSdkVersion="14" a:targetSdkVersion="16"/>
<supports-screens a:anyDensity="true" a:xlargeScreens="true" a:largeScreens="true" a:normalScreens="true" a:smallScreens="true"/>
<application a:label="@string/common.appname" a:icon="@drawable/ic_launcher" a:allowBackup="false">
<activity a:name="net.sourceforge.subsonic.androidapp.activity.MainActivity"
a:label="Subsonic"
a:configChanges="orientation|keyboardHidden"
a:launchMode="standard">
<intent-filter>
<action a:name="android.intent.action.MAIN"/>
<category a:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SelectArtistActivity"
a:configChanges="orientation|keyboardHidden"
a:launchMode="standard"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SelectAlbumActivity"
a:configChanges="orientation|keyboardHidden"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SearchActivity"
a:label="@string/search.label"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"
/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SelectPlaylistActivity"
a:label="@string/playlist.label"
a:configChanges="orientation|keyboardHidden"
a:launchMode="standard"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.DownloadActivity"
a:configChanges="keyboardHidden"
a:launchMode="singleTask"
a:theme="@android:style/Theme.NoTitleBar.Fullscreen"
/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SettingsActivity"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.HelpActivity"
a:label="@string/help.label"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.LyricsActivity"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.EqualizerActivity"
a:label="@string/equalizer.label"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.VoiceQueryReceiverActivity"
a:launchMode="singleTask">
<intent-filter>
<action a:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category a:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.QueryReceiverActivity"
a:launchMode="singleTask">
<intent-filter>
<action a:name="android.intent.action.SEARCH"/>
</intent-filter>
<meta-data a:name="android.app.searchable" a:resource="@xml/searchable"/>
</activity>
<service a:name="net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl" a:label="Subsonic Download Service">
<intent-filter>
<action a:name="net.sourceforge.subsonic.androidapp.CMD_TOGGLEPAUSE" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_PLAY" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_PAUSE" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_NEXT" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_PREVIOUS" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_STOP" />
</intent-filter>
</service>
<receiver a:name="net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver">
<intent-filter a:priority="999">
<action a:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<receiver a:name="net.sourceforge.subsonic.androidapp.receiver.BluetoothIntentReceiver">
<intent-filter>
<action a:name="android.bluetooth.a2dp.action.SINK_STATE_CHANGED"/>
</intent-filter>
</receiver>
<receiver a:name="net.sourceforge.subsonic.androidapp.provider.SubsonicAppWidgetProvider4x1" a:label="Subsonic (4x1)" >
<intent-filter>
<action a:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data a:name="android.appwidget.provider" a:resource="@xml/appwidget_info_4x1"/>
</receiver>
<provider a:name="net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider"
a:authorities="net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider"/>
<meta-data a:name="android.app.default_searchable"
a:value="net.sourceforge.subsonic.androidapp.activity.QueryReceiverActivity"/>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:a="http://schemas.android.com/apk/res/android"
package="net.sourceforge.subsonic.androidapp"
a:versionCode="57"
a:versionName="3.9.9.16" a:installLocation="auto">
<uses-permission a:name="android.permission.INTERNET"/>
<uses-permission a:name="android.permission.READ_PHONE_STATE"/>
<uses-permission a:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission a:name="android.permission.WAKE_LOCK"/>
<uses-permission a:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission a:name="android.permission.RECORD_AUDIO"/>
<uses-permission a:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-sdk a:minSdkVersion="14" a:targetSdkVersion="17"/>
<supports-screens a:anyDensity="true" a:xlargeScreens="true" a:largeScreens="true" a:normalScreens="true" a:smallScreens="true"/>
<application a:label="@string/common.appname" a:icon="@drawable/ic_launcher" a:allowBackup="false">
<activity a:name="net.sourceforge.subsonic.androidapp.activity.MainActivity"
a:label="Subsonic"
a:configChanges="orientation|keyboardHidden"
a:launchMode="standard">
<intent-filter>
<action a:name="android.intent.action.MAIN"/>
<category a:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SelectArtistActivity"
a:configChanges="orientation|keyboardHidden"
a:launchMode="standard"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SelectAlbumActivity"
a:configChanges="orientation|keyboardHidden"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SearchActivity"
a:label="@string/search.label"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"
/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SelectPlaylistActivity"
a:label="@string/playlist.label"
a:configChanges="orientation|keyboardHidden"
a:launchMode="standard"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.DownloadActivity"
a:configChanges="keyboardHidden"
a:launchMode="singleTask"
a:theme="@android:style/Theme.NoTitleBar.Fullscreen"
/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.SettingsActivity"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.HelpActivity"
a:label="@string/help.label"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.LyricsActivity"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.EqualizerActivity"
a:label="@string/equalizer.label"
a:configChanges="orientation|keyboardHidden"
a:launchMode="singleTask"/>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.VoiceQueryReceiverActivity"
a:launchMode="singleTask">
<intent-filter>
<action a:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category a:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity a:name="net.sourceforge.subsonic.androidapp.activity.QueryReceiverActivity"
a:launchMode="singleTask">
<intent-filter>
<action a:name="android.intent.action.SEARCH"/>
</intent-filter>
<meta-data a:name="android.app.searchable" a:resource="@xml/searchable"/>
</activity>
<service a:name="net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl" a:label="Subsonic Download Service">
<intent-filter>
<action a:name="net.sourceforge.subsonic.androidapp.CMD_TOGGLEPAUSE" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_PLAY" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_PAUSE" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_NEXT" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_PREVIOUS" />
<action a:name="net.sourceforge.subsonic.androidapp.CMD_STOP" />
</intent-filter>
</service>
<receiver a:name="net.sourceforge.subsonic.androidapp.receiver.MediaButtonIntentReceiver">
<intent-filter a:priority="999">
<action a:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<receiver a:name="net.sourceforge.subsonic.androidapp.receiver.BluetoothIntentReceiver">
<intent-filter>
<action a:name="android.bluetooth.a2dp.action.SINK_STATE_CHANGED"/>
</intent-filter>
</receiver>
<receiver a:name="net.sourceforge.subsonic.androidapp.provider.SubsonicAppWidgetProvider4x1" a:label="Subsonic (4x1)" >
<intent-filter>
<action a:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data a:name="android.appwidget.provider" a:resource="@xml/appwidget_info_4x1"/>
</receiver>
<provider a:name="net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider"
a:authorities="net.sourceforge.subsonic.androidapp.provider.SearchSuggestionProvider"/>
<meta-data a:name="android.app.default_searchable"
a:value="net.sourceforge.subsonic.androidapp.activity.QueryReceiverActivity"/>
</application>
</manifest>

View File

@ -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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
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.
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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<Artist> artists = new ArrayList<Artist>();
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.<Artist>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<String> names = new HashSet<String>();
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<MusicFolder> 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<Playlist> 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<MusicDirectory.Entry> 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<String> 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<File> children = new LinkedList<File>();
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<File> 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 <http://www.gnu.org/licenses/>.
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<Artist> artists = new ArrayList<Artist>();
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.<Artist>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<String> names = new HashSet<String>();
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<MusicFolder> 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<Playlist> 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<MusicDirectory.Entry> 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<String> 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<File> children = new LinkedList<File>();
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<File> children) {
for (File file : FileUtil.listMusicFiles(parent)) {
if (file.isFile()) {
children.add(file);
} else {
listFilesRecursively(file, children);
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
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();
}
});
}
}

View File

@ -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<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
try {
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
sortByAscendingModificationTime(files);
List<File> files = new ArrayList<File>();
List<File> dirs = new ArrayList<File>();
Set<File> undeletable = findUndeletableFiles();
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
sortByAscendingModificationTime(files);
deleteFiles(files, undeletable);
deleteEmptyDirs(dirs, undeletable);
Log.i(TAG, "Completed cache cleaning.");
Set<File> undeletable = findUndeletableFiles();
deleteFiles(files, undeletable);
deleteEmptyDirs(dirs, undeletable);
Log.i(TAG, "Completed cache cleaning.");
} catch (RuntimeException x) {
Log.e(TAG, "Error in cache cleaning.", x);
}
} catch (RuntimeException x) {
Log.e(TAG, "Error in cache cleaning.", x);
}
}
}).start();
}
private void deleteEmptyDirs(List<File> dirs, Set<File> 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()));

View File

@ -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 <http://www.gnu.org/licenses/>.
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<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
private static final File DEFAULT_MUSIC_DIR = createDirectory("music");
public static File getSongFile(Context context, MusicDirectory.Entry song) {
File dir = getAlbumDirectory(context, song);
StringBuilder fileName = new StringBuilder();
Integer track = song.getTrack();
if (track != null) {
if (track < 10) {
fileName.append("0");
}
fileName.append(track).append("-");
}
fileName.append(fileSystemSafe(song.getTitle())).append(".");
if (song.getTranscodedSuffix() != null) {
fileName.append(song.getTranscodedSuffix());
} else {
fileName.append(song.getSuffix());
}
return new File(dir, fileName.toString());
}
public static File getAlbumArtFile(Context context, MusicDirectory.Entry entry) {
File albumDir = getAlbumDirectory(context, entry);
return getAlbumArtFile(albumDir);
}
public static File getAlbumArtFile(File albumDir) {
File albumArtDir = getAlbumArtDirectory();
return new File(albumArtDir, Util.md5Hex(albumDir.getPath()) + ".jpeg");
}
public static Bitmap getAlbumArtBitmap(Context context, MusicDirectory.Entry entry, int size) {
File albumArtFile = getAlbumArtFile(context, entry);
if (albumArtFile.exists()) {
Bitmap bitmap = BitmapFactory.decodeFile(albumArtFile.getPath());
return bitmap == null ? null : Bitmap.createScaledBitmap(bitmap, size, size, true);
}
return null;
}
public static File getAlbumArtDirectory() {
File albumArtDir = new File(getSubsonicDirectory(), "artwork");
ensureDirectoryExistsAndIsReadWritable(albumArtDir);
ensureDirectoryExistsAndIsReadWritable(new File(albumArtDir, ".nomedia"));
return albumArtDir;
}
private static File getAlbumDirectory(Context context, MusicDirectory.Entry entry) {
File dir;
if (entry.getPath() != null) {
File f = new File(fileSystemSafeDir(entry.getPath()));
dir = new File(getMusicDirectory(context).getPath() + "/" + (entry.isDirectory() ? f.getPath() : f.getParent()));
} else {
String artist = fileSystemSafe(entry.getArtist());
String album = fileSystemSafe(entry.getAlbum());
dir = new File(getMusicDirectory(context).getPath() + "/" + artist + "/" + album);
}
return dir;
}
public static void createDirectoryForParent(File file) {
File dir = file.getParentFile();
if (!dir.exists()) {
if (!dir.mkdirs()) {
Log.e(TAG, "Failed to create directory " + dir);
}
}
}
private static File createDirectory(String name) {
File dir = new File(getSubsonicDirectory(), name);
if (!dir.exists() && !dir.mkdirs()) {
Log.e(TAG, "Failed to create " + name);
}
return dir;
}
public static File getSubsonicDirectory() {
return new File(Environment.getExternalStorageDirectory(), "subsonic");
}
public static File getDefaultMusicDirectory() {
return DEFAULT_MUSIC_DIR;
}
public static File getMusicDirectory(Context context) {
String path = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, DEFAULT_MUSIC_DIR.getPath());
File dir = new File(path);
return ensureDirectoryExistsAndIsReadWritable(dir) ? dir : getDefaultMusicDirectory();
}
public static boolean ensureDirectoryExistsAndIsReadWritable(File dir) {
if (dir == null) {
return false;
}
if (dir.exists()) {
if (!dir.isDirectory()) {
Log.w(TAG, dir + " exists but is not a directory.");
return false;
}
} else {
if (dir.mkdirs()) {
Log.i(TAG, "Created directory " + dir);
} else {
Log.w(TAG, "Failed to create directory " + dir);
return false;
}
}
if (!dir.canRead()) {
Log.w(TAG, "No read permission for directory " + dir);
return false;
}
if (!dir.canWrite()) {
Log.w(TAG, "No write permission for directory " + dir);
return false;
}
return true;
}
/**
* Makes a given filename safe by replacing special characters like slashes ("/" and "\")
* with dashes ("-").
*
* @param filename The filename in question.
* @return The filename with special characters replaced by hyphens.
*/
private static String fileSystemSafe(String filename) {
if (filename == null || filename.trim().length() == 0) {
return "unnamed";
}
for (String s : FILE_SYSTEM_UNSAFE) {
filename = filename.replace(s, "-");
}
return filename;
}
/**
* Makes a given filename safe by replacing special characters like colons (":")
* with dashes ("-").
*
* @param path The path of the directory in question.
* @return The the directory name with special characters replaced by hyphens.
*/
private static String fileSystemSafeDir(String path) {
if (path == null || path.trim().length() == 0) {
return "";
}
for (String s : FILE_SYSTEM_UNSAFE_DIR) {
path = path.replace(s, "-");
}
return path;
}
/**
* Similar to {@link File#listFiles()}, but returns a sorted set.
* Never returns {@code null}, instead a warning is logged, and an empty set is returned.
*/
public static SortedSet<File> listFiles(File dir) {
File[] files = dir.listFiles();
if (files == null) {
Log.w(TAG, "Failed to list children for " + dir.getPath());
return new TreeSet<File>();
}
return new TreeSet<File>(Arrays.asList(files));
}
public static SortedSet<File> listMusicFiles(File dir) {
SortedSet<File> files = listFiles(dir);
Iterator<File> iterator = files.iterator();
while (iterator.hasNext()) {
File file = iterator.next();
if (!file.isDirectory() && !isMusicFile(file)) {
iterator.remove();
}
}
return files;
}
private static boolean isMusicFile(File file) {
String extension = getExtension(file.getName());
return MUSIC_FILE_EXTENSIONS.contains(extension);
}
/**
* Returns the extension (the substring after the last dot) of the given file. The dot
* is not included in the returned extension.
*
* @param name The filename in question.
* @return The extension, or an empty string if no extension is found.
*/
public static String getExtension(String name) {
int index = name.lastIndexOf('.');
return index == -1 ? "" : name.substring(index + 1).toLowerCase(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 <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
File file = new File(context.getCacheDir(), fileName);
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(obj);
Log.i(TAG, "Serialized object to " + file);
return true;
} catch (Throwable x) {
Log.w(TAG, "Failed to serialize object to " + file);
return false;
} finally {
Util.close(out);
}
}
public static <T extends Serializable> T deserialize(Context context, String fileName) {
File file = new File(context.getCacheDir(), fileName);
if (!file.exists() || !file.isFile()) {
return null;
}
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new FileInputStream(file));
T result = (T)in.readObject();
Log.i(TAG, "Deserialized object from " + file);
return result;
} catch (Throwable x) {
Log.w(TAG, "Failed to deserialize object from " + file, x);
return null;
} finally {
Util.close(in);
}
}
}
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package 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<String> MUSIC_FILE_EXTENSIONS = Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma");
private static final File DEFAULT_MUSIC_DIR = createDirectory("music");
public static File getSongFile(Context context, MusicDirectory.Entry song) {
File dir = getAlbumDirectory(context, song);
StringBuilder fileName = new StringBuilder();
Integer track = song.getTrack();
if (track != null) {
if (track < 10) {
fileName.append("0");
}
fileName.append(track).append("-");
}
fileName.append(fileSystemSafe(song.getTitle())).append(".");
if (song.getTranscodedSuffix() != null) {
fileName.append(song.getTranscodedSuffix());
} else {
fileName.append(song.getSuffix());
}
return new File(dir, fileName.toString());
}
public static File 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<File> listFiles(File dir) {
File[] files = dir.listFiles();
if (files == null) {
Log.w(TAG, "Failed to list children for " + dir.getPath());
return new TreeSet<File>();
}
return new TreeSet<File>(Arrays.asList(files));
}
public static SortedSet<File> listMusicFiles(File dir) {
SortedSet<File> files = listFiles(dir);
Iterator<File> iterator = files.iterator();
while (iterator.hasNext()) {
File file = iterator.next();
if (!file.isDirectory() && !isMusicFile(file)) {
iterator.remove();
}
}
return files;
}
private static boolean isMusicFile(File file) {
String extension = getExtension(file.getName());
return MUSIC_FILE_EXTENSIONS.contains(extension);
}
/**
* Returns the extension (the substring after the last dot) of the given file. The dot
* is not included in the returned extension.
*
* @param name The filename in question.
* @return The extension, or an empty string if no extension is found.
*/
public static String getExtension(String name) {
int index = name.lastIndexOf('.');
return index == -1 ? "" : name.substring(index + 1).toLowerCase(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 <T extends Serializable> boolean serialize(Context context, T obj, String fileName) {
File file = new File(context.getCacheDir(), fileName);
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(obj);
Log.i(TAG, "Serialized object to " + file);
return true;
} catch (Throwable x) {
Log.w(TAG, "Failed to serialize object to " + file);
return false;
} finally {
Util.close(out);
}
}
public static <T extends Serializable> T deserialize(Context context, String fileName) {
File file = new File(context.getCacheDir(), fileName);
if (!file.exists() || !file.isFile()) {
return null;
}
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new FileInputStream(file));
T result = (T)in.readObject();
Log.i(TAG, "Deserialized object from " + file);
return result;
} catch (Throwable x) {
Log.w(TAG, "Failed to deserialize object from " + file, x);
return null;
} finally {
Util.close(in);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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.
* <p/>
* There should normally be only one instance of this class.
*
* @author Sindre Mehus
*/
public class ImageLoader implements Runnable {
private static final String TAG = ImageLoader.class.getSimpleName();
private static final int CONCURRENCY = 5;
private final LRUCache<String, Drawable> cache = new LRUCache<String, Drawable>(100);
private final BlockingQueue<Task> queue;
private final int imageSizeDefault;
private final int imageSizeLarge;
private Drawable largeUnknownImage;
private Drawable drawable;
public ImageLoader(Context context) {
queue = new LinkedBlockingQueue<Task>(500);
// Determine the density-dependent image sizes.
imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
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 <http://www.gnu.org/licenses/>.
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.
* <p/>
* There should normally be only one instance of this class.
*
* @author Sindre Mehus
*/
public class ImageLoader implements Runnable {
private static final String TAG = ImageLoader.class.getSimpleName();
private static final int CONCURRENCY = 5;
private final LRUCache<String, Drawable> cache = new LRUCache<String, Drawable>(100);
private final BlockingQueue<Task> queue;
private final int imageSizeDefault;
private final int imageSizeLarge;
private Drawable largeUnknownImage;
private Drawable drawable;
public ImageLoader(Context context) {
queue = new LinkedBlockingQueue<Task>(500);
// Determine the density-dependent image sizes.
imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
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);
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>();
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 <http://www.gnu.org/licenses/>.
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<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>();
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();
}
}

File diff suppressed because it is too large Load Diff