diff --git a/JMPDComm/src/main/java/com/anpmech/mpd/item/AbstractMusic.java b/JMPDComm/src/main/java/com/anpmech/mpd/item/AbstractMusic.java index 17b824b0e2..fc444a1398 100644 --- a/JMPDComm/src/main/java/com/anpmech/mpd/item/AbstractMusic.java +++ b/JMPDComm/src/main/java/com/anpmech/mpd/item/AbstractMusic.java @@ -357,7 +357,7 @@ public Album getAlbum() { } } - albumBuilder.setSongDetails(getDate(), findValue(RESPONSE_FILE)); + albumBuilder.setSongDetails(getDate(), getParentDirectory()); return albumBuilder.build(); } diff --git a/JMPDComm/src/main/java/com/anpmech/mpd/subsystem/Sticker.java b/JMPDComm/src/main/java/com/anpmech/mpd/subsystem/Sticker.java index bfc259ce2f..8aa05dd76b 100644 --- a/JMPDComm/src/main/java/com/anpmech/mpd/subsystem/Sticker.java +++ b/JMPDComm/src/main/java/com/anpmech/mpd/subsystem/Sticker.java @@ -39,6 +39,8 @@ import com.anpmech.mpd.item.Music; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -137,8 +139,6 @@ public class Sticker { * @param connection The connection to use to query the media server for sticker handling. */ public Sticker(final MPDConnection connection) { - super(); - mConnection = connection; } @@ -154,7 +154,7 @@ private static CommandQueue getMusicCommand(final KeyValueResponse response) { for (final Map.Entry entry : response) { if (CMD_RESPONSE_FILE.equals(entry.getKey())) { - commandQueue.add(MPDCommand.MPD_CMD_LISTALL, entry.getValue()); + commandQueue.add(MPDCommand.MPD_CMD_LISTALLINFO, entry.getValue()); } } @@ -185,16 +185,64 @@ private static void onlyMusicSupported(final FilesystemTreeEntry entry) { * will be removed. * @throws IOException Thrown upon a communication error with the server. * @throws MPDException Thrown if an error occurs as a result of command execution. + * @deprecated Use {@link #delete(String, FilesystemTreeEntry...)} */ + @Deprecated public void delete(final FilesystemTreeEntry entry, final String sticker) throws IOException, MPDException { - onlyMusicSupported(entry); + delete(sticker, entry); + } - if (isAvailable()) { - mConnection.send(CMD_ACTION_DELETE, CMD_STICKER_TYPE_SONG, entry.getFullPath(), - sticker); - } else { + /** + * Deletes a sticker from entries. + * + * @param sticker The sticker key to delete. If null, all stickers associated with this entry + * will be removed. + * @param entries The entry to delete the sticker. + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. + */ + public void delete(final String sticker, final FilesystemTreeEntry... entries) + throws IOException, MPDException { + delete(sticker, Arrays.asList(entries)); + } + + /** + * Deletes a sticker from entries. + * + * @param sticker The sticker key to delete. If null, all stickers associated with this entry + * will be removed. + * @param entries The entries to delete the sticker. + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. + */ + public void delete(final String sticker, + final Collection entries) + throws IOException, MPDException { + if (!isAvailable()) { Log.debug(TAG, STICKERS_NOT_AVAILABLE); + return; + } + + switch (entries.size()) { + case 0: + break; + + case 1: + final FilesystemTreeEntry singleEntry = entries.iterator().next(); + onlyMusicSupported(singleEntry); + mConnection.submit(CMD_ACTION_DELETE, CMD_STICKER_TYPE_SONG, + singleEntry.getFullPath(), sticker); + break; + + default: + final CommandQueue commands = new CommandQueue(); + for (final FilesystemTreeEntry entry : entries) { + onlyMusicSupported(entry); + commands.add(CMD_ACTION_DELETE, CMD_STICKER_TYPE_SONG, + entry.getFullPath(), sticker); + } + mConnection.submit(commands); } } @@ -207,46 +255,60 @@ public void delete(final FilesystemTreeEntry entry, final String sticker) * @return A map of entries 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. + * @deprecated Use {@link #find(String, String)} instead. */ + @Deprecated public Map> find(final FilesystemTreeEntry entry, final String name) throws IOException, MPDException { + //TODO: why are only music entries supported? Directories are generally the URI to find stickers onlyMusicSupported(entry); - final Map> foundStickers; - if (isAvailable()) { - final CommandResult result = - mConnection.submit(CMD_ACTION_FIND, entry.getFullPath(), name).get(); - final KeyValueResponse response = new KeyValueResponse(result); - - /** Generate a map used to create the result. */ - final Map musicPair = getMusicPair(response); - foundStickers = new HashMap<>(musicPair.size()); - final Map currentTrackStickers = new HashMap<>(); - Music currentMusic = null; - - for (final Map.Entry mapEntry : response) { - final String key = mapEntry.getKey(); - - if (CMD_RESPONSE_FILE.equals(key)) { - /** Clear the old map, start new! */ - if (!foundStickers.isEmpty()) { - foundStickers.put(currentMusic, currentTrackStickers); - currentTrackStickers.clear(); - } + final Map foundStickers = find(entry.getFullPath(), name); - currentMusic = musicPair.get(mapEntry.getValue()); - } else if (CMD_RESPONSE_STICKER.equals(key)) { - final String value = mapEntry.getValue(); - final int delimiterIndex = value.indexOf('='); - final String stickerKey = value.substring(0, delimiterIndex); - final String stickerValue = value.substring(delimiterIndex + 1); + final Map> restructuredFoundStickers = new HashMap<>(foundStickers.size()); + for (final Music song : foundStickers.keySet()) { + restructuredFoundStickers.put(song,Collections.singletonMap(name, foundStickers.get(song))); + } + return restructuredFoundStickers; + } - currentTrackStickers.put(stickerKey, stickerValue); - } - } - } else { + /** + * Searches the media server sticker database for matching stickers below the entry given. For + * each matching track, it prints the URI and that one sticker's value. + * + * @param path The to search below in the entry's hierarchy. + * @param name The name to search the stickers for. + * @return A map of entries from the media server with the corresponding sticker values. + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. + */ + public Map find(final String path, + final String name) throws IOException, MPDException { + if (!isAvailable()) { Log.debug(TAG, STICKERS_NOT_AVAILABLE); - foundStickers = Collections.emptyMap(); + return Collections.emptyMap(); + } + + final CommandResult result = + mConnection.submit(CMD_ACTION_FIND, CMD_STICKER_TYPE_SONG, path, name).get(); + final KeyValueResponse response = new KeyValueResponse(result); + + /* Generate a map used to create the result. */ + final Map musicPair = getMusicPair(response); + final Map foundStickers = new HashMap<>(musicPair.size()); + Music currentMusic = null; + + for (final Map.Entry mapEntry : response) { + final String key = mapEntry.getKey(); + + if (CMD_RESPONSE_FILE.equals(key)) { + currentMusic = musicPair.get(mapEntry.getValue()); + } else if (CMD_RESPONSE_STICKER.equals(key)) { + final String value = mapEntry.getValue(); + final int delimiterIndex = value.indexOf('='); + final String stickerValue = value.substring(delimiterIndex + 1); + foundStickers.put(currentMusic, stickerValue); + } } return foundStickers; @@ -309,6 +371,10 @@ public String get(final FilesystemTreeEntry entry, final String name) */ private Map getMusicPair(final KeyValueResponse response) throws IOException, MPDException { + if (response.isEmpty()) { + return Collections.emptyMap(); + } + final CommandResult result = mConnection.submit(getMusicCommand(response)).get(); final Map musicPair = new HashMap<>(); @@ -413,16 +479,64 @@ public Map list(final FilesystemTreeEntry entry) * @param value The sticker value. * @throws IOException Thrown upon a communication error with the server. * @throws MPDException Thrown if an error occurs as a result of command execution. + * @deprecated Use {@link #set(String, String, FilesystemTreeEntry...)} */ + @Deprecated public void set(final FilesystemTreeEntry entry, final String sticker, final String value) throws IOException, MPDException { - onlyMusicSupported(entry); + set(sticker, value, entry); + } - if (isAvailable()) { - mConnection.send(CMD_ACTION_SET, CMD_STICKER_TYPE_SONG, entry.getFullPath(), - sticker, value); - } else { + /** + * Sets a sticker key-value pair. + * + * @param sticker The sticker key. + * @param value The sticker value. + * @param entries The entries with which to associate the sticker key-value pair. + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. + */ + public void set(final String sticker, final String value, final FilesystemTreeEntry... entries) + throws IOException, MPDException { + set(sticker, value, Arrays.asList(entries)); + } + + /** + * Sets a sticker key-value pair. + * + * @param sticker The sticker key. + * @param value The sticker value. + * @param entries The entries with which to associate the sticker key-value pair. + * @throws IOException Thrown upon a communication error with the server. + * @throws MPDException Thrown if an error occurs as a result of command execution. + */ + public void set(final String sticker, final String value, + final Collection entries) + throws IOException, MPDException { + if (!isAvailable()) { Log.debug(TAG, STICKERS_NOT_AVAILABLE); + return; + } + + switch (entries.size()) { + case 0: + break; + + case 1: + final FilesystemTreeEntry singleEntry = entries.iterator().next(); + onlyMusicSupported(singleEntry); + mConnection.submit(CMD_ACTION_SET, CMD_STICKER_TYPE_SONG, + singleEntry.getFullPath(), sticker, value); + break; + + default: + final CommandQueue commands = new CommandQueue(); + for (final FilesystemTreeEntry entry : entries) { + onlyMusicSupported(entry); + commands.add(CMD_ACTION_SET, CMD_STICKER_TYPE_SONG, + entry.getFullPath(), sticker, value); + } + mConnection.submit(commands); } } diff --git a/JMPDComm/src/test/java/com/anpmech/mpd/item/MusicTest.java b/JMPDComm/src/test/java/com/anpmech/mpd/item/MusicTest.java index 49bbc6e203..5cf9f02957 100644 --- a/JMPDComm/src/test/java/com/anpmech/mpd/item/MusicTest.java +++ b/JMPDComm/src/test/java/com/anpmech/mpd/item/MusicTest.java @@ -210,7 +210,7 @@ public void testAlbumMatch() { } else { albumBuilder.setAlbumArtist(albumArtistName); } - final String fullPath = getValue(filePath, AbstractMusic.RESPONSE_FILE); + final String fullPath = mMusicList.get(filePath).getParentDirectory(); final String date = getValue(filePath, AbstractMusic.RESPONSE_DATE); final long parsedDate; diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplicationBase.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplicationBase.java index 59b0c9fb47..9b1f85092a 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplicationBase.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/MPDApplicationBase.java @@ -20,6 +20,7 @@ import com.anpmech.mpd.subsystem.status.IdleSubsystemMonitor; import com.anpmech.mpd.subsystem.status.StatusChangeListener; import com.anpmech.mpd.subsystem.status.TrackPositionListener; +import com.namelessdev.mpdroid.favorites.Favorites; import com.namelessdev.mpdroid.helpers.CachedMPD; import com.namelessdev.mpdroid.helpers.MPDAsyncHelper; import com.namelessdev.mpdroid.helpers.UpdateTrackInfo; @@ -82,6 +83,8 @@ class MPDApplicationBase extends Application implements private MPD mMPD; + private Favorites mFavorites; + private MPDAsyncHelper mMPDAsyncHelper; private ServiceBinder mServiceBinder; @@ -232,6 +235,10 @@ public MPD getMPD() { return mMPD; } + public Favorites getFavorites(){ + return mFavorites; + } + /** * Called upon receiving messages from any handler, in this case most often the Service. * @@ -395,6 +402,8 @@ public void onCreate() { mMPD = new MPD(); } + mFavorites = new Favorites(mMPD); + mIdleSubsystemMonitor = new IdleSubsystemMonitor(mMPD); } diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java index 25ed82ed76..28e3819c7a 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/SearchActivity.java @@ -22,6 +22,7 @@ import com.anpmech.mpd.item.Item; import com.anpmech.mpd.item.Music; import com.namelessdev.mpdroid.adapters.SeparatedListAdapter; +import com.namelessdev.mpdroid.favorites.Favorites; import com.namelessdev.mpdroid.helpers.MPDAsyncHelper.AsyncExecListener; import com.namelessdev.mpdroid.library.SimpleLibraryActivity; import com.namelessdev.mpdroid.tools.Tools; @@ -71,9 +72,9 @@ public class SearchActivity extends MPDActivity implements OnMenuItemClickListen public static final int GOTO_ALBUM = 4; - public static final int MAIN = 0; + //public static final int MAIN = 0; - public static final int PLAYLIST = 3; + //public static final int PLAYLIST = 3; private static final String PLAY_SERVICES_ACTION_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION"; @@ -100,7 +101,7 @@ public class SearchActivity extends MPDActivity implements OnMenuItemClickListen private final ArrayList mSongResults; - protected int mJobID = -1; + //protected int mJobID = -1; protected View mLoadingView; @@ -428,7 +429,7 @@ public void onItemClick(final AdapterView parent, final View view, final int intent.putExtra(Artist.EXTRA, (Parcelable) selectedItem); startActivityForResult(intent, -1); } else if (selectedItem instanceof Album) { - final Intent intent = new Intent(this, SimpleLibraryActivity.class); + final Intent intent = new Intent(this, SimpleLibraryActivity.class); intent.putExtra(Album.EXTRA, (Parcelable) selectedItem); startActivityForResult(intent, -1); } @@ -471,7 +472,8 @@ public boolean onMenuItemClick(final MenuItem item) { intent.putExtra(Album.EXTRA, music.getAlbum()); startActivityForResult(intent, -1); } - } else { + } + else { mApp.getAsyncHelper().execAsync(new Runnable() { @Override public void run() { diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/favorites/Favorites.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/favorites/Favorites.java new file mode 100644 index 0000000000..04cf0d4e32 --- /dev/null +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/favorites/Favorites.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2010-2017 The MPDroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.namelessdev.mpdroid.favorites; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.anpmech.mpd.MPD; +import com.anpmech.mpd.exception.MPDException; +import com.anpmech.mpd.item.Album; +import com.anpmech.mpd.item.Music; +import com.namelessdev.mpdroid.MPDApplication; +import com.namelessdev.mpdroid.R; +import com.namelessdev.mpdroid.tools.Tools; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Favorites { + + /** + * Sticker name for album favorites or just its prefix, if a personalization key is available. + */ + private static final String STICKER_ALBUM_FAVORITE = "albumfav"; + + /** + * Preference key of the personalization key. + */ + private static final String PREFERENCE_FAVORITE_KEY = "favoriteKey"; + + /** + * Preference key of the activation of favorites. + */ + private static final String PREFERENCE_USE_FAVORITE = "useFavorites"; + + /** + * MPD server + */ + private final MPD mMPD; + + public Favorites(final MPD mpd) { + this.mMPD = mpd; + } + + /** + * Marks an album as a favorite. + * @param album Favored album + * @throws IOException + * @throws MPDException + */ + public void addAlbum(final Album album) throws IOException, MPDException { + mMPD.getStickerManager().set(computeFavoriteStickerKey(), "Y", mMPD.getSongs(album)); + Tools.notifyUser(R.string.addToFavorites, album.getName()); + } + + /** + * Removes an album from favorites. + * @param album Album to remove + * @throws IOException + * @throws MPDException + */ + public void removeAlbum(final Album album) throws IOException, MPDException { + mMPD.getStickerManager().delete(computeFavoriteStickerKey(), mMPD.getSongs(album)); + Tools.notifyUser(R.string.removeFromFavorites, album.getName()); + } + + /** + * Determines if an album is favored. + * @param album Album to check + * @return true, if album is favored + * @throws IOException + * @throws MPDException + */ + public boolean isFavorite(final Album album) throws IOException, MPDException { + final List songs = mMPD.getSongs(album); + if (songs.isEmpty()) { + return false; + } + final String favorite = mMPD.getStickerManager().get(songs.get(0), + computeFavoriteStickerKey()); + return favorite != null && favorite.length() > 0; + } + + /** + * Determine all favored albums. + * @return all favored albums + * @throws IOException + * @throws MPDException + */ + public Collection getAlbums() throws IOException, MPDException { + final Set songs = + mMPD.getStickerManager().find("", computeFavoriteStickerKey()).keySet(); + final Set albums = new HashSet<>(); + for (final Music song : songs) { + albums.add(song.getAlbum()); + } + return albums; + } + + /** + * Computes the sticker name for album favorites incl. the personalization key. + * @return Sticker name for album favorites + */ + private static String computeFavoriteStickerKey() { + final SharedPreferences settings = + PreferenceManager.getDefaultSharedPreferences(MPDApplication.getInstance()); + final String personalizationKey = settings.getString(PREFERENCE_FAVORITE_KEY, "").trim(); + return STICKER_ALBUM_FAVORITE + + (!personalizationKey.isEmpty() ? "-" + personalizationKey : ""); + } + + /** + * Are favorites activated in preferences? + * @return true, if activated. + */ + public static boolean areFavoritesActivated() { + final SharedPreferences settings = + PreferenceManager.getDefaultSharedPreferences(MPDApplication.getInstance()); + return settings.getBoolean(PREFERENCE_USE_FAVORITE, false); + } + +} diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/AlbumsFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/AlbumsFragment.java index 63f3ef1e6d..28278a0c13 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/AlbumsFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/AlbumsFragment.java @@ -25,6 +25,7 @@ import com.namelessdev.mpdroid.adapters.ArrayIndexerAdapter; import com.namelessdev.mpdroid.cover.CoverAsyncHelper; import com.namelessdev.mpdroid.cover.CoverManager; +import com.namelessdev.mpdroid.favorites.Favorites; import com.namelessdev.mpdroid.helpers.AlbumInfo; import com.namelessdev.mpdroid.library.ILibraryFragmentActivity; import com.namelessdev.mpdroid.tools.Tools; @@ -50,6 +51,7 @@ import java.io.IOException; import java.util.Collections; +import java.util.List; public class AlbumsFragment extends BrowseFragment { @@ -108,7 +110,8 @@ protected void asyncUpdate() { final boolean sortByYear = settings.getBoolean(ALBUM_YEAR_SORT_KEY, false); try { - replaceItems(mApp.getMPD().getAlbums(mArtist, sortByYear, mIsCountDisplayed)); + //TODO: Why load albums sorted? They become sorted by the following code. + replaceItems(loadAlbums(sortByYear)); if (sortByYear) { Collections.sort(mItems, Album.SORT_BY_DATE); @@ -128,6 +131,10 @@ protected void asyncUpdate() { } } + protected List loadAlbums(final boolean sortByYear) throws IOException, MPDException { + return mApp.getMPD().getAlbums(mArtist, sortByYear, mIsCountDisplayed); + } + /** * Uses CoverManager to clean up a cover. * @@ -228,7 +235,6 @@ public View onCreateView(final LayoutInflater inflater, final ViewGroup containe final View view = super.onCreateView(inflater, container, savedInstanceState); mCoverArtProgress = (ProgressBar) view.findViewById(R.id.albumCoverProgress); return view; - } @Override diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragmentBase.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragmentBase.java index dcfefcd06f..c35dbeadb7 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragmentBase.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/BrowseFragmentBase.java @@ -495,11 +495,11 @@ public void onCreateContextMenu(final ContextMenu menu, final View v, } } - if (getArtist(item) != null) { - final MenuItem gotoArtistItem = - menu.add(GOTO_ARTIST, GOTO_ARTIST, 0, R.string.goToArtist); - gotoArtistItem.setOnMenuItemClickListener(this); - } + //if (getArtist(item) != null) { + // final MenuItem gotoArtistItem = + // menu.add(GOTO_ARTIST, GOTO_ARTIST, 0, R.string.goToArtist); + // gotoArtistItem.setOnMenuItemClickListener(this); + //} } } diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/FavoritesFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/FavoritesFragment.java new file mode 100644 index 0000000000..20256d73a8 --- /dev/null +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/FavoritesFragment.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010-2016 The MPDroid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.namelessdev.mpdroid.fragments; + +import com.anpmech.mpd.exception.MPDException; +import com.anpmech.mpd.item.Album; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FavoritesFragment extends AlbumsGridFragment { + + @Override + protected List loadAlbums(boolean sortByYear) throws IOException, MPDException { + return new ArrayList<>(mApp.getFavorites().getAlbums()); + } + +} diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/LibraryFragmentBase.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/LibraryFragmentBase.java index 9086751bfe..1801fe9e8e 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/LibraryFragmentBase.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/LibraryFragmentBase.java @@ -179,6 +179,9 @@ public Fragment getItem(final int position) { case LibraryTabsUtil.TAB_STREAMS: fragment = getFragment(StreamsFragment.class); break; + case LibraryTabsUtil.TAB_FAVORITES: + fragment = getFragment(FavoritesFragment.class); + break; default: throw new IllegalStateException("getItem() called with invalid Item."); } diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/SongsFragment.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/SongsFragment.java index efc58f6df1..2b7d6a2f00 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/SongsFragment.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/fragments/SongsFragment.java @@ -29,6 +29,7 @@ import com.namelessdev.mpdroid.cover.AlbumCoverDownloadListener; import com.namelessdev.mpdroid.cover.CoverAsyncHelper; import com.namelessdev.mpdroid.cover.CoverManager; +import com.namelessdev.mpdroid.favorites.Favorites; import com.namelessdev.mpdroid.helpers.AlbumInfo; import com.namelessdev.mpdroid.library.SimpleLibraryActivity; import com.namelessdev.mpdroid.tools.Tools; @@ -59,6 +60,7 @@ import android.view.ViewTreeObserver; import android.widget.AbsListView; import android.widget.AdapterView; +import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; @@ -66,6 +68,7 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.ToggleButton; import java.io.IOException; import java.util.Collection; @@ -88,6 +91,10 @@ public class SongsFragment extends BrowseFragment implements FloatingActionButton mAlbumMenu; + private ToggleButton mFavoriteButton; + + private CompoundButton.OnCheckedChangeListener mFavoriteButtonChangeListener; + ImageView mCoverArt; ProgressBar mCoverArtProgress; @@ -530,6 +537,24 @@ public boolean onLongClick(final View v) { } }); + mFavoriteButton.setVisibility(Favorites.areFavoritesActivated() ? View.VISIBLE : View.GONE); + + mFavoriteButtonChangeListener = new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) { + try { + if (isChecked) { + mApp.getFavorites().addAlbum(mAlbum); + } else { + mApp.getFavorites().removeAlbum(mAlbum); + } + } catch (final IOException | MPDException e) { + Log.e(TAG, "Unable to change favorite state of album.", e); + } + } + }; + mFavoriteButton.setOnCheckedChangeListener(mFavoriteButtonChangeListener); + updateFromItems(); updateToolbarVisibility(); @@ -607,6 +632,7 @@ private void populateViews(final View view) { mHeaderToolbar = (Toolbar) view.findViewById(R.id.toolbar); mCoverArtProgress = (ProgressBar) view.findViewById(R.id.albumCoverProgress); mAlbumMenu = (FloatingActionButton) view.findViewById(R.id.album_menu); + mFavoriteButton = (ToggleButton) view.findViewById(R.id.favoriteButton); } @Override @@ -632,45 +658,58 @@ public void updateCover(final AlbumInfo albumInfo) { @Override public void updateFromItems() { super.updateFromItems(); - if (!mItems.isEmpty() && mHeaderArtist != null && mHeaderInfo != null) { - final AlbumInfo fixedAlbumInfo; - fixedAlbumInfo = getFixedAlbumInfo(); - final String artist = fixedAlbumInfo.getArtistName(); - mHeaderArtist.setText(artist); - if (mHeaderAlbum != null) { - mHeaderAlbum.setText(fixedAlbumInfo.getAlbumName()); - } - // Display album year in header - String headerInfos = (String) getHeaderInfoString(); - if (mAlbum != null && mAlbum.getDate() > 0) { - headerInfos = mAlbum.getDate() + ", " + headerInfos; - } - mHeaderInfo.setText(headerInfos); - if (mCoverHelper != null) { - // Delay the cover art download for Lollipop transition - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mFirstRefresh) { - // Hardcode a delay, we don't have a transition end callback ... - // TODO : Refactor this with "onSharedElementEnd", if it's worth it. - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (mCoverHelper != null) { - mCoverHelper.downloadCover(fixedAlbumInfo, true); - } + if (mItems.isEmpty() || mHeaderArtist == null || mHeaderInfo == null) { + return; + } + + final AlbumInfo fixedAlbumInfo; + fixedAlbumInfo = getFixedAlbumInfo(); + final String artist = fixedAlbumInfo.getArtistName(); + mHeaderArtist.setText(artist); + if (mHeaderAlbum != null) { + mHeaderAlbum.setText(fixedAlbumInfo.getAlbumName()); + } + // Display album year in header + String headerInfos = (String) getHeaderInfoString(); + if (mAlbum != null && mAlbum.getDate() > 0) { + headerInfos = mAlbum.getDate() + ", " + headerInfos; + } + mHeaderInfo.setText(headerInfos); + if (mCoverHelper != null) { + // Delay the cover art download for Lollipop transition + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mFirstRefresh) { + // Hardcode a delay, we don't have a transition end callback ... + // TODO : Refactor this with "onSharedElementEnd", if it's worth it. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (mCoverHelper != null) { + mCoverHelper.downloadCover(fixedAlbumInfo, true); } - }, 500L); - } else { - mCoverHelper.downloadCover(fixedAlbumInfo, true); - } + } + }, 500L); } else { - mCoverArtListener.onCoverNotFound(fixedAlbumInfo); + mCoverHelper.downloadCover(fixedAlbumInfo, true); } - mFirstRefresh = false; + } else { + mCoverArtListener.onCoverNotFound(fixedAlbumInfo); + } + mFirstRefresh = false; - // Workaround a kitkat redraw bug, leading to a empty header - if (mTracksInfoContainer != null) { - mTracksInfoContainer.invalidate(); + // Workaround a kitkat redraw bug, leading to a empty header + if (mTracksInfoContainer != null) { + mTracksInfoContainer.invalidate(); + } + + if (Favorites.areFavoritesActivated()) { + try { + mFavoriteButton.setOnCheckedChangeListener(null); // disable change listening + mFavoriteButton.setChecked(mApp.getFavorites().isFavorite(mAlbum)); + mFavoriteButton.setOnCheckedChangeListener(mFavoriteButtonChangeListener); // re-enable change listening + } catch (final IOException | MPDException e) { + Log.e(TAG, "Unable to determine if album is a favorite.", e); } } } + } 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 f030652a1e..248b0504f2 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/tools/LibraryTabsUtil.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/tools/LibraryTabsUtil.java @@ -41,6 +41,8 @@ public final class LibraryTabsUtil { public static final String TAB_STREAMS = "streams"; + public static final String TAB_FAVORITES = "favorites"; + private static final MPDApplication APP = MPDApplication.getInstance(); private static final String LIBRARY_TABS_DELIMITER = "|"; @@ -50,7 +52,8 @@ public final class LibraryTabsUtil { + LIBRARY_TABS_DELIMITER + TAB_PLAYLISTS + LIBRARY_TABS_DELIMITER + TAB_STREAMS + LIBRARY_TABS_DELIMITER + TAB_FILES - + LIBRARY_TABS_DELIMITER + TAB_GENRES; + + LIBRARY_TABS_DELIMITER + TAB_GENRES + + LIBRARY_TABS_DELIMITER + TAB_FAVORITES; private static final String LIBRARY_TABS_SETTINGS_KEY = "currentLibraryTabs"; @@ -63,6 +66,7 @@ public final class LibraryTabsUtil { TABS.put(TAB_STREAMS, R.string.streams); TABS.put(TAB_FILES, R.string.files); TABS.put(TAB_GENRES, R.string.genres); + TABS.put(TAB_FAVORITES, R.string.favorites); } private LibraryTabsUtil() { diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/views/AlbumGridDataBinder.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/views/AlbumGridDataBinder.java index ff66a4f49b..65d20b97eb 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/views/AlbumGridDataBinder.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/views/AlbumGridDataBinder.java @@ -16,17 +16,31 @@ package com.namelessdev.mpdroid.views; +import com.anpmech.mpd.exception.MPDException; import com.anpmech.mpd.item.Album; import com.anpmech.mpd.item.Artist; +import com.namelessdev.mpdroid.MPDApplication; import com.namelessdev.mpdroid.R; import com.namelessdev.mpdroid.cover.CoverAsyncHelper; +import com.namelessdev.mpdroid.favorites.Favorites; import com.namelessdev.mpdroid.helpers.AlbumInfo; +import com.namelessdev.mpdroid.views.holders.AbstractViewHolder; import com.namelessdev.mpdroid.views.holders.AlbumViewHolder; +import android.content.Context; import android.support.annotation.LayoutRes; +import android.util.Log; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.ToggleButton; + +import java.io.IOException; +import java.util.List; public class AlbumGridDataBinder extends AlbumDataBinder { + private static final String TAG = "AlbumGridDataBinder"; + /** * Sole constructor. * @@ -56,4 +70,49 @@ protected void loadAlbumCovers(final AlbumViewHolder holder, final Album album) loadArtwork(coverHelper, albumInfo); } } + + @Override + public AbstractViewHolder findInnerViews(final View targetView) { + final AlbumViewHolder viewHolder = (AlbumViewHolder) super.findInnerViews(targetView); + viewHolder.mFavoriteButton = (ToggleButton) targetView.findViewById(R.id.favoriteButton); + return viewHolder; + } + + @Override + public void onDataBind(Context context, View targetView, AbstractViewHolder viewHolder, List items, Object item, int position) { + super.onDataBind(context, targetView, viewHolder, items, item, position); + + final AlbumViewHolder holder = (AlbumViewHolder) viewHolder; + final Album album = (Album) item; + + holder.mFavoriteButton.setOnCheckedChangeListener(null); + if (Favorites.areFavoritesActivated()) { + holder.mFavoriteButton.setVisibility(View.VISIBLE); + + final MPDApplication app = MPDApplication.getInstance(); + + try { + holder.mFavoriteButton.setChecked(app.getFavorites().isFavorite(album)); + } catch (final IOException | MPDException e) { + Log.e(TAG, "Unable to determine if album is a favorite.", e); + } + + holder.mFavoriteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) { + try { + if (isChecked) { + app.getFavorites().addAlbum(album); + } else { + app.getFavorites().removeAlbum(album); + } + } catch (final IOException | MPDException e) { + Log.e(TAG, "Unable to change favorite state of album.", e); + } + } + }); + } else { + holder.mFavoriteButton.setVisibility(View.GONE); + } + } } diff --git a/MPDroid/src/main/java/com/namelessdev/mpdroid/views/holders/AlbumViewHolder.java b/MPDroid/src/main/java/com/namelessdev/mpdroid/views/holders/AlbumViewHolder.java index a1ed88ae53..934be10ea5 100644 --- a/MPDroid/src/main/java/com/namelessdev/mpdroid/views/holders/AlbumViewHolder.java +++ b/MPDroid/src/main/java/com/namelessdev/mpdroid/views/holders/AlbumViewHolder.java @@ -17,6 +17,7 @@ package com.namelessdev.mpdroid.views.holders; import android.widget.TextView; +import android.widget.ToggleButton; public class AlbumViewHolder extends AlbumCoverHolder { @@ -24,4 +25,6 @@ public class AlbumViewHolder extends AlbumCoverHolder { public TextView mAlbumName; + public ToggleButton mFavoriteButton; + } diff --git a/MPDroid/src/main/res/drawable-hdpi/ic_media_favorite.png b/MPDroid/src/main/res/drawable-hdpi/ic_media_favorite.png new file mode 100644 index 0000000000..a27e760a8d Binary files /dev/null and b/MPDroid/src/main/res/drawable-hdpi/ic_media_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-hdpi/ic_media_no_favorite.png b/MPDroid/src/main/res/drawable-hdpi/ic_media_no_favorite.png new file mode 100644 index 0000000000..a600721656 Binary files /dev/null and b/MPDroid/src/main/res/drawable-hdpi/ic_media_no_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-mdpi/ic_media_favorite.png b/MPDroid/src/main/res/drawable-mdpi/ic_media_favorite.png new file mode 100644 index 0000000000..9b153e98d9 Binary files /dev/null and b/MPDroid/src/main/res/drawable-mdpi/ic_media_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-mdpi/ic_media_no_favorite.png b/MPDroid/src/main/res/drawable-mdpi/ic_media_no_favorite.png new file mode 100644 index 0000000000..7bb1a96698 Binary files /dev/null and b/MPDroid/src/main/res/drawable-mdpi/ic_media_no_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-xhdpi/ic_media_favorite.png b/MPDroid/src/main/res/drawable-xhdpi/ic_media_favorite.png new file mode 100644 index 0000000000..3452b26124 Binary files /dev/null and b/MPDroid/src/main/res/drawable-xhdpi/ic_media_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-xhdpi/ic_media_no_favorite.png b/MPDroid/src/main/res/drawable-xhdpi/ic_media_no_favorite.png new file mode 100644 index 0000000000..cafd6b462a Binary files /dev/null and b/MPDroid/src/main/res/drawable-xhdpi/ic_media_no_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-xxhdpi/ic_media_favorite.png b/MPDroid/src/main/res/drawable-xxhdpi/ic_media_favorite.png new file mode 100644 index 0000000000..df5eb2fb19 Binary files /dev/null and b/MPDroid/src/main/res/drawable-xxhdpi/ic_media_favorite.png differ diff --git a/MPDroid/src/main/res/drawable-xxhdpi/ic_media_no_favorite.png b/MPDroid/src/main/res/drawable-xxhdpi/ic_media_no_favorite.png new file mode 100644 index 0000000000..99e619657e Binary files /dev/null and b/MPDroid/src/main/res/drawable-xxhdpi/ic_media_no_favorite.png differ diff --git a/MPDroid/src/main/res/drawable/favorite.xml b/MPDroid/src/main/res/drawable/favorite.xml new file mode 100644 index 0000000000..c657bb6ed2 --- /dev/null +++ b/MPDroid/src/main/res/drawable/favorite.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/MPDroid/src/main/res/layout-land/song_header.xml b/MPDroid/src/main/res/layout-land/song_header.xml index e131ba06b7..5124a684c8 100644 --- a/MPDroid/src/main/res/layout-land/song_header.xml +++ b/MPDroid/src/main/res/layout-land/song_header.xml @@ -106,4 +106,18 @@ android:paddingRight="8dp" android:src="@drawable/ic_media_play" app:borderWidth="0dp" /> + + + \ No newline at end of file diff --git a/MPDroid/src/main/res/layout/album_grid_item.xml b/MPDroid/src/main/res/layout/album_grid_item.xml index 828592c5e7..afa414164b 100644 --- a/MPDroid/src/main/res/layout/album_grid_item.xml +++ b/MPDroid/src/main/res/layout/album_grid_item.xml @@ -74,7 +74,7 @@ android:paddingRight="10dp" android:singleLine="true" /> - + + + diff --git a/MPDroid/src/main/res/layout/favorite.xml b/MPDroid/src/main/res/layout/favorite.xml new file mode 100644 index 0000000000..3509b84116 --- /dev/null +++ b/MPDroid/src/main/res/layout/favorite.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/MPDroid/src/main/res/layout/song_header.xml b/MPDroid/src/main/res/layout/song_header.xml index 535a070602..1b1bddc05c 100644 --- a/MPDroid/src/main/res/layout/song_header.xml +++ b/MPDroid/src/main/res/layout/song_header.xml @@ -111,4 +111,18 @@ android:paddingRight="8dp" android:src="@drawable/ic_media_play" app:borderWidth="0dp" /> - \ No newline at end of file + + + + diff --git a/MPDroid/src/main/res/values-de/strings.xml b/MPDroid/src/main/res/values-de/strings.xml index 4bdeb1bff3..94252c23db 100644 --- a/MPDroid/src/main/res/values-de/strings.xml +++ b/MPDroid/src/main/res/values-de/strings.xml @@ -318,5 +318,11 @@ Der Stream-Codec wird nicht unterstützt. Wiedergabeliste mischen Wiedergabeliste wurde zufällig gemischt + Favoriten + %s als Favorit hinzugefügt + Favoritenschlüssel + Identifikation um Favoriten zu personalisieren + %s von den Favoriten entfernt + Verwende Favoriten diff --git a/MPDroid/src/main/res/values/strings.xml b/MPDroid/src/main/res/values/strings.xml index 5dfbf40200..18f227e1bd 100644 --- a/MPDroid/src/main/res/values/strings.xml +++ b/MPDroid/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Playlists Files Albums + Favorites Settings Next Previous @@ -182,6 +183,11 @@ Sort albums by year Album track count Show number of tracks on album + Use favorites + Favorite key + Key to identify personal favorites + %s added to favorites + %s removed from favorites Download local cover art Get cover art from the server running MPD (requires a web server, read the wiki!) Path to music diff --git a/MPDroid/src/main/res/xml/settings.xml b/MPDroid/src/main/res/xml/settings.xml index af330ff63d..9169ba677d 100644 --- a/MPDroid/src/main/res/xml/settings.xml +++ b/MPDroid/src/main/res/xml/settings.xml @@ -180,6 +180,27 @@ android:persistent="true" android:summary="@string/showAlbumTrackCountDescription" android:title="@string/showAlbumTrackCount" /> + + + + + + + + + +