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

View File

@ -1,126 +1,127 @@
/* /*
This file is part of Subsonic. This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Subsonic is distributed in the hope that it will be useful, Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>. along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package net.sourceforge.subsonic.androidapp.activity; package net.sourceforge.subsonic.androidapp.activity;
import android.app.Activity; import android.app.Activity;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle; import android.os.Bundle;
import android.view.KeyEvent; import android.util.Log;
import android.view.View; import android.view.KeyEvent;
import android.view.Window; import android.view.View;
import android.webkit.WebView; import android.view.Window;
import android.webkit.WebViewClient; import android.webkit.WebView;
import android.widget.Button; import android.webkit.WebViewClient;
import net.sourceforge.subsonic.androidapp.R; import android.widget.Button;
import net.sourceforge.subsonic.androidapp.util.Util; 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. /**
* * An HTML-based help screen with Back and Done buttons at the bottom.
* @author Sindre Mehus *
*/ * @author Sindre Mehus
public final class HelpActivity extends Activity { */
public final class HelpActivity extends Activity {
private WebView webView; private static final String TAG = HelpActivity.class.getSimpleName();
private Button backButton; private WebView webView;
private Button backButton;
@Override
protected void onCreate(Bundle bundle) { @Override
super.onCreate(bundle); protected void onCreate(Bundle bundle) {
getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); super.onCreate(bundle);
getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.help);
setContentView(R.layout.help);
webView = (WebView) findViewById(R.id.help_contents);
webView.getSettings().setJavaScriptEnabled(true); webView = (WebView) findViewById(R.id.help_contents);
webView.setWebViewClient(new HelpClient()); webView.getSettings().setJavaScriptEnabled(true);
if (bundle != null) { webView.setWebViewClient(new HelpClient());
webView.restoreState(bundle); if (bundle != null) {
} else { webView.restoreState(bundle);
webView.loadUrl(getResources().getString(R.string.help_url)); } else {
} webView.loadUrl(getResources().getString(R.string.help_url));
}
backButton = (Button) findViewById(R.id.help_back);
backButton.setOnClickListener(new Button.OnClickListener() { backButton = (Button) findViewById(R.id.help_back);
@Override backButton.setOnClickListener(new Button.OnClickListener() {
public void onClick(View view) { @Override
webView.goBack(); public void onClick(View view) {
} webView.goBack();
}); }
});
Button doneButton = (Button) findViewById(R.id.help_close);
doneButton.setOnClickListener(new Button.OnClickListener() { Button doneButton = (Button) findViewById(R.id.help_close);
@Override doneButton.setOnClickListener(new Button.OnClickListener() {
public void onClick(View view) { @Override
finish(); public void onClick(View view) {
} finish();
}); }
} });
}
@Override
public void onResume() { @Override
super.onResume(); public void onResume() {
} super.onResume();
}
@Override
protected void onSaveInstanceState(Bundle state) { @Override
webView.saveState(state); protected void onSaveInstanceState(Bundle state) {
} webView.saveState(state);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) { @Override
if (keyCode == KeyEvent.KEYCODE_BACK) { public boolean onKeyDown(int keyCode, KeyEvent event) {
if (webView.canGoBack()) { if (keyCode == KeyEvent.KEYCODE_BACK) {
webView.goBack(); if (webView.canGoBack()) {
return true; webView.goBack();
} return true;
} }
return super.onKeyDown(keyCode, event); }
} return super.onKeyDown(keyCode, event);
}
private final class HelpClient extends WebViewClient {
@Override private final class HelpClient extends WebViewClient {
public void onLoadResource(WebView webView, String url) { @Override
setProgressBarIndeterminateVisibility(true); public void onLoadResource(WebView webView, String url) {
setTitle(getResources().getString(R.string.help_loading)); setProgressBarIndeterminateVisibility(true);
super.onLoadResource(webView, url); setTitle(getResources().getString(R.string.help_loading));
} super.onLoadResource(webView, url);
}
@Override
public void onPageFinished(WebView view, String url) { @Override
setProgressBarIndeterminateVisibility(false); public void onPageFinished(WebView view, String url) {
String versionName = null; setProgressBarIndeterminateVisibility(false);
String versionName = null;
try {
versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; try {
} catch (NameNotFoundException e) { versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
e.printStackTrace(); } catch (NameNotFoundException e) {
} Log.e(TAG, e.getMessage(), e);
}
setTitle(view.getTitle() + " (" + versionName + ")");
backButton.setEnabled(view.canGoBack()); setTitle(view.getTitle() + " (" + versionName + ")");
} backButton.setEnabled(view.canGoBack());
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { @Override
Util.toast(HelpActivity.this, description); 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. This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Subsonic is distributed in the hope that it will be useful, Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>. along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus Copyright 2010 (C) Sindre Mehus
*/ */
package net.sourceforge.subsonic.androidapp.receiver; package net.sourceforge.subsonic.androidapp.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
import net.sourceforge.subsonic.androidapp.util.Util; import net.sourceforge.subsonic.androidapp.util.Util;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class MediaButtonIntentReceiver extends BroadcastReceiver { public class MediaButtonIntentReceiver extends BroadcastReceiver {
private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName(); private static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (Util.getMediaButtonsPreference(context)) { if (Util.getMediaButtonsPreference(context)) {
KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); String intentAction = intent.getAction();
Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction))
Intent serviceIntent = new Intent(context, DownloadServiceImpl.class); return;
serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
context.startService(serviceIntent); KeyEvent event = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT);
Log.i(TAG, "Got MEDIA_BUTTON key event: " + event);
try {
if (isOrderedBroadcast()) { Intent serviceIntent = new Intent(context, DownloadServiceImpl.class);
abortBroadcast(); serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
} context.startService(serviceIntent);
} catch (Exception x) {
// Ignored. try {
} if (isOrderedBroadcast()) {
} abortBroadcast();
} }
} } catch (Exception x) {
// Ignored.
}
}
}
}

