From e194ce8b2c022b2c6a19d35c7931866ef82a7116 Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Fri, 25 Mar 2016 07:48:50 +0100 Subject: [PATCH] Implemented SongsListFragment The SongsListFragment lists all available songs for a connected host or for a specific artist. Added the songs tab to the music screen and to the artist details screen. --- .../org/xbmc/kore/provider/MediaContract.java | 6 + .../org/xbmc/kore/provider/MediaDatabase.java | 12 +- .../org/xbmc/kore/provider/MediaProvider.java | 10 + .../xbmc/kore/ui/ArtistDetailsFragment.java | 15 +- .../xbmc/kore/ui/ArtistOverviewFragment.java | 6 +- .../org/xbmc/kore/ui/MusicListFragment.java | 3 +- .../org/xbmc/kore/ui/SongsListFragment.java | 326 ++++++++++++++++++ app/src/main/res/layout/grid_item_song.xml | 77 +++++ app/src/main/res/values/strings.xml | 1 + 9 files changed, 446 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java create mode 100644 app/src/main/res/layout/grid_item_song.xml diff --git a/app/src/main/java/org/xbmc/kore/provider/MediaContract.java b/app/src/main/java/org/xbmc/kore/provider/MediaContract.java index 818b436..ed77950 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaContract.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaContract.java @@ -609,6 +609,12 @@ public class MediaContract { .build(); } + public static Uri buildSongsListUri(long hostId) { + return Hosts.buildHostUri(hostId).buildUpon() + .appendPath(PATH_SONGS) + .build(); + } + /** Read {@link #_ID} from {@link Albums} {@link Uri}. */ public static String getSongId(Uri uri) { return uri.getPathSegments().get(5); diff --git a/app/src/main/java/org/xbmc/kore/provider/MediaDatabase.java b/app/src/main/java/org/xbmc/kore/provider/MediaDatabase.java index 08a9a63..eb4cf12 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaDatabase.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaDatabase.java @@ -96,7 +96,17 @@ public class MediaDatabase extends SQLiteOpenHelper { SONGS + " JOIN " + ALBUM_ARTISTS + " ON " + SONGS + "." + MediaContract.Songs.HOST_ID + "=" + ALBUM_ARTISTS + "." + MediaContract.AlbumArtists.HOST_ID + " AND " + - SONGS + "." + MediaContract.Songs.ALBUMID + "=" + ALBUM_ARTISTS + "." + MediaContract.AlbumArtists.ALBUMID; + SONGS + "." + MediaContract.Songs.ALBUMID + "=" + ALBUM_ARTISTS + "." + MediaContract.AlbumArtists.ALBUMID + + " JOIN " + ALBUMS + " ON " + + SONGS + "." + MediaContract.Songs.HOST_ID + "=" + ALBUMS + "." + MediaContract.Albums.HOST_ID + + " AND " + + SONGS + "." + MediaContract.Songs.ALBUMID + "=" + ALBUMS + "." + MediaContract.Albums.ALBUMID; + + String SONGS_AND_ALBUM_JOIN = + SONGS + " JOIN " + ALBUMS + " ON " + + SONGS + "." + MediaContract.Songs.HOST_ID + "=" + ALBUMS + "." + MediaContract.Albums.HOST_ID + + " AND " + + SONGS + "." + MediaContract.Songs.ALBUMID + "=" + ALBUMS + "." + MediaContract.Albums.ALBUMID; } private interface References { diff --git a/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java b/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java index 166ec34..e4efe9b 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java @@ -80,6 +80,7 @@ public class MediaProvider extends ContentProvider { private static final int SONGS_ARTIST = 801; private static final int SONGS_ALBUM = 802; private static final int SONGS_ID = 803; + private static final int SONGS_LIST = 804; private static final int AUDIO_GENRES_ALL = 900; private static final int AUDIO_GENRES_LIST = 901; @@ -180,6 +181,8 @@ public class MediaProvider extends ContentProvider { // Songs matcher.addURI(authority, MediaContract.PATH_SONGS, SONGS_ALL); + matcher.addURI(authority, MediaContract.PATH_HOSTS + "/*/" + + MediaContract.PATH_SONGS, SONGS_LIST); matcher.addURI(authority, MediaContract.PATH_HOSTS + "/*/" + MediaContract.PATH_ALBUMS + "/*/" + MediaContract.PATH_SONGS, SONGS_ALBUM); @@ -273,6 +276,7 @@ public class MediaProvider extends ContentProvider { case ALBUMS_ID: return MediaContract.Albums.CONTENT_ITEM_TYPE; case SONGS_ALL: + case SONGS_LIST: case SONGS_ARTIST: case SONGS_ALBUM: return MediaContract.Songs.CONTENT_TYPE; @@ -641,6 +645,11 @@ public class MediaProvider extends ContentProvider { case SONGS_ALL: { return builder.table(MediaDatabase.Tables.SONGS); } + case SONGS_LIST: { + final String hostId = MediaContract.Hosts.getHostId(uri); + return builder.table(MediaDatabase.Tables.SONGS_AND_ALBUM_JOIN) + .where(Qualified.SONGS_HOST_ID + "=?", hostId); + } case SONGS_ALBUM: { final String hostId = MediaContract.Hosts.getHostId(uri); final String albumId = MediaContract.Albums.getAlbumId(uri); @@ -651,6 +660,7 @@ public class MediaProvider extends ContentProvider { case SONGS_ARTIST: { final String hostId = MediaContract.Hosts.getHostId(uri); final String artistId = MediaContract.Artists.getArtistId(uri); + LogUtils.LOGD(TAG, "buildQuerySelection: SONGS_ARTIST: "+MediaDatabase.Tables.SONGS_FOR_ARTIST_JOIN); return builder.table(MediaDatabase.Tables.SONGS_FOR_ARTIST_JOIN) .where(Qualified.SONGS_HOST_ID + "=?", hostId) .where(Qualified.ALBUM_ARTISTS_ARTISTID + "=?", artistId); diff --git a/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java index 7d42504..cd2de63 100644 --- a/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java @@ -49,7 +49,7 @@ public class ArtistDetailsFragment extends Fragment { ArtistDetailsFragment fragment = new ArtistDetailsFragment(); Bundle args = new Bundle(); - args.putInt(ArtistOverviewFragment.BUNDLE_KEY_ID, vh.artistId); + args.putInt(ArtistOverviewFragment.BUNDLE_KEY_ARTISTID, vh.artistId); args.putInt(AlbumListFragment.BUNDLE_KEY_ARTISTID, vh.artistId); args.putString(ArtistOverviewFragment.BUNDLE_KEY_TITLE, vh.artistName); args.putString(ArtistOverviewFragment.BUNDLE_KEY_FANART, vh.fanart); @@ -67,7 +67,10 @@ public class ArtistDetailsFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - int id = getArguments().getInt(ArtistOverviewFragment.BUNDLE_KEY_ID, -1); + Bundle arguments = getArguments(); + int id = arguments.getInt(ArtistOverviewFragment.BUNDLE_KEY_ARTISTID, -1); + + arguments.putInt(SongsListFragment.BUNDLE_KEY_ARTISTID, id); if ((container == null) || (id == -1)) { // We're not being shown or there's nothing to show @@ -79,10 +82,12 @@ public class ArtistDetailsFragment extends Fragment { long baseFragmentId = id * 10; TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager()) - .addTab(ArtistOverviewFragment.class, getArguments(), R.string.info, + .addTab(ArtistOverviewFragment.class, arguments, R.string.info, baseFragmentId) - .addTab(AlbumListFragment.class, getArguments(), - R.string.albums, baseFragmentId + 1); + .addTab(AlbumListFragment.class, arguments, + R.string.albums, baseFragmentId + 1) + .addTab(SongsListFragment.class, arguments, + R.string.songs, baseFragmentId + 2); viewPager.setAdapter(tabsAdapter); pagerTabStrip.setViewPager(viewPager); diff --git a/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java b/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java index 174a9f3..ba33e6f 100644 --- a/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java @@ -73,7 +73,7 @@ public class ArtistOverviewFragment extends AbstractDetailsFragment implements LoaderManager.LoaderCallbacks { private static final String TAG = LogUtils.makeLogTag(ArtistOverviewFragment.class); - public static final String BUNDLE_KEY_ID = "id"; + public static final String BUNDLE_KEY_ARTISTID = "id"; public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; public static final String BUNDLE_KEY_TITLE = "title"; public static final String BUNDLE_KEY_GENRE = "genre"; @@ -116,7 +116,7 @@ public class ArtistOverviewFragment extends AbstractDetailsFragment @Override protected View createView(LayoutInflater inflater, ViewGroup container) { Bundle bundle = getArguments(); - artistId = bundle.getInt(BUNDLE_KEY_ID, -1); + artistId = bundle.getInt(BUNDLE_KEY_ARTISTID, -1); if ((container == null) || (artistId == -1)) { // We're not being shown or there's nothing to show @@ -455,7 +455,7 @@ public class ArtistOverviewFragment extends AbstractDetailsFragment } /** - * Movie cast list query parameters. + * Song list query parameters. */ private interface AlbumSongsListQuery { String[] PROJECTION = { diff --git a/app/src/main/java/org/xbmc/kore/ui/MusicListFragment.java b/app/src/main/java/org/xbmc/kore/ui/MusicListFragment.java index e392ed2..8b9b296 100644 --- a/app/src/main/java/org/xbmc/kore/ui/MusicListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/MusicListFragment.java @@ -53,7 +53,8 @@ public class MusicListFragment extends Fragment { .addTab(ArtistListFragment.class, getArguments(), R.string.artists, 1) .addTab(AlbumListFragment.class, getArguments(), R.string.albums, 2) .addTab(AudioGenresListFragment.class, getArguments(), R.string.genres, 3) - .addTab(MusicVideoListFragment.class, getArguments(), R.string.music_videos, 4); + .addTab(SongsListFragment.class, getArguments(), R.string.songs, 4) + .addTab(MusicVideoListFragment.class, getArguments(), R.string.music_videos, 5); viewPager.setAdapter(tabsAdapter); pagerTabStrip.setViewPager(viewPager); diff --git a/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java b/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java new file mode 100644 index 0000000..c68e1ce --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/SongsListFragment.java @@ -0,0 +1,326 @@ +/* + * Copyright 2015 Synced Synapse. All rights reserved. + * + * 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 org.xbmc.kore.ui; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.BaseColumns; +import android.support.v4.content.CursorLoader; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.TextView; + +import org.xbmc.kore.R; +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.provider.MediaDatabase; +import org.xbmc.kore.service.LibrarySyncService; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.MediaPlayerUtils; +import org.xbmc.kore.utils.UIUtils; + +/** + * Fragment that presents the songs list + */ +public class SongsListFragment extends AbstractCursorListFragment { + private static final String TAG = LogUtils.makeLogTag(SongsListFragment.class); + + public static final String BUNDLE_KEY_ARTISTID = "artistid"; + + private int artistId = -1; + + @Override + protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; } + + @Override + protected CursorAdapter createAdapter() { + return new SongsAdapter(getActivity()); + } + + @Override + protected void onListItemClicked(View view) { + ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); + showPopupMenu(contextMenu); + } + + @Override + protected CursorLoader createCursorLoader() { + Uri uri; + HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo(); + int hostId = hostInfo != null ? hostInfo.getId() : -1; + + if (artistId != -1) { + uri = MediaContract.Songs.buildArtistSongsListUri(hostId, artistId); + } else { + uri = MediaContract.Songs.buildSongsListUri(hostId); + } + + String selection = null; + String selectionArgs[] = null; + String searchFilter = getSearchFilter(); + if (!TextUtils.isEmpty(searchFilter)) { + selection = MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.TITLE + " LIKE ?"; + selectionArgs = new String[] {"%" + searchFilter + "%"}; + } + + return new CursorLoader(getActivity(), uri, + SongsAlbumsListQuery.PROJECTION, selection, selectionArgs, SongsAlbumsListQuery.SORT); + } + + @Override + public void onAttach(Activity activity) { + setSupportsSearch(true); + super.onAttach(activity); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Bundle args = getArguments(); + if (args != null) { + artistId = getArguments().getInt(BUNDLE_KEY_ARTISTID, -1); + } + + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (!isAdded()) { + // HACK: Fix crash reported on Play Store. Why does this is necessary is beyond me + super.onCreateOptionsMenu(menu, inflater); + return; + } + + inflater.inflate(R.menu.media_search, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + searchView.setOnQueryTextListener(this); + searchView.setQueryHint(getString(R.string.action_search_albums)); + super.onCreateOptionsMenu(menu, inflater); + } + + /** + * Song list query parameters. + */ + private interface SongsListQuery { + String[] PROJECTION = { + MediaDatabase.Tables.SONGS + "." + BaseColumns._ID, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.TITLE, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.TRACK, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.DURATION, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.FILE, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.SONGID, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.ALBUMID, + }; + + String SORT = MediaContract.Songs.TRACK + " ASC"; + + int ID = 0; + int TITLE = 1; + int TRACK = 2; + int DURATION = 3; + int FILE = 4; + int SONGID = 5; + int ALBUMID = 6; + } + + /** + * Album songs list query parameters. + */ + private interface SongsAlbumsListQuery { + String[] PROJECTION = { + MediaDatabase.Tables.SONGS + "." + BaseColumns._ID, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.TITLE, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.TRACK, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.DURATION, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.FILE, + MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.SONGID, + MediaDatabase.Tables.ALBUMS + "." + MediaContract.Albums.TITLE, + MediaDatabase.Tables.ALBUMS + "." + MediaContract.Albums.DISPLAYARTIST, + MediaDatabase.Tables.ALBUMS + "." + MediaContract.Albums.GENRE, + MediaDatabase.Tables.ALBUMS + "." + MediaContract.Albums.YEAR, + MediaDatabase.Tables.ALBUMS + "." + MediaContract.Albums.THUMBNAIL, + }; + + String SORT = MediaDatabase.sortCommonTokens(MediaDatabase.Tables.SONGS + + "." + + MediaContract.Songs.TITLE) + " ASC"; + + int ID = 0; + int TITLE = 1; + int TRACK = 2; + int DURATION = 3; + int FILE = 4; + int SONGID = 5; + int ALBUMTITLE = 6; + int ALBUMARTIST = 7; + int GENRE = 8; + int YEAR = 9; + int THUMBNAIL = 10; + } + + private class SongsAdapter extends CursorAdapter { + + private HostManager hostManager; + private int artWidth, artHeight; + + public SongsAdapter(Context context) { + super(context, null, false); + this.hostManager = HostManager.getInstance(context); + + // Get the art dimensions + // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when + // the user transitions to that fragment, avoiding another call and imediatelly showing the image + Resources resources = context.getResources(); + artWidth = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); + artHeight = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); + } + + /** {@inheritDoc} */ + @Override + public View newView(Context context, final Cursor cursor, ViewGroup parent) { + final View view = LayoutInflater.from(context) + .inflate(R.layout.grid_item_song, parent, false); + + // Setup View holder pattern + ViewHolder viewHolder = new ViewHolder(); + viewHolder.title = (TextView)view.findViewById(R.id.title); + viewHolder.details = (TextView)view.findViewById(R.id.details); + viewHolder.art = (ImageView)view.findViewById(R.id.art); + viewHolder.artist = (TextView)view.findViewById(R.id.artist); + + view.setTag(viewHolder); + return view; + } + + /** {@inheritDoc} */ + @TargetApi(21) + @Override + public void bindView(View view, Context context, Cursor cursor) { + final ViewHolder viewHolder = (ViewHolder)view.getTag(); + + String title = cursor.getString(SongsAlbumsListQuery.TITLE); + viewHolder.songId = cursor.getInt(SongsAlbumsListQuery.SONGID); + + viewHolder.title.setText(title); + viewHolder.artist.setText(String.valueOf(cursor.getString(SongsAlbumsListQuery.ALBUMARTIST))); + + int year = cursor.getInt(SongsAlbumsListQuery.YEAR); + if (year > 0) { + setDetails(viewHolder.details, + cursor.getString(SongsAlbumsListQuery.ALBUMTITLE), + String.valueOf(year), + cursor.getString(SongsAlbumsListQuery.GENRE)); + } else { + setDetails(viewHolder.details, + cursor.getString(SongsAlbumsListQuery.ALBUMTITLE), + cursor.getString(SongsAlbumsListQuery.GENRE)); + } + + String thumbnail = cursor.getString(SongsAlbumsListQuery.THUMBNAIL); + UIUtils.loadImageWithCharacterAvatar(context, hostManager, + thumbnail, title, + viewHolder.art, artWidth, artHeight); + + // For the popupmenu + ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); + contextMenu.setTag(viewHolder); + contextMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + showPopupMenu(v); + } + }); + } + } + + /** + * View holder pattern + */ + public static class ViewHolder { + ImageView art; + TextView title; + TextView details; + TextView artist; + + int songId; + } + + private void showPopupMenu(View v) { + ViewHolder viewHolder = (ViewHolder) v.getTag(); + + final PlaylistType.Item playListItem = new PlaylistType.Item(); + playListItem.songid = viewHolder.songId; + + final PopupMenu popupMenu = new PopupMenu(getActivity(), v); + popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_play: + MediaPlayerUtils.play(SongsListFragment.this, playListItem); + return true; + case R.id.action_queue: + MediaPlayerUtils.queue(SongsListFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO); + return true; + } + return false; + } + }); + popupMenu.show(); + } + + private void setDetails(TextView textView, String... elements) { + if ((elements == null) || (elements.length < 1)) { + return; + } + + StringBuilder stringBuilder = new StringBuilder(); + + int i = 0; + int size = elements.length - 1; + for (; i < size; i++) { + if (!TextUtils.isEmpty(elements[i])) { + stringBuilder.append(elements[i]); + stringBuilder.append(" | "); + } + } + + if (elements.length > 0) { + stringBuilder.append(elements[i]); + } + + textView.setText(stringBuilder.toString()); + } +} diff --git a/app/src/main/res/layout/grid_item_song.xml b/app/src/main/res/layout/grid_item_song.xml new file mode 100644 index 0000000..494b450 --- /dev/null +++ b/app/src/main/res/layout/grid_item_song.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f97757f..954cd34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -372,5 +372,6 @@ Download network types Allowed network types for media downloads + Songs