diff --git a/.travis.yml b/.travis.yml index 0739757d5e..d9a30eccc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ android: # - tools # The BuildTools version used by your project. - - build-tools-21.0.2 + - build-tools-21.1.1 # The SDK version used to compile your project. - android-21 diff --git a/JMPDComm/backends/android/build.gradle b/JMPDComm/backends/android/build.gradle index 1feda5dd73..8811202959 100644 --- a/JMPDComm/backends/android/build.gradle +++ b/JMPDComm/backends/android/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' android { compileSdkVersion 21 - buildToolsVersion "21.0.2" + buildToolsVersion "21.1.1" lintOptions { abortOnError false diff --git a/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Album.java b/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Album.java index 24badf06f0..3672b04ef8 100644 --- a/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Album.java +++ b/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Album.java @@ -53,6 +53,10 @@ public Album(final Album otherAlbum) { super(otherAlbum); } + public Album(final Album otherAlbum, final Artist artist, final boolean hasAlbumArtist) { + super(otherAlbum, artist, hasAlbumArtist); + } + public Album(final String name, final Artist artist) { super(name, artist, false, 0L, 0L, 0L, null); } diff --git a/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Music.java b/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Music.java index bfbec404af..af5ba308d2 100644 --- a/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Music.java +++ b/JMPDComm/backends/android/src/main/java/org/a0z/mpd/item/Music.java @@ -57,7 +57,7 @@ protected Music(final Music music) { super(music); } - protected Music(final String album, final String artist, final String albumArtist, + Music(final String album, final String artist, final String albumArtist, final String composer, final String fullPath, final int disc, final long date, final String genre, final long time, final String title, final int totalTracks, final int track, final int songId, final int songPos, final String name) { @@ -84,20 +84,20 @@ public int describeContents() { @Override public void writeToParcel(final Parcel dest, final int flags) { - dest.writeString(getAlbum()); - dest.writeString(getArtist()); - dest.writeString(getAlbumArtist()); - dest.writeString(getComposer()); - dest.writeString(getFullPath()); - dest.writeInt(getDisc()); - dest.writeLong(getDate()); - dest.writeString(getGenre()); - dest.writeLong(getTime()); - dest.writeString(getTitle()); - dest.writeInt(getTotalTracks()); - dest.writeInt(getTrack()); - dest.writeInt(getSongId()); - dest.writeInt(getPos()); - dest.writeString(getName()); + dest.writeString(mAlbum); + dest.writeString(mArtist); + dest.writeString(mAlbumArtist); + dest.writeString(mComposer); + dest.writeString(mFullPath); + dest.writeInt(mDisc); + dest.writeLong(mDate); + dest.writeString(mGenre); + dest.writeLong(mTime); + dest.writeString(mTitle); + dest.writeInt(mTotalTracks); + dest.writeInt(mTrack); + dest.writeInt(mSongId); + dest.writeInt(mSongPos); + dest.writeString(mName); } } diff --git a/JMPDComm/backends/java/src/main/java/org/a0z/mpd/item/Album.java b/JMPDComm/backends/java/src/main/java/org/a0z/mpd/item/Album.java index f3b6c20a59..87741926af 100644 --- a/JMPDComm/backends/java/src/main/java/org/a0z/mpd/item/Album.java +++ b/JMPDComm/backends/java/src/main/java/org/a0z/mpd/item/Album.java @@ -38,6 +38,10 @@ public Album(final Album otherAlbum) { super(otherAlbum); } + public Album(final Album otherAlbum, final Artist artist, final boolean hasAlbumArtist) { + super(otherAlbum, artist, hasAlbumArtist); + } + public Album(final String name, final Artist artist) { super(name, artist, false, 0L, 0L, 0L, null); } diff --git a/JMPDComm/src/main/java/org/a0z/mpd/CommandQueue.java b/JMPDComm/src/main/java/org/a0z/mpd/CommandQueue.java index b490ab0d7d..257bdc4151 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/CommandQueue.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/CommandQueue.java @@ -155,6 +155,10 @@ void clear() { mCommandQueue.clear(); } + public boolean isEmpty() { + return mCommandQueue.isEmpty(); + } + /** Reverse the command queue order, useful for removing playlist entries. */ void reverse() { Collections.reverse(mCommandQueue); @@ -215,6 +219,10 @@ public List sendSeparated(final MPDConnection mpdConnection) return separatedQueueResults(send(mpdConnection, true)); } + public int size() { + return mCommandQueue.size(); + } + /** * Returns the command queue in {@code String} format. * diff --git a/JMPDComm/src/main/java/org/a0z/mpd/MPD.java b/JMPDComm/src/main/java/org/a0z/mpd/MPD.java index 09104424f3..7fab1c89b4 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/MPD.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/MPD.java @@ -187,28 +187,6 @@ private static MPDCommand nextCommand() { return new MPDCommand(MPDCommand.MPD_CMD_NEXT); } - /** - * Parse the response from tag list command for album artists. - * - * @param response The album artist list response from the MPD server database. - * @param substring The substring from the response to remove. - * @param sortInsensitive Whether to sort insensitively. - * @return Returns a parsed album artist list. - */ - private static List parseResponse(final Collection response, - final String substring, final boolean sortInsensitive) { - final List result = new ArrayList<>(response.size()); - for (final String line : response) { - result.add(line.substring((substring + ": ").length())); - } - if (sortInsensitive) { - Collections.sort(result, String.CASE_INSENSITIVE_ORDER); - } else { - Collections.sort(result); - } - return result; - } - private static MPDCommand skipToPositionCommand(final int position) { return new MPDCommand(MPDCommand.MPD_CMD_PLAY, Integer.toString(position)); } @@ -352,7 +330,13 @@ public void add(final CommandQueue commandQueue, final boolean replace, } - commandQueue.send(mConnection); + /** + * It's rare, but possible to make it through the add() + * methods without adding to the command queue. + */ + if (!commandQueue.isEmpty()) { + commandQueue.send(mConnection); + } } /** @@ -388,16 +372,12 @@ public void add(final PlaylistFile databasePlaylist, final boolean replace, fina add(commandQueue, replace, play); } - protected void addAlbumPaths(final List albums) { + protected void addAlbumPaths(final List albums) throws IOException, MPDException { if (albums != null && !albums.isEmpty()) { for (final Album album : albums) { - try { - final List songs = getFirstTrack(album); - if (!songs.isEmpty()) { - album.setPath(songs.get(0).getPath()); - } - } catch (final IOException | MPDException e) { - Log.error(TAG, "Failed to add an album path.", e); + final List songs = getFirstTrack(album); + if (!songs.isEmpty()) { + album.setPath(songs.get(0).getPath()); } } } @@ -611,13 +591,14 @@ protected void fixAlbumArtists(final List albums) { .isEmpty()) { // one albumartist, fix this // album final Artist artist = new Artist(aartists[0]); - albums.set(i, album.setAlbumArtist(artist)); + final Album newAlbum = new Album(album, artist, true); + albums.set(i, newAlbum); } // do nothing if albumartist is "" if (aartists.length > 1) { // it's more than one album, insert for (int n = 1; n < aartists.length; n++) { - final Album newalbum = - new Album(album.getName(), new Artist(aartists[n]), true); - splitAlbums.add(newalbum); + final Artist artist = new Artist(aartists[n]); + final Album newAlbum = new Album(album, artist, true); + splitAlbums.add(newAlbum); } } } @@ -1109,8 +1090,7 @@ public List listAlbumArtists(final boolean sortInsensitive) throws IOException, MPDException { final List response = mConnection.sendCommand(MPDCommand.MPD_CMD_LIST_TAG, MPDCommand.MPD_TAG_ALBUM_ARTIST); - - return parseResponse(response, "albumartist", sortInsensitive); + return Tools.parseResponse(response, "AlbumArtist", sortInsensitive); } public List listAlbumArtists(final Genre genre) throws IOException, MPDException { @@ -1130,7 +1110,7 @@ public List listAlbumArtists(final Genre genre, final boolean sortInsens MPDCommand.MPD_CMD_LIST_TAG, MPDCommand.MPD_TAG_ALBUM_ARTIST, MPDCommand.MPD_TAG_GENRE, genre.getName()); - return parseResponse(response, MPDCommand.MPD_TAG_ALBUM_ARTIST, sortInsensitive); + return Tools.parseResponse(response, "AlbumArtist", sortInsensitive); } public List listAlbumArtists(final List albums) @@ -1290,7 +1270,7 @@ public List listAllAlbumsGrouped(final boolean useAlbumArtist, final List response = mConnection.sendCommand(listAllAlbumsGroupedCommand(useAlbumArtist)); final List result = new ArrayList<>(response.size() / 2); - Album currentAlbum = null; + String currentAlbum = null; if (useAlbumArtist) { artistResponse = "AlbumArtist"; @@ -1299,17 +1279,22 @@ public List listAllAlbumsGrouped(final boolean useAlbumArtist, } for (final String[] pair : Tools.splitResponse(response)) { + if (artistResponse.equals(pair[KEY])) { - // Don't make the check with the other so we don't waste time doing string - // comparisons for nothing. if (currentAlbum != null) { - currentAlbum.setAlbumArtist(new Artist(pair[VALUE])); + final Artist artist = new Artist(pair[VALUE]); + result.add(new Album(currentAlbum, artist, useAlbumArtist)); + + currentAlbum = null; } } else if (albumResponse.equals(pair[KEY])) { + if (currentAlbum != null) { + /** There was no artist in this response, add the album alone */ + result.add(new Album(currentAlbum, null)); + } + if (!pair[VALUE].isEmpty() || includeUnknownAlbum) { - currentAlbum = new Album(pair[VALUE], null); - currentAlbum.setHasAlbumArtist(useAlbumArtist); - result.add(currentAlbum); + currentAlbum = pair[VALUE]; } else { currentAlbum = null; } @@ -1359,7 +1344,7 @@ public List listArtists(final boolean sortInsensitive) final List response = mConnection.sendCommand(MPDCommand.MPD_CMD_LIST_TAG, MPDCommand.MPD_TAG_ARTIST); - return parseResponse(response, "Artist", sortInsensitive); + return Tools.parseResponse(response, "Artist", sortInsensitive); } /** @@ -1435,7 +1420,7 @@ public List listArtists(final String genre, final boolean sortInsensitiv final List response = mConnection.sendCommand(MPDCommand.MPD_CMD_LIST_TAG, MPDCommand.MPD_TAG_ARTIST, MPDCommand.MPD_TAG_GENRE, genre); - return parseResponse(response, "Artist", sortInsensitive); + return Tools.parseResponse(response, "Artist", sortInsensitive); } private List listArtistsCommand(final Iterable albums, @@ -1489,7 +1474,7 @@ public List listGenres(final boolean sortInsensitive) throws IOException final List response = mConnection.sendCommand(MPDCommand.MPD_CMD_LIST_TAG, MPDCommand.MPD_TAG_GENRE); - return parseResponse(response, "Genre", sortInsensitive); + return Tools.parseResponse(response, "Genre", sortInsensitive); } public void movePlaylistSong(final String playlistName, final int from, final int to) diff --git a/JMPDComm/src/main/java/org/a0z/mpd/MPDPlaylist.java b/JMPDComm/src/main/java/org/a0z/mpd/MPDPlaylist.java index cfeef5013f..68a9d58286 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/MPDPlaylist.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/MPDPlaylist.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import java.util.List; /** @@ -187,20 +188,6 @@ public void clear() throws IOException, MPDException { mConnection.sendCommand(clearCommand()); } - /** - * This replaces the entire {@code MusicList} with a full playlist response from the media - * server. - * - * @throws IOException Thrown upon a communication error with the server. - * @throws MPDException Thrown if an error occurs as a result of command execution. - */ - private void fullPlaylistRefresh() throws IOException, MPDException { - final List response = mConnection.sendCommand(MPD_CMD_PLAYLIST_LIST); - final List playlist = Music.getMusicFromList(response, false); - - mList.replace(playlist); - } - /** * Retrieves music at position index in playlist. Operates on local copy of * playlist, may not reflect server's current playlist. @@ -212,6 +199,18 @@ public Music getByIndex(final int index) { return mList.getByIndex(index); } + /** + * This replaces the entire {@code MusicList} with a full playlist response from the media + * server. + * + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. + */ + private Collection getFullPlaylist() throws IOException, MPDException { + final List response = mConnection.sendCommand(MPD_CMD_PLAYLIST_LIST); + return Music.getMusicFromList(response, false); + } + /** * Retrieves all songs as an {@code List} of {@code Music}. * @@ -283,7 +282,8 @@ public void moveByPosition(final int start, final int number, final int to) } /** - * Reloads the playlist content. + * Reloads the playlist content. This is the only place the {@link org.a0z.mpd.MusicList} + * should be modified. * * @param mpdStatus A current {@code MPDStatus} object. * @throws IOException Thrown upon a communication error with the server. @@ -295,26 +295,18 @@ void refresh(final MPDStatus mpdStatus) throws IOException, MPDException { final int newPlaylistVersion = mpdStatus.getPlaylistVersion(); if (mLastPlaylistVersion == -1 || mList.size() == 0) { - fullPlaylistRefresh(); + mList.replace(getFullPlaylist()); } else if (mLastPlaylistVersion != newPlaylistVersion) { final List response = mConnection.sendCommand(MPD_CMD_PLAYLIST_CHANGES, Integer.toString(mLastPlaylistVersion)); - final List changes = Music.getMusicFromList(response, false); - final int playlistLength = mpdStatus.getPlaylistLength(); - final int listSize; - - for (final Music song : changes) { - mList.manipulate(song); - } + final Collection changes = Music.getMusicFromList(response, false); - listSize = mList.size(); - if (playlistLength > listSize) { - Log.warning(TAG, "Race detected, status playlist length > playlist length " + - "after changes have been applied. Reverting to full update."); - fullPlaylistRefresh(); - } else { - mList.removeByRange(mpdStatus.getPlaylistLength(), listSize); + try { + mList.manipulate(changes, mpdStatus.getPlaylistLength()); + } catch (final IllegalStateException e) { + Log.error(TAG, "Partial update failed, running full update.", e); + mList.replace(getFullPlaylist()); } } @@ -332,21 +324,22 @@ void refresh(final MPDStatus mpdStatus) throws IOException, MPDException { */ public void removeAlbumById(final int songId) throws IOException, MPDException { // Better way to get artist of given songId? - final List songs = mList.getMusic(); String artist = ""; String album = ""; int num = 0; boolean usingAlbumArtist = true; - for (final Music song : songs) { - if (song.getSongId() == songId) { - artist = song.getAlbumArtist(); - if (artist == null || artist.isEmpty()) { - usingAlbumArtist = false; - artist = song.getArtist(); + synchronized (mList) { + for (final Music song : mList) { + if (song.getSongId() == songId) { + artist = song.getAlbumArtist(); + if (artist == null || artist.isEmpty()) { + usingAlbumArtist = false; + artist = song.getArtist(); + } + album = song.getAlbum(); + break; } - album = song.getAlbum(); - break; } } @@ -358,9 +351,7 @@ public void removeAlbumById(final int songId) throws IOException, MPDException { /** Don't allow the list to change before we've computed the CommandList. */ synchronized (mList) { - final List tracks = mList.getMusic(); - - for (final Music track : tracks) { + for (final Music track : mList) { if (album.equals(track.getAlbum())) { final boolean songIsAlbumArtist = usingAlbumArtist && artist.equals(track.getAlbumArtist()); @@ -492,9 +483,9 @@ public void swapByPosition(final int song1, final int song2) throws IOException, * @return a string representation of the object. */ public String toString() { - final StringBuilder stringBuilder = new StringBuilder(mList.toString().length()); + final StringBuilder stringBuilder = new StringBuilder(); synchronized (mList) { - for (final Music music : mList.getMusic()) { + for (final Music music : mList) { stringBuilder.append(music); stringBuilder.append(MPDCommand.MPD_CMD_NEWLINE); } diff --git a/JMPDComm/src/main/java/org/a0z/mpd/MPDStatusMonitor.java b/JMPDComm/src/main/java/org/a0z/mpd/MPDStatusMonitor.java index d03ad9f617..ad5f36ff07 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/MPDStatusMonitor.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/MPDStatusMonitor.java @@ -81,12 +81,12 @@ public class MPDStatusMonitor extends Thread { private final long mDelay; - private final MPDCommand mIdleCommand; - private final MPD mMPD; private final Queue mStatusChangeListeners; + private final String[] mSupportedSubsystems; + private final Queue mTrackPositionListeners; private volatile boolean mGiveup; @@ -94,11 +94,11 @@ public class MPDStatusMonitor extends Thread { /** * Constructs a MPDStatusMonitor. * - * @param mpd MPD server to monitor. - * @param delay status query interval. - * @param supportedIdle Idle subsystems to support, see IDLE fields in this class. + * @param mpd MPD server to monitor. + * @param delay status query interval. + * @param supportedSubsystems Idle subsystems to support, see IDLE fields in this class. */ - public MPDStatusMonitor(final MPD mpd, final long delay, final String[] supportedIdle) { + public MPDStatusMonitor(final MPD mpd, final long delay, final String[] supportedSubsystems) { super("MPDStatusMonitor"); mMPD = mpd; @@ -106,7 +106,7 @@ public MPDStatusMonitor(final MPD mpd, final long delay, final String[] supporte mGiveup = false; mStatusChangeListeners = new LinkedList<>(); mTrackPositionListeners = new LinkedList<>(); - mIdleCommand = new MPDCommand(MPDCommand.MPD_CMD_IDLE, supportedIdle); + mSupportedSubsystems = supportedSubsystems.clone(); } /** @@ -309,18 +309,23 @@ public void run() { // connection lost connectionState = Boolean.FALSE; connectionLost = true; + if (mMPD.isConnected()) { + Log.error(TAG, "Exception caught while looping.", e); + } } catch (final MPDException e) { - e.printStackTrace(); + Log.error(TAG, "Exception caught while looping.", e); } } + try { synchronized (this) { - wait(mDelay); + if (!mMPD.isConnected()) { + wait(mDelay); + } } } catch (final InterruptedException e) { - e.printStackTrace(); + Log.error(TAG, "Interruption caught during disconnection and wait.", e); } - } } @@ -334,9 +339,11 @@ public void run() { */ private List waitForChanges() throws IOException, MPDException { final MPDConnection mpdIdleConnection = mMPD.getIdleConnection(); + final MPDCommand idleCommand = new MPDCommand(MPDCommand.MPD_CMD_IDLE, + mSupportedSubsystems); while (mpdIdleConnection != null && mpdIdleConnection.isConnected()) { - final List data = mpdIdleConnection.sendCommand(mIdleCommand); + final List data = mpdIdleConnection.sendCommand(idleCommand); if (data == null || data.isEmpty()) { continue; diff --git a/JMPDComm/src/main/java/org/a0z/mpd/MusicList.java b/JMPDComm/src/main/java/org/a0z/mpd/MusicList.java index 04394bc2ea..12e2d89f0e 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/MusicList.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/MusicList.java @@ -29,43 +29,39 @@ import org.a0z.mpd.item.Music; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; +import java.util.Iterator; import java.util.List; /** * @author Felipe Gustavo de Almeida, Stefan Agner */ -/** The skeleton of the playlist. */ -final class MusicList { +/** + * These lists store the internal structure store of the playlist. All modifications are + * synchronized, iterating over the list will still require manual locking. + */ +final class MusicList implements Iterable { + /** The debug flag, change to true for debugging log output. */ + private static final boolean DEBUG = false; + + /** The debug log identifier. */ private static final String TAG = "MusicList"; + /** The playlist store in positional order. */ private final List mList; - private final AbstractMap mSongIDMap; + /** A list of songIDs in songPos order. */ + private final List mSongID; MusicList() { super(); - mSongIDMap = new HashMap<>(); mList = Collections.synchronizedList(new ArrayList()); - } - - /** - * Constructs a new {@code MusicList} containing all music from - * {@code list}. - * - * @param list a {@code MusicList} - */ - MusicList(final MusicList list) { - this(); - - addAll(list.getMusic()); + mSongID = Collections.synchronizedList(new ArrayList()); } /** @@ -73,37 +69,37 @@ final class MusicList { * * @param music music to be added. */ - void add(final Music music) { - if (getById(music.getSongId()) != null) { - throw new IllegalArgumentException("Music is already on list"); - } - - // add it to the map and at the right position to the list - mSongIDMap.put(Integer.valueOf(music.getSongId()), music); - while (mList.size() < music.getPos() + 1) { - mList.add(null); - } - mList.set(music.getPos(), music); - } - - /** - * Adds all Musics from {@code playlist} to this {@code MusicList} - * . - * - * @param playlist {@code Collection} of {@code Music} to be added - * to this {@code MusicList}. - * @throws ClassCastException when {@code playlist} contains elements - * not assignable to {@code Music}. - */ - void addAll(final Collection playlist) { - mList.addAll(playlist); - } - - /** Removes all {@code Music} objects from this {@code MusicList}. */ - void clear() { + private void add(final Music music) { synchronized (mList) { - mList.clear(); - mSongIDMap.clear(); + final int songPos = music.getPos(); + + if (DEBUG) { + Log.debug(TAG, "listSize: " + mList.size() + " songPos: " + songPos); + } + if (mList.size() == songPos) { + if (DEBUG) { + Log.debug(TAG, "Adding music to the end"); + } + mList.add(music); + mSongID.add(music.getSongId()); + } else { + if (DEBUG) { + Log.debug(TAG, + "Adding beyond the end, or setting within the current playlist."); + } + /** + * Grow the list to the size of the songPos, THEN set it to the position necessary. + * The while loop shouldn't be necessary at all, unless, the result response is out + * of positional order. + */ + while (mList.size() <= songPos) { + mList.add(null); + mSongID.add(null); + } + + mList.set(songPos, music); + mSongID.set(songPos, music.getSongId()); + } } } @@ -115,7 +111,9 @@ void clear() { * present on this {@code MusicList}. */ Music getById(final int songId) { - return mSongIDMap.get(Integer.valueOf(songId)); + final int songPos = Integer.valueOf(mSongID.indexOf(songId)); + + return mList.get(songPos); } /** @@ -145,107 +143,66 @@ List getMusic() { } /** - * Remove a {@code Music} object with given {@code songId} - * from this {@code MusicList}, if it is present. - * Adds or moves a Music item on the {@code MusicList}. + * Returns an {@link java.util.Iterator} for the music list. * - * @param music {@code Music} to be added or moved. + * @return An {@code Iterator} instance. */ - void manipulate(final Music music) { - final int songId = music.getSongId(); - final Integer songIdInteger = Integer.valueOf(songId); - final int songPos = music.getPos(); - - /** SongIDs can't exist in two places at the same time, remove it prior to adding it. */ - if (getById(songId) != null) { - mSongIDMap.remove(songIdInteger); - mList.remove(music); - } - - synchronized (mList) { - // add it to the map and at the right position to the list - mSongIDMap.put(songIdInteger, music); - while (mList.size() < songPos + 1) { - mList.add(null); - } - mList.set(songPos, music); - } + @Override + public Iterator iterator() { + return mList.iterator(); } /** - * Remove music with given {@code songId} from this - * {@code MusicList}, if it is present. + * Modifies the list to reflect the changes coming in from the {@code playlist}. Lock to an + * object prior to running this method. * - * @param songId songId of the {@code Music} to be removed from this - * {@code MusicList}. + * @param musicList The changes to make to the backing stores. + * @param listCapacity The size of the resulting list. */ - void removeById(final int songId) { - final Music music = getById(songId); - - synchronized (mList) { - if (music != null) { - mSongIDMap.remove(Integer.valueOf(songId)); - mList.remove(music); - } + void manipulate(final Iterable musicList, final int listCapacity) { + for (final Music music : musicList) { + /** + * Do not remove from either list. it will be removed by range. + */ + add(music); } - } - /** - * Removes a {@code Music} object at {@code position} from this {@code MusicList}, - * if it is present. - * - * @param index position of the {@code Music} to be removed from this - * {@code MusicList}. - */ - void removeByIndex(final int index) { + /** + * Consistency checks and cleanups. + */ synchronized (mList) { - final Music music = getByIndex(index); - - if (music != null) { - mList.remove(index); - mSongIDMap.remove(Integer.valueOf(music.getSongId())); + final int listSize = mList.size(); + final int songIDSize = mSongID.size(); + if (listSize < listCapacity) { + throw new IllegalStateException( + "List store: " + listSize + " and playlistLength: " + listCapacity + + " size differs."); } - } - } - - /** - * Removes all items sequentially within a specified range from the {@code MusicList}. - * - * @param start The position with which to start removing. - * @param end The final position to remove from the list. - */ - void removeByRange(final int start, final int end) { - if (start < -1 || start > mList.size()) { - throw new IndexOutOfBoundsException("Attempted invalid start range value: " + start + - ", list size: " + mList.size() + '.'); - } - if (end < -1 || end > mList.size()) { - throw new IndexOutOfBoundsException("Attempted invalid end range value: " + end + - ", list size: " + mList.size() + '.'); - } + mList.subList(listCapacity, listSize).clear(); + mSongID.subList(listCapacity, songIDSize).clear(); - int iterator = start; - synchronized (mList) { - while (iterator < end) { - removeByIndex(start); - iterator++; + if (songIDSize != listSize) { + throw new IllegalStateException("List store: " + listSize + + " and SongID: " + songIDSize + " size differs."); } } } /** - * Replace the current {@code MusicList} object. + * Replace all elements in this object. * * @param collection The {@code Music} collection to replace the {@code MusicList} with. */ void replace(final Collection collection) { + synchronized (mList) { - clear(); + mList.clear(); mList.addAll(collection); + mSongID.clear(); for (final Music track : collection) { - mSongIDMap.put(track.getSongId(), track); + mSongID.add(track.getSongId()); } } } diff --git a/JMPDComm/src/main/java/org/a0z/mpd/Tools.java b/JMPDComm/src/main/java/org/a0z/mpd/Tools.java index 46fc844b43..4457a25215 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/Tools.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/Tools.java @@ -28,7 +28,10 @@ package org.a0z.mpd; import java.security.MessageDigest; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; public final class Tools { @@ -162,6 +165,46 @@ public static boolean isNotEqual(final int[][] arrays) { return result; } + /** + * Parse a media server response for one entry type. + * + * @param response The media server response. + * @param type The entry type in the response to add to the collection. + * @return A collection of entries of one type in a media server response. + */ + public static List parseResponse(final Collection response, final String type) { + final List result = new ArrayList<>(response.size()); + + for (final String[] lines : splitResponse(response)) { + if (lines[KEY].equals(type)) { + result.add(lines[VALUE]); + } + } + + return result; + } + + /** + * Parse a media server response for one entry type, then sort the resulting list. + * + * @param response The media server response. + * @param substring The entry type in the response to add to the collection. + * @param sortInsensitive Whether to sort insensitively. + * @return A sorted collection of entries of one type in a media server response. + */ + public static List parseResponse(final Collection response, + final String substring, final boolean sortInsensitive) { + final List result = parseResponse(response, substring); + + if (sortInsensitive) { + Collections.sort(result, String.CASE_INSENSITIVE_ORDER); + } else { + Collections.sort(result); + } + + return result; + } + /** * Split the standard MPD protocol response into a three dimensional array consisting of a * two element String array key / value pairs. diff --git a/JMPDComm/src/main/java/org/a0z/mpd/connection/CommandResult.java b/JMPDComm/src/main/java/org/a0z/mpd/connection/CommandResult.java index f526e3c01e..55dba40f6b 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/connection/CommandResult.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/connection/CommandResult.java @@ -37,32 +37,33 @@ /** This class stores the result for MPDCallable. */ class CommandResult { + private Boolean isIOExceptionLast = null; + private String mConnectionResult; private IOException mIOException; - private MPDException mLastException; + private MPDException mMPDException; private List mResult; - final IOException getIOException() { - return mIOException; + /** + * Returns the first string response from the media server after connection. This method is + * mainly for debugging. + * + * @return A string representation of the connection result. + * @see #getMPDVersion() Use of this method is preferred. + */ + public String getConnectionResult() { + return mConnectionResult; } - final MPDException getLastException() { - return mLastException; + final IOException getIOException() { + return mIOException; } - public boolean isHeaderValid() { - final boolean isHeaderValid; - - if (mConnectionResult == null) { - isHeaderValid = false; - } else { - isHeaderValid = true; - } - - return isHeaderValid; + final MPDException getMPDException() { + return mMPDException; } /** @@ -93,30 +94,34 @@ final List getResult() { return mResult; } - final boolean isMPDException() { - final boolean isMPDException; + public boolean isHeaderValid() { + final boolean isHeaderValid; - if (mLastException == null) { - isMPDException = false; + if (mConnectionResult == null) { + isHeaderValid = false; } else { - isMPDException = true; + isHeaderValid = true; } - return isMPDException; + return isHeaderValid; + } + + public Boolean isIOExceptionLast() { + return isIOExceptionLast; } final void setConnectionResult(final String result) { mConnectionResult = result; } - final void setException(final IOException ioException) { - mLastException = null; - mIOException = ioException; + final void setException(final IOException exception) { + isIOExceptionLast = Boolean.TRUE; + mIOException = exception; } - final void setException(final MPDException lastException) { - mIOException = null; - mLastException = lastException; + final void setException(final MPDException exception) { + isIOExceptionLast = Boolean.FALSE; + mMPDException = exception; } final void setResult(final List result) { diff --git a/JMPDComm/src/main/java/org/a0z/mpd/connection/MPDConnection.java b/JMPDComm/src/main/java/org/a0z/mpd/connection/MPDConnection.java index 297c7afc04..a3b27b2fcf 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/connection/MPDConnection.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/connection/MPDConnection.java @@ -41,7 +41,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.SocketException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -60,6 +59,8 @@ */ public abstract class MPDConnection { + static final String MPD_RESPONSE_OK = "OK"; + private static final int CONNECTION_TIMEOUT = 10000; /** The debug flag to enable or disable debug logging output. */ @@ -73,12 +74,8 @@ public abstract class MPDConnection { private static final String MPD_RESPONSE_ERR = "ACK"; - static final String MPD_RESPONSE_OK = "OK"; - private static final String POOL_THREAD_NAME_PREFIX = "pool"; - private final String mTag; - /** A set containing all available commands, populated on connection. */ private final Collection mAvailableCommands = new HashSet<>(); @@ -91,6 +88,8 @@ public abstract class MPDConnection { /** The command communication timeout. */ private final int mReadWriteTimeout; + private final String mTag; + /** If set to true, this will cancel any processing commands at next opportunity. */ private boolean mCancelled = false; @@ -98,7 +97,7 @@ public abstract class MPDConnection { private boolean mIsConnected = false; /** Current media server's major/minor/micro version. */ - private int[] mMPDVersion = null; + private int[] mMPDVersion = {0, 0, 0}; /** Current media server password. */ private String mPassword = null; @@ -307,21 +306,21 @@ private CommandResult processCommand(final MPDCommand command) } if (result.getResult() == null) { - if (result.isMPDException()) { - throw result.getLastException(); - } else { - final IOException e = result.getIOException(); - - if (e == null) { - if (!mCancelled) { - Log.error(mTag, "There was no result, command was not cancelled, and no " + - "exception generated. This is probably a problem."); - } - } else if (!(mCancelled && e instanceof SocketException && - command.toString().contains(MPDCommand.MPD_CMD_IDLE))) { - /** Don't throw if it's just about a cancelled command. That's expected. */ - throw e; - } + if (result.isIOExceptionLast() == null) { + /** + * This should not occur, and this exception should extend RuntimeException, + * BUT a RuntimeException would most likely not help the situation. + */ + throw new IOException( + "No result, no exception. This is a bug. Please report." + '\n' + + "Cancelled: " + mCancelled + '\n' + + "Command: " + command + '\n' + + "Connected: " + mIsConnected + '\n' + + "Connection result: " + result.getConnectionResult() + '\n'); + } else if (result.isIOExceptionLast().equals(Boolean.TRUE)) { + throw result.getIOException(); + } else if (result.isIOExceptionLast().equals(Boolean.FALSE)) { + throw result.getMPDException(); } } @@ -451,34 +450,6 @@ public final CommandResult call() { return result; } - private void logError(final CommandResult result, final String baseCommand, - final int retryCount) { - final StringBuilder stringBuilder = new StringBuilder(50); - - stringBuilder.append("Command "); - stringBuilder.append(baseCommand); - stringBuilder.append(" failed after "); - stringBuilder.append(retryCount + 1); - - if (retryCount == 0) { - stringBuilder.append(" attempt."); - } else { - stringBuilder.append(" attempts."); - } - - if (result.isMPDException()) { - Log.error(mTag, stringBuilder.toString(), result.getLastException()); - } else { - final IOException e = result.getIOException(); - - /** Don't log if it's just about a cancelled command. That's expected. */ - if (!(mCancelled && e instanceof SocketException && - baseCommand.contains(MPDCommand.MPD_CMD_IDLE))) { - Log.error(mTag, stringBuilder.toString(), e); - } - } - } - /** * Used after a server error, sleeps for a small time then tries to reconnect. * @@ -594,6 +565,30 @@ private boolean isNonfatalACK(final String message) { return isNonfatalACK; } + private void logError(final CommandResult result, final String baseCommand, + final int retryCount) { + final StringBuilder stringBuilder = new StringBuilder(50); + + stringBuilder.append("Command "); + stringBuilder.append(baseCommand); + stringBuilder.append(" failed after "); + stringBuilder.append(retryCount + 1); + + if (retryCount == 0) { + stringBuilder.append(" attempt."); + } else { + stringBuilder.append(" attempts."); + } + + if (result.isIOExceptionLast() == null) { + Log.error(mTag, stringBuilder.toString()); + } else if (result.isIOExceptionLast().equals(Boolean.TRUE)) { + Log.error(mTag, stringBuilder.toString(), result.getIOException()); + } else if (result.isIOExceptionLast().equals(Boolean.FALSE)) { + Log.error(mTag, stringBuilder.toString(), result.getMPDException()); + } + } + /** * Read the server response after a {@code write()} to the server. * diff --git a/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractAlbum.java b/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractAlbum.java index 4d8fd13c79..f15fc887b4 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractAlbum.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractAlbum.java @@ -33,7 +33,7 @@ import java.util.Comparator; /** This class is the generic base for the Album items, abstracted for backend. */ -abstract class AbstractAlbum extends Item { +public abstract class AbstractAlbum extends Item { private final Artist mArtist; @@ -134,6 +134,11 @@ private int formattedYear(final long date) { otherAlbum.mPath); } + AbstractAlbum(final AbstractAlbum album, final Artist artist, final boolean hasAlbumArtist) { + this(album.mName, artist, hasAlbumArtist, album.mSongCount, album.mDuration, album.mYear, + album.mPath); + } + AbstractAlbum(final String name, final Artist artist) { this(name, artist, false, 0L, 0L, 0L, null); } @@ -244,17 +249,6 @@ public int hashCode() { return Arrays.hashCode(new Object[]{mName, mArtist}); } - /** - * This sets the album artist in a new album object, due to the required immutability of name - * and artist to satisfy the requirement that the hash code not change over time. - * - * @param albumArtist The album artist for this album. - * @return A new Album object based off this Album object. - */ - public Album setAlbumArtist(final Artist albumArtist) { - return new Album(mName, albumArtist, true, mSongCount, mDuration, mYear, mPath); - } - public void setDuration(final long duration) { mDuration = duration; } diff --git a/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractMusic.java b/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractMusic.java index 9e34c51d46..4e36786b20 100644 --- a/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractMusic.java +++ b/JMPDComm/src/main/java/org/a0z/mpd/item/AbstractMusic.java @@ -48,7 +48,7 @@ * * @author Felipe Gustavo de Almeida */ -abstract class AbstractMusic extends Item implements FilesystemTreeEntry { +public abstract class AbstractMusic extends Item implements FilesystemTreeEntry { /** * This is like the default {@code Comparable} for the Music class, but it compares without @@ -100,35 +100,35 @@ public int compare(final AbstractMusic lhs, final AbstractMusic rhs) { private static final int UNDEFINED_INT = -1; - private final String mAlbum; + final String mAlbum; - private final String mAlbumArtist; + final String mAlbumArtist; - private final String mArtist; + final String mArtist; - private final String mComposer; + final String mComposer; - private final long mDate; + final long mDate; - private final int mDisc; + final int mDisc; - private final String mFullPath; + final String mFullPath; - private final String mGenre; + final String mGenre; - private final String mName; + final String mName; - private final int mSongId; + final int mSongId; - private final int mSongPos; + final int mSongPos; - private final long mTime; + final long mTime; - private final String mTitle; + final String mTitle; - private final int mTotalTracks; + final int mTotalTracks; - private final int mTrack; + final int mTrack; AbstractMusic() { this(null, /** Album */ @@ -614,12 +614,18 @@ public int getDisc() { * @return filename. */ public String getFilename() { - final int pos = mFullPath.lastIndexOf('/'); - if (pos == -1 || pos == mFullPath.length() - 1) { - return mFullPath; - } else { - return mFullPath.substring(pos + 1); + String result = null; + + if (mFullPath != null) { + final int pos = mFullPath.lastIndexOf('/'); + if (pos == -1 || pos == mFullPath.length() - 1) { + result = mFullPath; + } else { + result = mFullPath.substring(pos + 1); + } } + + return result; } /** diff --git a/MPDroid/MPDroid.iml b/MPDroid/MPDroid.iml index 69c0cf8327..784584d7de 100644 --- a/MPDroid/MPDroid.iml +++ b/MPDroid/MPDroid.iml @@ -107,12 +107,12 @@ - - + + - + diff --git a/MPDroid/build.gradle b/MPDroid/build.gradle index ddca05e604..9412ba48d7 100644 --- a/MPDroid/build.gradle +++ b/MPDroid/build.gradle @@ -22,13 +22,13 @@ def gitShortHash() { android { compileSdkVersion 21 - buildToolsVersion "21.0.2" + buildToolsVersion "21.1.1" defaultConfig { minSdkVersion 14 targetSdkVersion 21 - versionCode 50 - versionName "1.07 RC2 " + gitShortHash() + versionCode 51 + versionName "1.07 RC3 " + gitShortHash() } lintOptions { @@ -79,8 +79,8 @@ android { dependencies { // Support Libraries - compile 'com.android.support:support-v4:21.0.0' - compile 'com.android.support:appcompat-v7:21.0.0' + compile 'com.android.support:support-v4:21.0.2' + compile 'com.android.support:appcompat-v7:21.0.2' // Projects compile project(':JMPDCommAndroid') diff --git a/MPDroid/src/main/AndroidManifest.xml b/MPDroid/src/main/AndroidManifest.xml index b0f95c9368..547d908351 100644 --- a/MPDroid/src/main/AndroidManifest.xml +++ b/MPDroid/src/main/AndroidManifest.xml @@ -54,6 +54,7 @@ android:label="@string/search"> + diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplication.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplication.java index 35c652d8a0..a70cf4c626 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplication.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplication.java @@ -485,14 +485,18 @@ public final void setupServiceBinder() { } private void startDisconnectScheduler() { - mDisconnectScheduler.schedule(new TimerTask() { - @Override - public void run() { - Log.w(TAG, "Disconnecting (" + DISCONNECT_TIMER + " ms timeout)"); - oMPDAsyncHelper.stopStatusMonitor(); - oMPDAsyncHelper.disconnect(); - } - }, DISCONNECT_TIMER); + try { + mDisconnectScheduler.schedule(new TimerTask() { + @Override + public void run() { + Log.w(TAG, "Disconnecting (" + DISCONNECT_TIMER + " ms timeout)"); + oMPDAsyncHelper.stopStatusMonitor(); + oMPDAsyncHelper.disconnect(); + } + }, DISCONNECT_TIMER); + } catch (final IllegalStateException e) { + Log.d(TAG, "Disconnection timer interrupted.", e); + } } public final void startNotification() { diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java index 099d09eacd..8d572ea2c4 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java @@ -76,6 +76,9 @@ public class SearchActivity extends MPDroidActivity implements OnMenuItemClickLi public static final int PLAYLIST = 3; + private static final String PLAY_SERVICES_ACTION_SEARCH + = "com.google.android.gms.actions.SEARCH_ACTION"; + private static final String TAG = "SearchActivity"; private final ArrayList mAlbumResults; @@ -327,7 +330,8 @@ public void onPageSelected(final int position) { final Intent queryIntent = getIntent(); final String queryAction = queryIntent.getAction(); - if (Intent.ACTION_SEARCH.equals(queryAction)) { + if (Intent.ACTION_SEARCH.equals(queryAction) || PLAY_SERVICES_ACTION_SEARCH + .equals(queryAction)) { mSearchKeywords = queryIntent.getStringExtra(SearchManager.QUERY).trim(); final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchRecentProvider.AUTHORITY, SearchRecentProvider.MODE); diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/SettingsFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/SettingsFragment.java index a621091f6f..19e23a9922 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/SettingsFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/SettingsFragment.java @@ -86,26 +86,30 @@ public void onAttach(final Activity activity) { public void onConnectionStateChanged() { final MPD mpd = mApp.oMPDAsyncHelper.oMPD; - mInformationScreen.setEnabled(mpd.isConnected()); - - new Thread(new Runnable() { - @Override - public void run() { - final String versionText = mpd.getMpdVersion(); - final MPDStatistics mpdStatistics = mpd.getStatistics(); - - mHandler.post(new Runnable() { - - @Override - public void run() { - mVersion.setSummary(versionText); - mArtists.setSummary(String.valueOf(mpdStatistics.getArtists())); - mAlbums.setSummary(String.valueOf(mpdStatistics.getAlbums())); - mSongs.setSummary(String.valueOf(mpdStatistics.getSongs())); - } - }); - } - }).start(); + final boolean isConnected = mpd.isConnected(); + + mInformationScreen.setEnabled(isConnected); + + if (isConnected) { + new Thread(new Runnable() { + @Override + public void run() { + final String versionText = mpd.getMpdVersion(); + final MPDStatistics mpdStatistics = mpd.getStatistics(); + + mHandler.post(new Runnable() { + + @Override + public void run() { + mVersion.setSummary(versionText); + mArtists.setSummary(String.valueOf(mpdStatistics.getArtists())); + mAlbums.setSummary(String.valueOf(mpdStatistics.getAlbums())); + mSongs.setSummary(String.valueOf(mpdStatistics.getSongs())); + } + }); + } + }).start(); + } } @Override diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/ArtistsFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/ArtistsFragment.java index 402ad2ad4f..18d4c20a37 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/ArtistsFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/ArtistsFragment.java @@ -28,6 +28,7 @@ import org.a0z.mpd.item.Item; import android.content.SharedPreferences; +import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.StringRes; import android.util.Log; @@ -38,6 +39,8 @@ public class ArtistsFragment extends BrowseFragment { + private static final String EXTRA_GENRE = "genre"; + private static final String TAG = "ArtistsFragment"; private Genre mGenre = null; @@ -125,6 +128,14 @@ public ArtistsFragment init(final Genre g) { return this; } + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + init((Genre) savedInstanceState.getParcelable(EXTRA_GENRE)); + } + } + @Override public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { @@ -138,4 +149,13 @@ public void onItemClick(final AdapterView parent, final View view, final int } ((ILibraryFragmentActivity) getActivity()).pushLibraryFragment(af, "album"); } + + @Override + public void onSaveInstanceState(final Bundle outState) { + if (mGenre != null) { + outState.putParcelable(EXTRA_GENRE, mGenre); + } + super.onSaveInstanceState(outState); + } + } diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragment.java index 9ee81b945e..f56772197c 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragment.java @@ -26,6 +26,10 @@ import org.a0z.mpd.MPDCommand; import org.a0z.mpd.MPDStatus; import org.a0z.mpd.exception.MPDException; +import org.a0z.mpd.item.AbstractAlbum; +import org.a0z.mpd.item.AbstractMusic; +import org.a0z.mpd.item.Album; +import org.a0z.mpd.item.Artist; import org.a0z.mpd.item.Item; import org.a0z.mpd.item.Music; @@ -369,10 +373,20 @@ public boolean onMenuItemClick(final MenuItem item) { final AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); final Object selectedItem = mItems.get((int) info.id); final Intent intent = new Intent(getActivity(), SimpleLibraryActivity.class); - final Music music = (Music) selectedItem; + Artist artist = null; + + if (selectedItem instanceof Album) { + artist = ((AbstractAlbum) selectedItem).getArtist(); + } else if (selectedItem instanceof Artist) { + artist = (Artist) selectedItem; + } else if (selectedItem instanceof Music) { + artist = new Artist(((AbstractMusic) selectedItem).getAlbumArtistOrArtist()); + } - intent.putExtra("artist", music.getArtistAsArtist()); - startActivityForResult(intent, -1); + if (artist != null) { + intent.putExtra("artist", artist); + startActivityForResult(intent, -1); + } break; default: final String name = item.getTitle().toString(); diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/NowPlayingFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/NowPlayingFragment.java index 451cd7a7d9..8cc1f57b84 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/NowPlayingFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/NowPlayingFragment.java @@ -313,6 +313,7 @@ private void forceStatusUpdate() { updateTrackInfo(status, true); setButtonAttribute(getRepeatAttribute(status.isRepeat()), mRepeatButton); setButtonAttribute(getShuffleAttribute(status.isRandom()), mShuffleButton); + setStickerVisibility(); } } @@ -804,6 +805,10 @@ public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, mIsAudioNameTextEnabled = sharedPreferences.getBoolean(key, false); updateAudioNameText(mApp.oMPDAsyncHelper.oMPD.getStatus()); break; + case "enableRating": + setStickerVisibility(); + updateTrackInfo(mApp.oMPDAsyncHelper.oMPD.getStatus(), false); + break; default: break; } @@ -902,6 +907,14 @@ private void setButtonAttribute(@AttrRes final int attribute, final ImageButton ta.recycle(); } + private void setStickerVisibility() { + if (mApp.oMPDAsyncHelper.oMPD.getStickerManager().isAvailable()) { + applyViewVisibility(mSongRating, "enableRating"); + } else { + mSongRating.setVisibility(View.GONE); + } + } + private void startPosTimer(final long start, final long total) { stopPosTimer(); mPosTimer = new Timer(); @@ -958,12 +971,6 @@ private void toggleTrackProgress(final MPDStatus status) { updateTrackProgress(elapsedTime, totalTime); } - if (mApp.oMPDAsyncHelper.oMPD.getStickerManager().isAvailable()) { - applyViewVisibility(mSongRating, "enableRating"); - } else { - mSongRating.setVisibility(View.GONE); - } - mTrackSeekBar.setMax((int) totalTime); mTrackTime.setVisibility(View.VISIBLE); diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/StreamsFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/StreamsFragment.java index 66a8ba0c34..1790aac81c 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/StreamsFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/StreamsFragment.java @@ -20,6 +20,7 @@ import com.namelessdev.mpdroid.tools.StreamFetcher; import com.namelessdev.mpdroid.tools.Tools; +import org.a0z.mpd.MPDCommand; import org.a0z.mpd.exception.MPDException; import org.a0z.mpd.item.Item; import org.a0z.mpd.item.Music; @@ -229,10 +230,16 @@ private void loadStreams() { List mpdStreams = null; int iterator = 0; - try { - mpdStreams = mApp.oMPDAsyncHelper.oMPD.getSavedStreams(); - } catch (final IOException | MPDException e) { - Log.e(TAG, "Failed to retrieve saved streams.", e); + /** Many users have playlist support disabled, no need for an exception. */ + if (mApp.oMPDAsyncHelper.oMPD.isCommandAvailable(MPDCommand.MPD_CMD_LISTPLAYLISTS)) { + try { + mpdStreams = mApp.oMPDAsyncHelper.oMPD.getSavedStreams(); + } catch (final IOException | MPDException e) { + Log.e(TAG, "Failed to retrieve saved streams.", e); + } + } else { + Log.w(TAG, "Streams fragment can't load streams, playlist support not enabled."); + mpdStreams = Collections.emptyList(); } if (null != mpdStreams) { diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/CachedMPD.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/CachedMPD.java index 46ed5e81a3..8aabf0c07e 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/CachedMPD.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/CachedMPD.java @@ -79,9 +79,11 @@ private static String getArtistName(final Artist artist) { * Adds path information to all album objects in a list. * * @param albums List of Album objects to add path information. + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. */ @Override - protected void addAlbumPaths(final List albums) { + protected void addAlbumPaths(final List albums) throws IOException, MPDException { if (!isCached()) { super.addAlbumPaths(albums); return; diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/UpdateTrackInfo.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/UpdateTrackInfo.java index 5d654f2ba4..ccdc848be2 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/UpdateTrackInfo.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/helpers/UpdateTrackInfo.java @@ -212,7 +212,8 @@ protected final Void doInBackground(final MPDStatus... params) { private float getTrackRating() { float rating = 0.0f; - if (mCurrentTrack != null && mSticker.isAvailable()) { + if (mCurrentTrack != null && mSticker.isAvailable() && + mSettings.getBoolean("enableRating", false)) { try { rating = (float) mSticker.getRating(mCurrentTrack) / 2.0f; } catch (final IOException | MPDException e) { diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/models/AbstractPlaylistMusic.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/models/AbstractPlaylistMusic.java index b8f16c582e..e3e7f100c5 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/models/AbstractPlaylistMusic.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/models/AbstractPlaylistMusic.java @@ -24,14 +24,8 @@ public abstract class AbstractPlaylistMusic extends Music { private boolean mForceCoverRefresh = false; - protected AbstractPlaylistMusic(final String album, final String artist, - final String albumartist, final String composer, - final String fullpath, final int disc, final long date, final String genre, - final long time, final String title, - final int totalTracks, final int track, final int songId, final int pos, - final String name) { - super(album, artist, albumartist, composer, fullpath, disc, date, genre, time, title, - totalTracks, track, songId, pos, name); + protected AbstractPlaylistMusic(final Music music) { + super(music); } public int getCurrentSongIconRefID() { diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistSong.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistSong.java index 29271b1d8f..29bc8d4350 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistSong.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistSong.java @@ -27,10 +27,7 @@ public class PlaylistSong extends AbstractPlaylistMusic { public PlaylistSong(final Music music) { - super(music.getAlbum(), music.getArtist(), music.getAlbumArtist(), music.getComposer(), - music.getFullPath(), music.getDisc(), music.getDate(), music.getGenre(), - music.getTime(), music.getTitle(), music.getTotalTracks(), music.getTrack(), - music.getSongId(), music.getPos(), music.getName()); + super(music); } @Override diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistStream.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistStream.java index ffb5ee6d72..813b652a4b 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistStream.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/models/PlaylistStream.java @@ -22,10 +22,7 @@ public class PlaylistStream extends AbstractPlaylistMusic { public PlaylistStream(final Music music) { - super(music.getAlbum(), music.getArtist(), music.getAlbumArtist(), music.getComposer(), - music.getFullPath(), music.getDisc(), music.getDate(), music.getGenre(), - music.getTime(), music.getTitle(), music.getTotalTracks(), music.getTrack(), - music.getSongId(), music.getPos(), music.getName()); + super(music); } @Override diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/service/StreamHandler.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/service/StreamHandler.java index eb0b316c6f..a296216963 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/service/StreamHandler.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/service/StreamHandler.java @@ -23,6 +23,7 @@ import org.a0z.mpd.MPDStatus; import android.content.res.Resources; +import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.MediaPlayer; @@ -30,6 +31,7 @@ import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnInfoListener; import android.media.MediaPlayer.OnPreparedListener; +import android.os.Build; import android.os.Handler; import android.os.Message; import android.os.PowerManager; @@ -690,5 +692,15 @@ private void windUpResources() { mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setWakeMode(mServiceContext, PowerManager.PARTIAL_WAKE_LOCK); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + + mMediaPlayer.setAudioAttributes(audioAttributes); + } } } diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/tools/LibraryTabsUtil.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/tools/LibraryTabsUtil.java index a2ee269429..4bc35b94b1 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/tools/LibraryTabsUtil.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/tools/LibraryTabsUtil.java @@ -21,6 +21,7 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.support.annotation.StringRes; import java.util.ArrayList; import java.util.Arrays; @@ -83,6 +84,7 @@ public static ArrayList getCurrentLibraryTabs() { + LIBRARY_TABS_DELIMITER))); } + @StringRes public static int getTabTitleResId(final String tab) { return TABS.get(tab); } diff --git a/MPDroid/src/main/res/layout-v21/notification.xml b/MPDroid/src/main/res/layout-v21/notification.xml index 0e85cb91e2..221ecba636 100644 --- a/MPDroid/src/main/res/layout-v21/notification.xml +++ b/MPDroid/src/main/res/layout-v21/notification.xml @@ -1,5 +1,4 @@ - - - - - MPDroid - 연주자 - 장르 - 연주목록 - 파일 - 음반 - 설정 - 다음 곡 - 이전 곡 - 확인 - 나가기 - 다시 시도 - 불러오는 중… - 장르 불러오는 중… - 음반 불러오는 중… - 연주자 불러오는 중… - 곡 불러오는 중… - 연주목록 불러오는 중… - 연주목록 - 추가와 교체 - 추가와 교체와 연주 - 음반 추가 - 연주목록 추가 - 추가와 연주 - 연주자 추가 - 장르 추가 - 추가 - 곡 추가 - 여러 곡 지우기 - 잘라내기 - %s 곡이 지워짐 - 지우기 - 주 메뉴 - 연주목록 지워짐 - 연주목록에서 곡 지움 - 음반 %s 추가됨 - 연주목록 %s 추가됨 - 연주목록에 %s 추가됨 - 연주자 %s 추가됨 - 호스트 - MPD 서버 호스트 이름이나 IP 주소 - 스트리밍 호스트 - 스트리밍 서버 호스트 이름 또는 IP 주소 - 포트 - MPD 서버 포트 (기본값: 6600) - 비밀번호 - 서버 비밀번호 - 버전 - - 서버 정보 - 버전과 곡 수 등 각종 MPD 서버 정보를 보여줍니다 - 연결 설정 - MPD 호스트/포트와 비밀번호를 설정합니다 - 출력 - 인터페이스 설정 - 라이브러리 설정 - 곡 %s 추가됨 - 정보 - 연결 안됨 - MPD 서버 연결이 안 됩니다! 서버가 가동 중이고 연결할 수 있는지 확인해야 합니다 (%s) - 연결 - 연결 중… - MPD 서버로 연결 중입니다 - 연결됨 - 연결 안됨 - 곡 정보 없음 - 저장된 범위에 없음 - 연주목록 %s 지워짐 - 연주목록 지우기 - 기본 연결 - 우선 연결 - 무선랜을 선택합니다 - 무선랜 기반 연결 - 기본 연결 설정 - 무선랜 기반의 MPD 호스트/서버와 비밀번호를 설정합니다 - 기본 MPD 호스트/서버와 비밀번호를 설정합니다 - 스트리밍 - 버퍼링… - 스트리밍 포트 - MPD 스트리밍 포트 (기본값: 8000) - 스트리밍 주소 덧붙임 - 스트림 주소 뒤에 덧붙입니다 (예. mpd.mp3) - MPDroid는 오픈 소스 소프트웨어로 소스코드는 http://github.com/abarisain/dmix를 방문하면 됩니다. - MPDroid는 PMix (http://code.google.com/p/pmix/) 에서 나왔습니다. 감사합니다! - 찾기 - MPDroid - 찾기 … - 라이브러리 - 읽을거리 - MPDroid 1.0에 오신 걸 환영합니다!\n이 화면은 한 번만 보입니다.\n\n먼저 MPD 서버에 대하여 모르신다면, 이 앱은 도움이 되지 않으므로 지금 지우시기 바랍니다.\n\n알려진 주요 문제점 : 위젯이 가끔 다운되거나 배터리를 소모할 수 있습니다.\n\n기존 사용자분들의 지원과 기여에 감사합니다. 저는 여러분이 MPDroid를 좋아해 주시기를 진심으로 바랍니다. 싫어하거나 되돌리기를 바라는 부분은 언제라도 메일로 알려주시기 바랍니다 (^_^)\n이전 버전은 여전히 저의 github 페이지에 있습니다. - 수정 - 지금 연주 중 - 취소 - 다음 곡 연주 - 가기… - 처음으로 이동 - 마지막으로 이동 - 연주목록에서 지우기 - 연주목록에서 음반 지우기 - 저장 - MPD 데이터베이스 새로 고침 - 음반 캐시 사용 - MPD 데이터베이스 정보를 로컬에 저장합니다 - 음반 연도 - 음반을 연도로 정리합니다 - 음반 곡 수 - 음반의 곡 수를 보여줍니다 - 로컬 음반표지 내려받기 - MPD 서버에서 음반표지를 내려받습니다 (웹서버가 필요하며, 위키를 보시기 바랍니다!) - 음악 경로 - MPD 서버의 음악 디렉터리, 예) files/mp3 (vortexbox 사용자들은 music/). 복잡한 설정에서는 절대 경로를 써도 됩니다, 예) "http://ip:port/music". - 음반표지 이름 - 음반표지의 기본 파일 이름, 예) folder.jpg (vortexbox 사용자들은 cover.jpg) - 캐시 사용 - 이 서버를 지우기 - Asher (http://kyo-tux.deviantart.com/) 와 brsev (http://brsev.deviantart.com/) 의 원래 앱 아이콘을 MPDroid에 맞게 수정하였습니다. - 연결이 안되므로, 설정을 확인하시기 바랍니다 (%s) - 결과 없음 - 나가려면 뒤로 가기를 다시 누릅니다 - 연주순서 - %s 을 지우는 것이 맞습니까? - %s 가 지워지지 않습니다 - 여러 연주자 - %1$s 곡, %2$s - %1$s 곡, %2$s - 연주목록에 추가 - (새로운 연주목록) - 연주목록 이름 - 새로운 연주목록 이름을 입력합니다: - 스트림 - 스트림 추가 - 스트림 수정 - 스트림 %s 추가됨 - 스트림 읽는 중… - 스트림 %s 지워짐 - 스트림 지우기 - %s 을 지우는 것이 맞습니까? - 이름: - 주소: - 장르 %s 추가됨 - 정지 단추 보기 - 단일 모드 - 소비 모드 - 음반표지 - 나눔 단추 - MPDroid 시작 - 메뉴 단추 - 처리 항목 목록 - 연주 - 정지 - 음반 연도 보기 - 음반연주자 보기 - 오디오속성 보기 - 등급 보기 - 스트리밍 시작 - 스트리밍 중지 - - 전화 받기 - 연주 잠시 멈춤 - 전화 중일 때 연주를 잠시 멈춥니다 - 연주 다시 시작 - 전화가 끝나면 연주를 다시 시작합니다 - - 라이브러리 탭 설정 - 보일 탭과 순서를 바꿉니다 - 보일 탭 - 숨길 탭 - - 음반표지 설정 - 음반표지를 내려받고 저장하는 방법을 바꿉니다 - 음반표지 캐시 - 음반표지를 빨리 찾기 위하여 내려받은 표지를 장치에 저장합니다 - 음반표지 캐시 지우기 - 음반표지 캐시의 저장 공간을 비웁니다 - 음반표지 캐시를 모두 지웁니까? - 로컬 음반 캐시 업데이트 - - 음반표지를 라이브러리에 보기 - 큰 음반표지를 라이브러리에 타일형태로 보여줍니다. 이 기능은 실험적입니다! 느려지거나 실패할 수 있습니다 (음반표지 캐시 필요) - - 음반으로 가기 - 연주자로 가기 - 음반 연주자로 가기 - 폴더로 가기 - 스트림으로 가기 - 현재 곡으로 이동 - 기타 사진 - 이미지 재설정 - - 한 곡이 선택됨 - %s 곡이 선택됨 - 연주순서 찾기… - 와이파이로만 내려받기 - 이동 통신으로 음반표지를 내려받지 않습니다 (요금 절약) - 나가기 전에 확인 - 나가려면 두 번 누릅니다 - 밝은 테마 사용 - 공유 - 지금 연주 중 : - 태블릿 UI 사용 - 바꾸려면 앱을 다시 시작해야 합니다 - 음반표지 내려받기 - 인터넷으로 음반표지를 내려받습니다 (음악정보 보냄) - GraceNote 클라이언트 ID - GraceNote 클라이언트 ID (XXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) - 서랍 보기 - 서랍 닫기 - - 현재 연주 중 설정 - 작은 찾기 사용 - 찾은 이력 지우기 - 스트림 저장됨 - - 저자 - 라이브러리 - 나눔 - 음반표지 - - 실행 선택하기 - - 알림 - 알림 닫기 - 연주 켰다 끄기 - 잠시 멈춤 - 되감기 - 조용히 - 음량 설정 - - 다시 맞추기 - 단순 모드 - 일반 안드로이드 미디어 연주기처럼 연주순서를 관리합니다 - 연주순서에 추가 - - 모두 - 음반연주자 - 연주자 - 프로젝트 아이콘 - 연주자 항목에 쓸 태그 - 연주자 항목을 덧붙이는 데에 사용할 태그를 선택합니다. 편집음반에만 나오는 연주자를 숨기는 데에 유용합니다. - 계속 알림 - 이 네트워크에 머물도록 알림 설정 - 무작위 모드에서 추가된 항목 연주 안 됨. - - - 스트리밍을 할 수 없습니다 : 다른 앱이 사용 중입니다. - %1$s 스트림 오류: %2$s. - 이 오류는 스트림을 준비하는 과정에서 서버 상태가 변경되어서입니다. - 스트림 소스 설정 실패: %1$s. - - - - 미디어 플레이어 미지정 오류. - 미디어 서버 없음. - - 네트워크 관련 운영 실패. - 스트림이 관련 코딩 규격과 맞지 않음. - 운영 시간 초과. - 미디어 프레임워크가 스트림 코넥을 지원하지 않음. - + + + MPDroid + 연주자 + 장르 + 연주목록 + 파일 + 음반 + 설정 + 다음 곡 + 이전 곡 + 확인 + 나가기 + 다시 시도 + 불러오는 중… + 장르 불러오는 중… + 음반 불러오는 중… + 연주자 불러오는 중… + 곡 불러오는 중… + 연주목록 불러오는 중… + 연주목록 + 추가와 교체 + 추가와 교체와 연주 + 음반 추가 + 연주목록 추가 + 추가와 연주 + 연주자 추가 + 장르 추가 + 추가 + 곡 추가 + 여러 곡 지우기 + 잘라내기 + %s 곡이 지워짐 + 지우기 + 주 메뉴 + 연주목록 지워짐 + 연주목록에서 곡 지움 + 음반 %s 추가됨 + 연주목록 %s 추가됨 + 연주목록에 %s 추가됨 + 연주자 %s 추가됨 + 호스트 + MPD 서버 호스트 이름이나 IP 주소 + 스트리밍 호스트 + 스트리밍 서버 호스트 이름 또는 IP 주소 + 포트 + MPD 서버 포트 (기본값: 6600) + 비밀번호 + 서버 비밀번호 + 버전 + + 서버 정보 + 버전과 곡 수 등 각종 MPD 서버 정보를 보여줍니다 + 연결 설정 + MPD 호스트/포트와 비밀번호를 설정합니다 + 출력 + 인터페이스 설정 + 라이브러리 설정 + 곡 %s 추가됨 + 정보 + 연결 안됨 + MPD 서버 연결이 안 됩니다! 서버가 가동 중이고 연결할 수 있는지 확인해야 합니다 (%s) + 연결 + 연결 중… + MPD 서버로 연결 중입니다 + 연결됨 + 연결 안됨 + 곡 정보 없음 + 저장된 범위에 없음 + 연주목록 %s 지워짐 + 연주목록 지우기 + 기본 연결 + 우선 연결 + 무선랜을 선택합니다 + 무선랜 기반 연결 + 기본 연결 설정 + 무선랜 기반의 MPD 호스트/서버와 비밀번호를 설정합니다 + 기본 MPD 호스트/서버와 비밀번호를 설정합니다 + 스트리밍 + 버퍼링… + 스트리밍 포트 + MPD 스트리밍 포트 (기본값: 8000) + 스트리밍 주소 덧붙임 + 스트림 주소 뒤에 덧붙입니다 (예. mpd.mp3) + MPDroid는 오픈 소스 소프트웨어로 소스코드는 http://github.com/abarisain/dmix를 방문하면 됩니다. + MPDroid는 PMix (http://code.google.com/p/pmix/) 에서 나왔습니다. 감사합니다! + 찾기 + MPDroid + 찾기 … + 라이브러리 + 읽을거리 + MPDroid 1.0에 오신 걸 환영합니다!\n이 화면은 한 번만 보입니다.\n\n먼저 MPD 서버에 대하여 모르신다면, 이 앱은 도움이 되지 않으므로 지금 지우시기 바랍니다.\n\n알려진 주요 문제점 : 위젯이 가끔 다운되거나 배터리를 소모할 수 있습니다.\n\n기존 사용자분들의 지원과 기여에 감사합니다. 저는 여러분이 MPDroid를 좋아해 주시기를 진심으로 바랍니다. 싫어하거나 되돌리기를 바라는 부분은 언제라도 메일로 알려주시기 바랍니다 (^_^)\n이전 버전은 여전히 저의 github 페이지에 있습니다. + 수정 + 지금 연주 중 + 취소 + 다음 곡 연주 + 가기… + 처음으로 이동 + 마지막으로 이동 + 연주목록에서 지우기 + 연주목록에서 음반 지우기 + 저장 + MPD 데이터베이스 새로 고침 + 음반 캐시 사용 + MPD 데이터베이스 정보를 로컬에 저장합니다 + 음반 연도 + 음반을 연도로 정리합니다 + 음반 곡 수 + 음반의 곡 수를 보여줍니다 + 로컬 음반표지 내려받기 + MPD 서버에서 음반표지를 내려받습니다 (웹서버가 필요하며, 위키를 보시기 바랍니다!) + 음악 경로 + MPD 서버의 음악 디렉터리, 예) files/mp3 (vortexbox 사용자들은 music/). 복잡한 설정에서는 절대 경로를 써도 됩니다, 예) "http://ip:port/music". + 음반표지 이름 + 음반표지의 기본 파일 이름, 예) folder.jpg (vortexbox 사용자들은 cover.jpg) + 캐시 사용 + 이 서버를 지우기 + Asher (http://kyo-tux.deviantart.com/) 와 brsev (http://brsev.deviantart.com/) 의 원래 앱 아이콘을 MPDroid에 맞게 수정하였습니다. + 연결이 안되므로, 설정을 확인하시기 바랍니다 (%s) + 결과 없음 + 나가려면 뒤로 가기를 다시 누릅니다 + 연주순서 + %s 을 지우는 것이 맞습니까? + %s 가 지워지지 않습니다 + 여러 연주자 + %1$s 곡, %2$s + %1$s 곡, %2$s + 연주목록에 추가 + (새로운 연주목록) + 연주목록 이름 + 새로운 연주목록 이름을 입력합니다: + 스트림 + 스트림 추가 + 스트림 수정 + 스트림 %s 추가됨 + 스트림 읽는 중… + 스트림 %s 지워짐 + 스트림 지우기 + %s 을 지우는 것이 맞습니까? + 이름: + 주소: + 장르 %s 추가됨 + 정지 단추 보기 + 단일 모드 + 소비 모드 + 음반표지 + 나눔 단추 + MPDroid 시작 + 메뉴 단추 + 처리 항목 목록 + 연주 + 정지 + 음반 연도 보기 + 음반연주자 보기 + 오디오속성 보기 + 등급 보기 + 스트리밍 시작 + 스트리밍 중지 + + 전화 받기 + 연주 잠시 멈춤 + 전화 중일 때 연주를 잠시 멈춥니다 + 연주 다시 시작 + 전화가 끝나면 연주를 다시 시작합니다 + + 라이브러리 탭 설정 + 보일 탭과 순서를 바꿉니다 + 보일 탭 + 숨길 탭 + + 음반표지 설정 + 음반표지를 내려받고 저장하는 방법을 바꿉니다 + 음반표지 캐시 + 음반표지를 빨리 찾기 위하여 내려받은 표지를 장치에 저장합니다 + 음반표지 캐시 지우기 + 음반표지 캐시의 저장 공간을 비웁니다 + 음반표지 캐시를 모두 지웁니까? + 로컬 음반 캐시 업데이트 + + 음반표지를 라이브러리에 보기 + 큰 음반표지를 라이브러리에 타일형태로 보여줍니다. 이 기능은 실험적입니다! 느려지거나 실패할 수 있습니다 (음반표지 캐시 필요) + + 음반으로 가기 + 연주자로 가기 + 음반 연주자로 가기 + 폴더로 가기 + 스트림으로 가기 + 지금 곡으로 이동 + 기타 사진 + 이미지 재설정 + + 한 곡이 선택됨 + %s 곡이 선택됨 + 연주순서 찾기… + 와이파이로만 내려받기 + 이동 통신으로 음반표지를 내려받지 않습니다 (요금 절약) + 나가기 전에 확인 + 나가려면 두 번 누릅니다 + 밝은 테마 사용 + 공유 + 지금 연주 중 : + 태블릿 UI 사용 + 바꾸려면 앱을 다시 시작해야 합니다 + 음반표지 내려받기 + 인터넷으로 음반표지를 내려받습니다 (음악정보 보냄) + GraceNote 클라이언트 ID + GraceNote 클라이언트 ID (XXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) + 서랍 보기 + 서랍 닫기 + + 지금 연주 중 설정 + 작은 찾기 사용 + 찾은 이력 지우기 + 스트림 저장됨 + + 저자 + 라이브러리 + 나눔 + 음반표지 + + 실행 선택하기 + + 알림 + 알림 닫기 + 연주 켰다 끄기 + 잠시 멈춤 + 되감기 + 조용히 + 음량 설정 + + 다시 맞추기 + 단순 모드 + 일반 안드로이드 미디어 연주기처럼 연주순서를 관리합니다 + 연주순서에 추가 + + 모두 + 음반연주자 + 연주자 + 프로젝트 아이콘 + 연주자 항목에 쓸 태그 + 연주자 항목을 덧붙이는 데에 사용할 태그를 선택합니다. 편집음반에만 나오는 연주자를 숨기는 데에 유용합니다. + 계속 알림 + 이 네트워크에 머물도록 알림 설정 + 무작위 모드에서 추가된 항목 연주 안 됨. + + + 스트리밍을 할 수 없습니다 : 다른 앱이 사용 중입니다. + %1$s 스트림 오류: %2$s. + 이 오류는 스트림을 준비하는 과정에서 서버 상태가 변경되어서입니다. + 스트림 소스 설정 실패: %1$s. + + + + 미디어 플레이어 미지정 오류. + 미디어 서버 없음. + + 네트워크 관련 운영 실패. + 스트림이 관련 코딩 규격과 맞지 않음. + 운영 시간 초과. + 미디어 프레임워크가 스트림 코덱을 지원하지 않음. + \ No newline at end of file