View File

@ -1,259 +1,261 @@
/* /*
This file is part of Subsonic. This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Subsonic is distributed in the hope that it will be useful, Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>. along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package net.sourceforge.subsonic.androidapp.service; package net.sourceforge.subsonic.androidapp.service;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import net.sourceforge.subsonic.androidapp.domain.Artist; import android.util.Log;
import net.sourceforge.subsonic.androidapp.domain.Indexes; import net.sourceforge.subsonic.androidapp.domain.Artist;
import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus; import net.sourceforge.subsonic.androidapp.domain.Indexes;
import net.sourceforge.subsonic.androidapp.domain.Lyrics; import net.sourceforge.subsonic.androidapp.domain.JukeboxStatus;
import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; import net.sourceforge.subsonic.androidapp.domain.Lyrics;
import net.sourceforge.subsonic.androidapp.domain.MusicFolder; import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
import net.sourceforge.subsonic.androidapp.domain.Playlist; import net.sourceforge.subsonic.androidapp.domain.MusicFolder;
import net.sourceforge.subsonic.androidapp.domain.SearchCritera; import net.sourceforge.subsonic.androidapp.domain.Playlist;
import net.sourceforge.subsonic.androidapp.domain.SearchResult; import net.sourceforge.subsonic.androidapp.domain.SearchCritera;
import net.sourceforge.subsonic.androidapp.util.Constants; import net.sourceforge.subsonic.androidapp.domain.SearchResult;
import net.sourceforge.subsonic.androidapp.util.FileUtil; import net.sourceforge.subsonic.androidapp.util.Constants;
import net.sourceforge.subsonic.androidapp.util.ProgressListener; import net.sourceforge.subsonic.androidapp.util.FileUtil;
import net.sourceforge.subsonic.androidapp.util.Util; import net.sourceforge.subsonic.androidapp.util.ProgressListener;
import net.sourceforge.subsonic.androidapp.util.Util;
/**
* @author Sindre Mehus /**
*/ * @author Sindre Mehus
public class OfflineMusicService extends RESTMusicService { */
public class OfflineMusicService extends RESTMusicService {
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { @Override
return true; 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 { @Override
List<Artist> artists = new ArrayList<Artist>(); public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
File root = FileUtil.getMusicDirectory(context); List<Artist> artists = new ArrayList<Artist>();
for (File file : FileUtil.listFiles(root)) { File root = FileUtil.getMusicDirectory(context);
if (file.isDirectory()) { for (File file : FileUtil.listFiles(root)) {
Artist artist = new Artist(); if (file.isDirectory()) {
artist.setId(file.getPath()); Artist artist = new Artist();
artist.setIndex(file.getName().substring(0, 1)); artist.setId(file.getPath());
artist.setName(file.getName()); artist.setIndex(file.getName().substring(0, 1));
artists.add(artist); artist.setName(file.getName());
} artists.add(artist);
} }
return new Indexes(0L, Collections.<Artist>emptyList(), artists); }
} return new Indexes(0L, Collections.<Artist>emptyList(), artists);
}
@Override
public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception { @Override
File dir = new File(id); public MusicDirectory getMusicDirectory(String id, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
MusicDirectory result = new MusicDirectory(); File dir = new File(id);
result.setName(dir.getName()); MusicDirectory result = new MusicDirectory();
result.setName(dir.getName());
Set<String> names = new HashSet<String>();
Set<String> names = new HashSet<String>();
for (File file : FileUtil.listMusicFiles(dir)) {
String name = getName(file); for (File file : FileUtil.listMusicFiles(dir)) {
if (name != null & !names.contains(name)) { String name = getName(file);
names.add(name); if (name != null & !names.contains(name)) {
result.addChild(createEntry(context, file, name)); names.add(name);
} result.addChild(createEntry(context, file, name));
} }
return result; }
} return result;
}
private String getName(File file) {
String name = file.getName(); private String getName(File file) {
if (file.isDirectory()) { String name = file.getName();
return name; if (file.isDirectory()) {
} return name;
}
if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) {
return null; if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) {
} return null;
}
name = name.replace(".complete", "");
return FileUtil.getBaseName(name); name = name.replace(".complete", "");
} return FileUtil.getBaseName(name);
}
private MusicDirectory.Entry createEntry(Context context, File file, String name) {
MusicDirectory.Entry entry = new MusicDirectory.Entry(); private MusicDirectory.Entry createEntry(Context context, File file, String name) {
entry.setDirectory(file.isDirectory()); MusicDirectory.Entry entry = new MusicDirectory.Entry();
entry.setId(file.getPath()); entry.setDirectory(file.isDirectory());
entry.setParent(file.getParent()); entry.setId(file.getPath());
entry.setSize(file.length()); entry.setParent(file.getParent());
String root = FileUtil.getMusicDirectory(context).getPath(); entry.setSize(file.length());
entry.setPath(file.getPath().replaceFirst("^" + root + "/" , "")); String root = FileUtil.getMusicDirectory(context).getPath();
if (file.isFile()) { entry.setPath(file.getPath().replaceFirst("^" + root + "/" , ""));
entry.setArtist(file.getParentFile().getParentFile().getName()); if (file.isFile()) {
entry.setAlbum(file.getParentFile().getName()); entry.setArtist(file.getParentFile().getParentFile().getName());
} entry.setAlbum(file.getParentFile().getName());
entry.setTitle(name); }
entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", ""))); entry.setTitle(name);
entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", "")));
File albumArt = FileUtil.getAlbumArtFile(context, entry);
if (albumArt.exists()) { File albumArt = FileUtil.getAlbumArtFile(context, entry);
entry.setCoverArt(albumArt.getPath()); if (albumArt.exists()) {
} entry.setCoverArt(albumArt.getPath());
return entry; }
} return entry;
}
@Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception { @Override
InputStream in = new FileInputStream(entry.getCoverArt()); public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, ProgressListener progressListener) throws Exception {
try { InputStream in = new FileInputStream(entry.getCoverArt());
byte[] bytes = Util.toByteArray(in); try {
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); byte[] bytes = Util.toByteArray(in);
return Bitmap.createScaledBitmap(bitmap, size, size, true); Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
} finally { Log.i("getCoverArt", "getCoverArt");
Util.close(in); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); @Override
List<File> children = new LinkedList<File>(); public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception {
listFilesRecursively(root, children); File root = FileUtil.getMusicDirectory(context);
MusicDirectory result = new MusicDirectory(); List<File> children = new LinkedList<File>();
listFilesRecursively(root, children);
if (children.isEmpty()) { MusicDirectory result = new MusicDirectory();
return result;
} if (children.isEmpty()) {
Random random = new Random(); return result;
for (int i = 0; i < size; i++) { }
File file = children.get(random.nextInt(children.size())); Random random = new Random();
result.addChild(createEntry(context, file, getName(file))); for (int i = 0; i < size; i++) {
} File file = children.get(random.nextInt(children.size()));
result.addChild(createEntry(context, file, getName(file)));
return result; }
}
return result;
private void listFilesRecursively(File parent, List<File> children) { }
for (File file : FileUtil.listMusicFiles(parent)) {
if (file.isFile()) { private void listFilesRecursively(File parent, List<File> children) {
children.add(file); for (File file : FileUtil.listMusicFiles(parent)) {
} else { if (file.isFile()) {
listFilesRecursively(file, children); children.add(file);
} } else {
} listFilesRecursively(file, children);
} }
} }
}
}

