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.
This commit is contained in:
Martijn Brekhof 2016-03-25 07:48:50 +01:00
parent 6db788f037
commit e194ce8b2c
9 changed files with 446 additions and 10 deletions

View File

@ -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);

View File

@ -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 {

View File

@ -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);

View File

@ -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);

View File

@ -73,7 +73,7 @@ public class ArtistOverviewFragment extends AbstractDetailsFragment
implements LoaderManager.LoaderCallbacks<Cursor> {
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 = {

View File

@ -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);

View File

@ -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());
}
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardElevation="@dimen/default_card_elevation"
card_view:cardBackgroundColor="?attr/appCardBackgroundColor">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/art"
android:layout_width="@dimen/albumlist_art_width"
android:layout_height="@dimen/albumlist_art_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/poster"/>
<ImageView
android:id="@+id/list_context_menu"
android:layout_width="@dimen/default_icon_size"
android:layout_height="@dimen/default_icon_size"
android:padding="@dimen/default_icon_padding"
android:layout_alignTop="@id/art"
android:layout_alignParentRight="true"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconOverflow"
android:contentDescription="@string/action_options"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_toLeftOf="@+id/list_context_menu"
android:layout_toRightOf="@id/art"
android:layout_alignTop="@id/art">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Medialist.Title"/>
<TextView
android:id="@+id/artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Medialist.Details"/>
<TextView
android:id="@+id/details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Medialist.OtherInfo"/>
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -372,5 +372,6 @@
<string name="download_network_types_title">Download network types</string>
<string name="download_network_types_summary">Allowed network types for media downloads</string>
<string name="songs">Songs</string>
</resources>