View File

@ -1,93 +1,95 @@
/* /*
This file is part of Subsonic. This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Subsonic is distributed in the hope that it will be useful, Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>. along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package net.sourceforge.subsonic.androidapp.util; package net.sourceforge.subsonic.androidapp.util;
import android.content.Context; import android.content.Context;
import android.view.LayoutInflater; import android.util.Log;
import android.view.View; import android.view.LayoutInflater;
import android.widget.ImageView; import android.view.View;
import android.widget.LinearLayout; import android.widget.ImageView;
import android.widget.TextView; import android.widget.LinearLayout;
import net.sourceforge.subsonic.androidapp.R; import android.widget.TextView;
import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; import net.sourceforge.subsonic.androidapp.R;
import net.sourceforge.subsonic.androidapp.service.MusicService; import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; import net.sourceforge.subsonic.androidapp.service.MusicService;
import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
/**
* Used to display albums in a {@code ListView}. /**
* * Used to display albums in a {@code ListView}.
* @author Sindre Mehus *
*/ * @author Sindre Mehus
public class AlbumView extends LinearLayout { */
public class AlbumView extends LinearLayout {
private TextView titleView;
private TextView artistView; private static final String TAG = AlbumView.class.getSimpleName();
private View coverArtView; private TextView titleView;
private ImageView starImageView; private TextView artistView;
private View coverArtView;
public AlbumView(Context context) { private ImageView starImageView;
super(context);
LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true); public AlbumView(Context context) {
super(context);
titleView = (TextView) findViewById(R.id.album_title); LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
artistView = (TextView) findViewById(R.id.album_artist);
coverArtView = findViewById(R.id.album_coverart); titleView = (TextView) findViewById(R.id.album_title);
starImageView = (ImageView) findViewById(R.id.album_star); 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()); public void setAlbum(final MusicDirectory.Entry album, ImageLoader imageLoader) {
artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE); titleView.setText(album.getTitle());
starImageView.setImageDrawable(album.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow)); artistView.setText(album.getArtist());
imageLoader.loadImage(coverArtView, album, false, true); artistView.setVisibility(album.getArtist() == null ? View.GONE : View.VISIBLE);
starImageView.setImageDrawable(album.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow));
starImageView.setOnClickListener(new View.OnClickListener() { imageLoader.loadImage(coverArtView, album, false, true);
@Override
public void onClick(View view) { starImageView.setOnClickListener(new View.OnClickListener() {
final boolean isStarred = album.getStarred(); @Override
final String id = album.getId(); public void onClick(View view) {
final boolean isStarred = album.getStarred();
if (!isStarred) { final String id = album.getId();
starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star));
album.setStarred(true); if (!isStarred) {
} else { starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star));
starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); album.setStarred(true);
album.setStarred(false); } else {
} starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow));
album.setStarred(false);
new Thread(new Runnable() { }
public void run() {
MusicService musicService = MusicServiceFactory.getMusicService(null); new Thread(new Runnable() {
public void run() {
try { MusicService musicService = MusicServiceFactory.getMusicService(null);
if (!isStarred) {
musicService.star(id, getContext(), null); try {
} else { if (!isStarred) {
musicService.unstar(id, getContext(), null); musicService.star(id, getContext(), null);
} } else {
} catch (Exception e) { musicService.unstar(id, getContext(), null);
e.printStackTrace(); }
} } catch (Exception e) {
} Log.e(TAG, e.getMessage(), e);
}).start(); }
} }
}); }).start();
} }
} });
}
}

View File

@ -1,6 +1,9 @@
package net.sourceforge.subsonic.androidapp.util; package net.sourceforge.subsonic.androidapp.util;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -32,31 +35,33 @@ public class CacheCleaner {
} }
public void clean() { 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) { try {
Log.e(TAG, "DownloadService not set. Aborting cache cleaning."); List<File> files = new ArrayList<File>();
return; List<File> dirs = new ArrayList<File>();
}
try { findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs);
sortByAscendingModificationTime(files);
List<File> files = new ArrayList<File>(); Set<File> undeletable = findUndeletableFiles();
List<File> dirs = new ArrayList<File>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, dirs); deleteFiles(files, undeletable);
sortByAscendingModificationTime(files); deleteEmptyDirs(dirs, undeletable);
Log.i(TAG, "Completed cache cleaning.");
Set<File> undeletable = findUndeletableFiles(); } catch (RuntimeException x) {
Log.e(TAG, "Error in cache cleaning.", x);
deleteFiles(files, undeletable); }
deleteEmptyDirs(dirs, undeletable); }
Log.i(TAG, "Completed cache cleaning."); }).start();
} catch (RuntimeException x) {
Log.e(TAG, "Error in cache cleaning.", x);
}
} }
private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) { private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
@ -68,7 +73,7 @@ public class CacheCleaner {
File[] children = dir.listFiles(); File[] children = dir.listFiles();
// Delete empty directory and associated album artwork. // Delete empty directory and associated album artwork.
if (children.length == 0) { if (children != null && children.length == 0) {
Util.delete(dir); Util.delete(dir);
Util.delete(FileUtil.getAlbumArtFile(dir)); Util.delete(FileUtil.getAlbumArtFile(dir));
} }
@ -88,25 +93,31 @@ public class CacheCleaner {
bytesUsedBySubsonic += file.length(); bytesUsedBySubsonic += file.length();
} }
long bytesToDelete = 0;
// Ensure that file system is not more than 95% full. // Ensure that file system is not more than 95% full.
StatFs stat = new StatFs(files.get(0).getPath()); try
long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize(); {
long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize(); StatFs stat = new StatFs(files.get(0).getPath());
long bytesUsedFs = bytesTotalFs - bytesAvailableFs; long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
long minFsAvailability = Math.round(MAX_FILE_SYSTEM_USAGE * (double) bytesTotalFs); 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 bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L); long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit); bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available"); 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 limit : " + Util.formatBytes(cacheSizeBytes));
Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic)); Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete)); Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
} catch (Exception x) {
//
}
long bytesDeleted = 0L; long bytesDeleted = 0L;
for (File file : files) { for (File file : files) {
if (file.getName().equals(Constants.ALBUM_ART_FILE)) { if (file.getName().equals(Constants.ALBUM_ART_FILE)) {
// Move artwork to new folder. // Move artwork to new folder.
file.renameTo(FileUtil.getAlbumArtFile(file.getParentFile())); file.renameTo(FileUtil.getAlbumArtFile(file.getParentFile()));

View File

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

View File

@ -1,247 +1,249 @@
/* /*
This file is part of Subsonic. This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Subsonic is distributed in the hope that it will be useful, Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>. along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package net.sourceforge.subsonic.androidapp.util; package net.sourceforge.subsonic.androidapp.util;
import android.app.ActionBar; import android.app.ActionBar;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.LinearGradient; import android.graphics.LinearGradient;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Shader; import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable; import android.graphics.drawable.TransitionDrawable;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import net.sourceforge.subsonic.androidapp.R; import net.sourceforge.subsonic.androidapp.R;
import net.sourceforge.subsonic.androidapp.activity.DownloadActivity; import net.sourceforge.subsonic.androidapp.activity.DownloadActivity;
import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
import net.sourceforge.subsonic.androidapp.service.MusicService; import net.sourceforge.subsonic.androidapp.service.MusicService;
import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
/** /**
* Asynchronous loading of images, with caching. * Asynchronous loading of images, with caching.
* <p/> * <p/>
* There should normally be only one instance of this class. * There should normally be only one instance of this class.
* *
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class ImageLoader implements Runnable { public class ImageLoader implements Runnable {
private static final String TAG = ImageLoader.class.getSimpleName(); private static final String TAG = ImageLoader.class.getSimpleName();
private static final int CONCURRENCY = 5; private static final int CONCURRENCY = 5;
private final LRUCache<String, Drawable> cache = new LRUCache<String, Drawable>(100); private final LRUCache<String, Drawable> cache = new LRUCache<String, Drawable>(100);
private final BlockingQueue<Task> queue; private final BlockingQueue<Task> queue;
private final int imageSizeDefault; private final int imageSizeDefault;
private final int imageSizeLarge; private final int imageSizeLarge;
private Drawable largeUnknownImage; private Drawable largeUnknownImage;
private Drawable drawable; private Drawable drawable;
public ImageLoader(Context context) { public ImageLoader(Context context) {
queue = new LinkedBlockingQueue<Task>(500); queue = new LinkedBlockingQueue<Task>(500);
// Determine the density-dependent image sizes. // Determine the density-dependent image sizes.
imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight(); imageSizeDefault = context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight();
DisplayMetrics metrics = context.getResources().getDisplayMetrics(); DisplayMetrics metrics = context.getResources().getDisplayMetrics();
imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels)); imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels));
for (int i = 0; i < CONCURRENCY; i++) { for (int i = 0; i < CONCURRENCY; i++) {
new Thread(this, "ImageLoader").start(); new Thread(this, "ImageLoader").start();
} }
createLargeUnknownImage(context); createLargeUnknownImage(context);
} }
private void createLargeUnknownImage(Context context) { private void createLargeUnknownImage(Context context) {
BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large); BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album_large);
Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true); Log.i(TAG, "createLargeUnknownImage");
//bitmap = createReflection(bitmap); Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true);
largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap);
} //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); public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
return; 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) { int size = large ? imageSizeLarge : imageSizeDefault;
setImage(view, drawable, large); Drawable drawable = cache.get(getKey(entry.getCoverArt(), size));
return; if (drawable != null) {
} setImage(view, drawable, large);
return;
if (!large) { }
setUnknownImage(view, large);
} if (!large) {
queue.offer(new Task(view, entry, size, large, large, crossfade)); 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); 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));
final int size = imageSizeLarge;
if (drawable != null) { drawable = cache.get(getKey(entry.getCoverArt(), size));
ab.setLogo(drawable);
} if (drawable != null) {
ab.setLogo(drawable);
final Handler handler = new Handler(){ }
@Override
public void handleMessage(Message msg) { final Handler handler = new Handler(){
drawable = (Drawable) msg.obj; @Override
ab.setLogo(drawable); 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()); new Thread(new Runnable() {
public void run() {
try MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
{
Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, true, null); try
drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap); {
Message msg = Message.obtain(); Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, true, null);
msg.obj = drawable; drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap);
handler.sendMessage(msg); Message msg = Message.obtain();
cache.put(getKey(entry.getCoverArt(), size), drawable); msg.obj = drawable;
} catch (Throwable x) { handler.sendMessage(msg);
Log.e(TAG, "Failed to download album art.", x); cache.put(getKey(entry.getCoverArt(), size), drawable);
} } catch (Throwable x) {
} Log.e(TAG, "Failed to download album art.", x);
}).start(); }
} }
}).start();
private String getKey(String coverArtId, int size) { }
return coverArtId + size;
} 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. private void setImage(View view, Drawable drawable, boolean crossfade) {
TextView textView = (TextView) view; if (view instanceof TextView) {
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); // Cross-fading is not implemented for TextView since it's not in use. It would be easy to add it, though.
} else if (view instanceof ImageView) { TextView textView = (TextView) view;
ImageView imageView = (ImageView) view; textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
if (crossfade) { } else if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
Drawable existingDrawable = imageView.getDrawable(); if (crossfade) {
if (existingDrawable == null) {
Bitmap emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Drawable existingDrawable = imageView.getDrawable();
existingDrawable = new BitmapDrawable(emptyImage); 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); Drawable[] layers = new Drawable[]{existingDrawable, drawable};
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(250); TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
} else { imageView.setImageDrawable(transitionDrawable);
imageView.setImageDrawable(drawable); transitionDrawable.startTransition(250);
} } else {
} imageView.setImageDrawable(drawable);
} }
}
private void setUnknownImage(View view, boolean large) { }
if (large) {
setImage(view, largeUnknownImage, false); private void setUnknownImage(View view, boolean large) {
} else { if (large) {
if (view instanceof TextView) { setImage(view, largeUnknownImage, false);
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0); } else {
} else if (view instanceof ImageView) { if (view instanceof TextView) {
((ImageView) view).setImageResource(R.drawable.unknown_album); ((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();
} public void clear() {
queue.clear();
@Override }
public void run() {
while (true) { @Override
try { public void run() {
Task task = queue.take(); while (true) {
task.execute(); try {
} catch (Throwable x) { Task task = queue.take();
Log.e(TAG, "Unexpected exception in ImageLoader.", x); 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 class Task {
private final Handler handler; private final View view;
private final int size; private final MusicDirectory.Entry entry;
private final boolean reflection; private final Handler handler;
private final boolean saveToFile; private final int size;
private final boolean crossfade; private final boolean reflection;
private final boolean saveToFile;
public Task(View view, MusicDirectory.Entry entry, int size, boolean reflection, boolean saveToFile, boolean crossfade) { private final boolean crossfade;
this.view = view;
this.entry = entry; public Task(View view, MusicDirectory.Entry entry, int size, boolean reflection, boolean saveToFile, boolean crossfade) {
this.size = size; this.view = view;
this.reflection = reflection; this.entry = entry;
this.saveToFile = saveToFile; this.size = size;
this.crossfade = crossfade; this.reflection = reflection;
handler = new Handler(); this.saveToFile = saveToFile;
} this.crossfade = crossfade;
handler = new Handler();
public void execute() { }
try {
MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); public void execute() {
Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, null); try {
MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
if (reflection) { Bitmap bitmap = musicService.getCoverArt(view.getContext(), entry, size, saveToFile, null);
//bitmap = createReflection(bitmap);
} if (reflection) {
//bitmap = createReflection(bitmap);
final Drawable drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap); }
cache.put(getKey(entry.getCoverArt(), size), drawable);
final Drawable drawable = Util.createDrawableFromBitmap(view.getContext(), bitmap);
handler.post(new Runnable() { cache.put(getKey(entry.getCoverArt(), size), drawable);
@Override
public void run() { handler.post(new Runnable() {
setImage(view, drawable, crossfade); @Override
} public void run() {
}); setImage(view, drawable, crossfade);
} catch (Throwable x) { }
Log.e(TAG, "Failed to download album art.", x); });
} } catch (Throwable x) {
} Log.e(TAG, "Failed to download album art.", x);
} }
} }
}
}

View File

@ -1,224 +1,224 @@
/* /*
This file is part of Subsonic. This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Subsonic is distributed in the hope that it will be useful, Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>. along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package net.sourceforge.subsonic.androidapp.util; package net.sourceforge.subsonic.androidapp.util;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Checkable; import android.widget.Checkable;
import android.widget.CheckedTextView; import android.widget.CheckedTextView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import net.sourceforge.subsonic.androidapp.R; import net.sourceforge.subsonic.androidapp.R;
import net.sourceforge.subsonic.androidapp.domain.MusicDirectory; import net.sourceforge.subsonic.androidapp.domain.MusicDirectory;
import net.sourceforge.subsonic.androidapp.service.DownloadService; import net.sourceforge.subsonic.androidapp.service.DownloadService;
import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl; import net.sourceforge.subsonic.androidapp.service.DownloadServiceImpl;
import net.sourceforge.subsonic.androidapp.service.DownloadFile; import net.sourceforge.subsonic.androidapp.service.DownloadFile;
import net.sourceforge.subsonic.androidapp.service.MusicService; import net.sourceforge.subsonic.androidapp.service.MusicService;
import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory; import net.sourceforge.subsonic.androidapp.service.MusicServiceFactory;
import java.io.File; import java.io.File;
import java.util.WeakHashMap; import java.util.WeakHashMap;
/** /**
* Used to display songs in a {@code ListView}. * Used to display songs in a {@code ListView}.
* *
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class SongView extends LinearLayout implements Checkable { public class SongView extends LinearLayout implements Checkable {
private static final String TAG = SongView.class.getSimpleName(); private static final String TAG = SongView.class.getSimpleName();
private static final WeakHashMap<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>(); private static final WeakHashMap<SongView, ?> INSTANCES = new WeakHashMap<SongView, Object>();
private static Handler handler; private static Handler handler;
private CheckedTextView checkedTextView; private CheckedTextView checkedTextView;
private ImageView starImageView; private ImageView starImageView;
private TextView titleTextView; private TextView titleTextView;
private TextView artistTextView; private TextView artistTextView;
private TextView durationTextView; private TextView durationTextView;
private TextView statusTextView; private TextView statusTextView;
private MusicDirectory.Entry song; private MusicDirectory.Entry song;
public SongView(Context context) { public SongView(Context context) {
super(context); super(context);
LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true); LayoutInflater.from(context).inflate(R.layout.song_list_item, this, true);
checkedTextView = (CheckedTextView) findViewById(R.id.song_check); checkedTextView = (CheckedTextView) findViewById(R.id.song_check);
starImageView = (ImageView) findViewById(R.id.song_star); starImageView = (ImageView) findViewById(R.id.song_star);
titleTextView = (TextView) findViewById(R.id.song_title); titleTextView = (TextView) findViewById(R.id.song_title);
artistTextView = (TextView) findViewById(R.id.song_artist); artistTextView = (TextView) findViewById(R.id.song_artist);
durationTextView = (TextView) findViewById(R.id.song_duration); durationTextView = (TextView) findViewById(R.id.song_duration);
statusTextView = (TextView) findViewById(R.id.song_status); statusTextView = (TextView) findViewById(R.id.song_status);
INSTANCES.put(this, null); INSTANCES.put(this, null);
int instanceCount = INSTANCES.size(); int instanceCount = INSTANCES.size();
if (instanceCount > 50) { if (instanceCount > 50) {
Log.w(TAG, instanceCount + " live SongView instances"); Log.w(TAG, instanceCount + " live SongView instances");
} }
startUpdater(); startUpdater();
} }
public void setSong(final MusicDirectory.Entry song, boolean checkable) { public void setSong(final MusicDirectory.Entry song, boolean checkable) {
this.song = song; this.song = song;
StringBuilder artist = new StringBuilder(40); StringBuilder artist = new StringBuilder(40);
String bitRate = null; String bitRate = null;
if (song.getBitRate() != null) { if (song.getBitRate() != null) {
bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate()); bitRate = String.format(getContext().getString(R.string.song_details_kbps), song.getBitRate());
} }
String fileFormat = null; String fileFormat = null;
if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) { if (song.getTranscodedSuffix() != null && !song.getTranscodedSuffix().equals(song.getSuffix())) {
fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix()); fileFormat = String.format("%s > %s", song.getSuffix(), song.getTranscodedSuffix());
} else { } else {
fileFormat = song.getSuffix(); fileFormat = song.getSuffix();
} }
artist.append(song.getArtist()).append(" (") artist.append(song.getArtist()).append(" (")
.append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat)) .append(String.format(getContext().getString(R.string.song_details_all), bitRate == null ? "" : bitRate, fileFormat))
.append(")"); .append(")");
titleTextView.setText(song.getTitle()); titleTextView.setText(song.getTitle());
artistTextView.setText(artist); artistTextView.setText(artist);
durationTextView.setText(Util.formatDuration(song.getDuration())); durationTextView.setText(Util.formatDuration(song.getDuration()));
starImageView.setImageDrawable(song.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow)); starImageView.setImageDrawable(song.getStarred() ? getResources().getDrawable(R.drawable.star) : getResources().getDrawable(R.drawable.star_hollow));
checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE); checkedTextView.setVisibility(checkable && !song.isVideo() ? View.VISIBLE : View.GONE);
starImageView.setOnClickListener(new View.OnClickListener() { starImageView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
final boolean isStarred = song.getStarred(); final boolean isStarred = song.getStarred();
final String id = song.getId(); final String id = song.getId();
if (!isStarred) { if (!isStarred) {
starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star));
song.setStarred(true); song.setStarred(true);
} else { } else {
starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow));
song.setStarred(false); song.setStarred(false);
} }
new Thread(new Runnable() { new Thread(new Runnable() {
public void run() { public void run() {
MusicService musicService = MusicServiceFactory.getMusicService(null); MusicService musicService = MusicServiceFactory.getMusicService(null);
try { try {
if (!isStarred) { if (!isStarred) {
musicService.star(id, getContext(), null); musicService.star(id, getContext(), null);
} else { } else {
musicService.unstar(id, getContext(), null); musicService.unstar(id, getContext(), null);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); Log.e(TAG, e.getMessage(), e);
} }
} }
}).start(); }).start();
} }
}); });
update(); update();
} }
private void update() { private void update() {
DownloadService downloadService = DownloadServiceImpl.getInstance(); DownloadService downloadService = DownloadServiceImpl.getInstance();
if (downloadService == null) { if (downloadService == null) {
return; return;
} }
DownloadFile downloadFile = downloadService.forSong(song); DownloadFile downloadFile = downloadService.forSong(song);
File completeFile = downloadFile.getCompleteFile(); File completeFile = downloadFile.getCompleteFile();
File partialFile = downloadFile.getPartialFile(); File partialFile = downloadFile.getPartialFile();
int leftImage = 0; int leftImage = 0;
int rightImage = 0; int rightImage = 0;
if (completeFile.exists()) { if (completeFile.exists()) {
leftImage = downloadFile.isSaved() ? R.drawable.ic_stat_saved : R.drawable.ic_stat_downloaded; leftImage = downloadFile.isSaved() ? R.drawable.ic_stat_saved : R.drawable.ic_stat_downloaded;
} }
if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) { if (downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && partialFile.exists()) {
statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext())); statusTextView.setText(Util.formatLocalizedBytes(partialFile.length(), getContext()));
rightImage = R.drawable.ic_stat_downloading; rightImage = R.drawable.ic_stat_downloading;
} else { } else {
statusTextView.setText(null); statusTextView.setText(null);
} }
statusTextView.setCompoundDrawablesWithIntrinsicBounds(leftImage, 0, rightImage, 0); statusTextView.setCompoundDrawablesWithIntrinsicBounds(leftImage, 0, rightImage, 0);
if (!song.getStarred()) { if (!song.getStarred()) {
starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow)); starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star_hollow));
} else { } else {
starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star)); starImageView.setImageDrawable(getResources().getDrawable(R.drawable.star));
} }
boolean playing = downloadService.getCurrentPlaying() == downloadFile; boolean playing = downloadService.getCurrentPlaying() == downloadFile;
if (playing) { if (playing) {
titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_stat_play, 0, 0, 0); titleTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_stat_play, 0, 0, 0);
} else { } else {
titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); titleTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
} }
} }
private static synchronized void startUpdater() { private static synchronized void startUpdater() {
if (handler != null) { if (handler != null) {
return; return;
} }
handler = new Handler(); handler = new Handler();
Runnable runnable = new Runnable() { Runnable runnable = new Runnable() {
@Override @Override
public void run() { public void run() {
updateAll(); updateAll();
handler.postDelayed(this, 1000L); handler.postDelayed(this, 1000L);
} }
}; };
handler.postDelayed(runnable, 1000L); handler.postDelayed(runnable, 1000L);
} }
private static void updateAll() { private static void updateAll() {
try { try {
for (SongView view : INSTANCES.keySet()) { for (SongView view : INSTANCES.keySet()) {
if (view.isShown()) { if (view.isShown()) {
view.update(); view.update();
} }
} }
} catch (Throwable x) { } catch (Throwable x) {
Log.w(TAG, "Error when updating song views.", x); Log.w(TAG, "Error when updating song views.", x);
} }
} }
@Override @Override
public void setChecked(boolean b) { public void setChecked(boolean b) {
checkedTextView.setChecked(b); checkedTextView.setChecked(b);
} }
@Override @Override
public boolean isChecked() { public boolean isChecked() {
return checkedTextView.isChecked(); return checkedTextView.isChecked();
} }
@Override @Override
public void toggle() { public void toggle() {
checkedTextView.toggle(); checkedTextView.toggle();
} }
} }

File diff suppressed because it is too large Load Diff