Implemented showing artist details

When user selects artist from ArtistListFragment it will now
show a screen displaying artist details. I've taken the TV show
details setup as an example.

This adds the following new functionality:

* Displaying artist fanart
* Displaying artist description
* Download all songs from an artist
This commit is contained in:
Martijn Brekhof 2016-01-26 13:24:01 +01:00
parent b0f09b6cf5
commit f0f21d118a
11 changed files with 839 additions and 48 deletions

View File

@ -587,13 +587,20 @@ public class MediaContract {
public static final String CONTENT_ITEM_TYPE =
"vnd.android.cursor.item/vnd.org.xbmc." + PATH_SONGS;
/** Build {@link Uri} for albums list. */
public static Uri buildSongsListUri(long hostId, long albumId) {
/** Build {@link Uri} for album songs list. */
public static Uri buildAlbumSongsListUri(long hostId, long albumId) {
return Albums.buildAlbumUri(hostId, albumId).buildUpon()
.appendPath(PATH_SONGS)
.build();
}
/** Build {@link Uri} for artists songs list. */
public static Uri buildArtistSongsListUri(long hostId, long artistId) {
return Artists.buildArtistUri(hostId, artistId).buildUpon()
.appendPath(PATH_SONGS)
.build();
}
/** Build {@link Uri} for requested {@link #_ID}. */
public static Uri buildSongUri(long hostId, long albumId, long songId) {
return Albums.buildAlbumUri(hostId, albumId).buildUpon()

View File

@ -88,7 +88,16 @@ public class MediaDatabase extends SQLiteOpenHelper {
ALBUM_GENRES + "." + MediaContract.AlbumGenres.HOST_ID + "=" + AUDIO_GENRES + "." + MediaContract.AudioGenres.HOST_ID +
" AND " +
ALBUM_GENRES + "." + MediaContract.AlbumGenres.GENREID + "=" + AUDIO_GENRES + "." + MediaContract.AudioGenres.GENREID;
}
/**
* Join to get Songs for an Artist
*/
String SONGS_FOR_ARTIST_JOIN =
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;
}
private interface References {
String HOST_ID =

View File

@ -77,7 +77,8 @@ public class MediaProvider extends ContentProvider {
private static final int ALBUM_GENRES_LIST = 711;
private static final int SONGS_ALL = 800;
private static final int SONGS_LIST = 802;
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 AUDIO_GENRES_ALL = 900;
@ -181,10 +182,13 @@ public class MediaProvider extends ContentProvider {
matcher.addURI(authority, MediaContract.PATH_SONGS, SONGS_ALL);
matcher.addURI(authority, MediaContract.PATH_HOSTS + "/*/" +
MediaContract.PATH_ALBUMS + "/*/" +
MediaContract.PATH_SONGS, SONGS_LIST);
MediaContract.PATH_SONGS, SONGS_ALBUM);
matcher.addURI(authority, MediaContract.PATH_HOSTS + "/*/" +
MediaContract.PATH_ALBUMS + "/*/" +
MediaContract.PATH_SONGS + "/*", SONGS_ID);
matcher.addURI(authority, MediaContract.PATH_HOSTS + "/*/" +
MediaContract.PATH_ARTISTS + "/*/" +
MediaContract.PATH_SONGS, SONGS_ARTIST);
// Genres
matcher.addURI(authority, MediaContract.PATH_AUDIO_GENRES, AUDIO_GENRES_ALL);
@ -269,7 +273,8 @@ 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;
case SONGS_ID:
return MediaContract.Songs.CONTENT_ITEM_TYPE;
@ -656,13 +661,20 @@ public class MediaProvider extends ContentProvider {
case SONGS_ALL: {
return builder.table(MediaDatabase.Tables.SONGS);
}
case SONGS_LIST: {
case SONGS_ALBUM: {
final String hostId = MediaContract.Hosts.getHostId(uri);
final String albumId = MediaContract.Albums.getAlbumId(uri);
return builder.table(MediaDatabase.Tables.SONGS)
.where(MediaContract.Songs.HOST_ID + "=?", hostId)
.where(MediaContract.Songs.ALBUMID + "=?", albumId);
}
case SONGS_ARTIST: {
final String hostId = MediaContract.Hosts.getHostId(uri);
final String artistId = MediaContract.Artists.getArtistId(uri);
return builder.table(MediaDatabase.Tables.SONGS_FOR_ARTIST_JOIN)
.where(Qualified.SONGS_HOST_ID + "=?", hostId)
.where(Qualified.ALBUM_ARTISTS_ARTISTID + "=?", artistId);
}
case SONGS_ID: {
final String hostId = MediaContract.Hosts.getHostId(uri);
final String albumId = MediaContract.Albums.getAlbumId(uri);
@ -780,6 +792,7 @@ public class MediaProvider extends ContentProvider {
MediaDatabase.Tables.ALBUM_GENRES + "." + MediaContract.AlbumGenres.GENREID;
String ALBUM_GENRES_ALBUMID =
MediaDatabase.Tables.ALBUM_GENRES + "." + MediaContract.AlbumGenres.ALBUMID;
String SONGS_HOST_ID =
MediaDatabase.Tables.SONGS + "." + MediaContract.Songs.HOST_ID;
}
}

View File

@ -265,7 +265,7 @@ public class AlbumDetailsFragment extends AbstractDetailsFragment
return new CursorLoader(getActivity(), uri,
AlbumDetailsQuery.PROJECTION, null, null, null);
case LOADER_SONGS:
uri = MediaContract.Songs.buildSongsListUri(hostInfo.getId(), albumId);
uri = MediaContract.Songs.buildAlbumSongsListUri(hostInfo.getId(), albumId);
return new CursorLoader(getActivity(), uri,
AlbumSongsListQuery.PROJECTION, null, null, AlbumSongsListQuery.SORT);
default:

View File

@ -61,8 +61,8 @@ public class AlbumListFragment extends AbstractListFragment {
public void onAlbumSelected(ViewHolder vh);
}
private static final String GENREID = "genreid",
ARTISTID = "artistid";
public static final String BUNDLE_KEY_GENREID = "genreid",
BUNDLE_KEY_ARTISTID = "artistid";
private int genreId = -1;
private int artistId = -1;
@ -80,7 +80,7 @@ public class AlbumListFragment extends AbstractListFragment {
AlbumListFragment fragment = new AlbumListFragment();
Bundle args = new Bundle();
args.putInt(GENREID, genreId);
args.putInt(BUNDLE_KEY_GENREID, genreId);
fragment.setArguments(args);
return fragment;
}
@ -92,7 +92,7 @@ public class AlbumListFragment extends AbstractListFragment {
AlbumListFragment fragment = new AlbumListFragment();
Bundle args = new Bundle();
args.putInt(ARTISTID, artistId);
args.putInt(BUNDLE_KEY_ARTISTID, artistId);
fragment.setArguments(args);
return fragment;
}
@ -145,8 +145,8 @@ public class AlbumListFragment extends AbstractListFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Bundle args = getArguments();
if (args != null) {
genreId = getArguments().getInt(GENREID, -1);
artistId = getArguments().getInt(ARTISTID, -1);
genreId = getArguments().getInt(BUNDLE_KEY_GENREID, -1);
artistId = getArguments().getInt(BUNDLE_KEY_ARTISTID, -1);
}
return super.onCreateView(inflater, container, savedInstanceState);

View File

@ -0,0 +1,117 @@
/*
* 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.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.astuetz.PagerSlidingTabStrip;
import org.xbmc.kore.R;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.TabsAdapter;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import butterknife.ButterKnife;
import butterknife.InjectView;
public class ArtistDetailsFragment extends Fragment {
private static final String TAG = LogUtils.makeLogTag(ArtistDetailsFragment.class);
@InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip;
@InjectView(R.id.pager) ViewPager viewPager;
/**
* Create a new instance of this, initialized to show tvshowId
*/
@TargetApi(21)
public static ArtistDetailsFragment newInstance(ArtistListFragment.ViewHolder vh) {
ArtistDetailsFragment fragment = new ArtistDetailsFragment();
Bundle args = new Bundle();
args.putInt(ArtistOverviewFragment.BUNDLE_KEY_ID, 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);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_DESCRIPTION, vh.description);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_GENRE, vh.genres);
args.putString(ArtistOverviewFragment.BUNDLE_KEY_POSTER, vh.poster);
if( Utils.isLollipopOrLater()) {
args.putString(ArtistOverviewFragment.POSTER_TRANS_NAME, vh.artView.getTransitionName());
}
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
int id = getArguments().getInt(ArtistOverviewFragment.BUNDLE_KEY_ID, -1);
if ((container == null) || (id == -1)) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false);
ButterKnife.inject(this, root);
long baseFragmentId = id * 10;
TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager())
.addTab(ArtistOverviewFragment.class, getArguments(), R.string.info,
baseFragmentId)
.addTab(AlbumListFragment.class, getArguments(),
R.string.albums, baseFragmentId + 1);
viewPager.setAdapter(tabsAdapter);
pagerTabStrip.setViewPager(viewPager);
return root;
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
public View getSharedElement() {
View view = getView();
if (view == null)
return null;
//Note: this works as R.id.poster is only used in ArtistShowOverviewFragment.
//If the same id is used in other fragments in the TabsAdapter we
//need to check which fragment is currently displayed
View artView = view.findViewById(R.id.poster);
View scrollView = view.findViewById(R.id.media_panel);
if (( artView != null ) &&
( scrollView != null ) &&
UIUtils.isViewInBounds(scrollView, artView)) {
return artView;
}
return null;
}
}

View File

@ -15,6 +15,7 @@
*/
package org.xbmc.kore.ui;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
@ -48,6 +49,7 @@ import org.xbmc.kore.service.LibrarySyncService;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
/**
* Fragment that presents the artists list
@ -56,7 +58,7 @@ public class ArtistListFragment extends AbstractListFragment {
private static final String TAG = LogUtils.makeLogTag(ArtistListFragment.class);
public interface OnArtistSelectedListener {
public void onArtistSelected(int artistId, String artistName);
public void onArtistSelected(ViewHolder tag);
}
// Activity listener
@ -73,7 +75,7 @@ public class ArtistListFragment extends AbstractListFragment {
// Get the artist id from the tag
ViewHolder tag = (ViewHolder) view.getTag();
// Notify the activity
listenerActivity.onArtistSelected(tag.artistId, tag.artistName);
listenerActivity.onArtistSelected(tag);
}
};
}
@ -140,16 +142,20 @@ public class ArtistListFragment extends AbstractListFragment {
MediaContract.Artists.ARTISTID,
MediaContract.Artists.ARTIST,
MediaContract.Artists.GENRE,
MediaContract.Movies.THUMBNAIL,
MediaContract.Artists.THUMBNAIL,
MediaContract.Artists.DESCRIPTION,
MediaContract.Artists.FANART
};
String SORT = MediaDatabase.sortCommonTokens(MediaContract.Artists.ARTIST) + " ASC";
final int ID = 0;
final int ARTISTID = 1;
final int ARTIST = 2;
final int GENRE = 3;
final int THUMBNAIL = 4;
int ID = 0;
int ARTISTID = 1;
int ARTIST = 2;
int GENRE = 3;
int THUMBNAIL = 4;
int DESCRIPTION = 5;
int FANART = 6;
}
private class ArtistsAdapter extends CursorAdapter {
@ -163,10 +169,8 @@ public class ArtistListFragment extends AbstractListFragment {
// Get the art dimensions
Resources resources = context.getResources();
artWidth = (int)(resources.getDimension(R.dimen.artistlist_art_width) /
UIUtils.IMAGE_RESIZE_FACTOR);
artHeight = (int)(resources.getDimension(R.dimen.artistlist_art_heigth) /
UIUtils.IMAGE_RESIZE_FACTOR);
artWidth = (int)(resources.getDimension(R.dimen.albumdetail_poster_width));
artHeight = (int)(resources.getDimension(R.dimen.albumdetail_poster_heigth));
}
/** {@inheritDoc} */
@ -186,6 +190,7 @@ public class ArtistListFragment extends AbstractListFragment {
}
/** {@inheritDoc} */
@TargetApi(21)
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ViewHolder viewHolder = (ViewHolder)view.getTag();
@ -193,32 +198,43 @@ public class ArtistListFragment extends AbstractListFragment {
// Save the movie id
viewHolder.artistId = cursor.getInt(ArtistListQuery.ARTISTID);
viewHolder.artistName = cursor.getString(ArtistListQuery.ARTIST);
viewHolder.genres = cursor.getString(ArtistListQuery.GENRE);
viewHolder.description = cursor.getString(ArtistListQuery.DESCRIPTION);
viewHolder.fanart = cursor.getString(ArtistListQuery.FANART);
viewHolder.nameView.setText(viewHolder.artistName);
viewHolder.genresView.setText(cursor.getString(ArtistListQuery.GENRE));
viewHolder.poster = cursor.getString(ArtistListQuery.THUMBNAIL);
String thumbnail = cursor.getString(ArtistListQuery.THUMBNAIL);
UIUtils.loadImageWithCharacterAvatar(context, hostManager,
thumbnail, viewHolder.artistName,
viewHolder.poster, viewHolder.artistName,
viewHolder.artView, artWidth, artHeight);
// For the popupmenu
ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu);
contextMenu.setTag(viewHolder);
contextMenu.setOnClickListener(artistlistItemMenuClickListener);
if(Utils.isLollipopOrLater()) {
viewHolder.artView.setTransitionName("a"+viewHolder.artistId);
}
}
}
/**
* View holder pattern
*/
private static class ViewHolder {
public static class ViewHolder {
TextView nameView;
TextView genresView;
ImageView artView;
int artistId;
String artistName;
String genres;
String description;
String fanart;
String poster;
}
private View.OnClickListener artistlistItemMenuClickListener = new View.OnClickListener() {

View File

@ -0,0 +1,481 @@
/*
* 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.AlertDialog;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
import com.melnykov.fab.ObservableScrollView;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
import org.xbmc.kore.host.HostManager;
import org.xbmc.kore.jsonrpc.ApiCallback;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.method.Player;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
import org.xbmc.kore.provider.MediaContract;
import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
import org.xbmc.kore.utils.UIUtils;
import org.xbmc.kore.utils.Utils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import butterknife.ButterKnife;
import butterknife.InjectView;
import butterknife.OnClick;
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 POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
public static final String BUNDLE_KEY_TITLE = "title";
public static final String BUNDLE_KEY_GENRE = "genre";
public static final String BUNDLE_KEY_DESCRIPTION = "description";
public static final String BUNDLE_KEY_FANART = "fanart";
public static final String BUNDLE_KEY_POSTER = "poster";
// Loader IDs
private static final int LOADER_ARTIST = 0,
LOADER_ALBUMS = 1,
LOADER_SONGS = 2;
private HostManager hostManager;
private HostInfo hostInfo;
/**
* Handler on which to post RPC callbacks
*/
private Handler callbackHandler = new Handler();
private int artistId = -1;
private String artistTitle;
private HashMap<Integer, String> albumTitles = new HashMap<>();
@InjectView(R.id.exit_transition_view) View exitTransitionView;
// Buttons
@InjectView(R.id.fab) ImageButton fabButton;
// Detail views
@InjectView(R.id.media_panel) ScrollView mediaPanel;
@InjectView(R.id.art) ImageView mediaArt;
@InjectView(R.id.poster) ImageView mediaPoster;
@InjectView(R.id.media_title) TextView mediaTitle;
@InjectView(R.id.media_undertitle) TextView mediaUndertitle;
@InjectView(R.id.media_description) TextView mediaDescription;
@TargetApi(21)
@Override
protected View createView(LayoutInflater inflater, ViewGroup container) {
Bundle bundle = getArguments();
artistId = bundle.getInt(BUNDLE_KEY_ID, -1);
if ((container == null) || (artistId == -1)) {
// We're not being shown or there's nothing to show
return null;
}
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_artist_details, container, false);
ButterKnife.inject(this, root);
hostManager = HostManager.getInstance(getActivity());
hostInfo = hostManager.getHostInfo();
// Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources();
final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
mediaPanel.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
float y = mediaPanel.getScrollY();
float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
mediaArt.setAlpha(newAlpha);
}
});
((FloatingActionButton) fabButton).attachToScrollView((ObservableScrollView) mediaPanel);
if(Utils.isLollipopOrLater()) {
mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME));
}
artistTitle = bundle.getString(BUNDLE_KEY_TITLE);
mediaTitle.setText(artistTitle);
mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_GENRE));
mediaDescription.setText(bundle.getString(BUNDLE_KEY_DESCRIPTION));
setArt(bundle.getString(BUNDLE_KEY_POSTER), bundle.getString(BUNDLE_KEY_FANART));
return root;
}
@Override
protected String getSyncType() {
return null;
}
@Override
protected String getSyncID() {
return null;
}
@Override
protected int getSyncItemID() {
return 0;
}
@Override
protected SwipeRefreshLayout getSwipeRefreshLayout() {
return null;
}
@Override
protected void onDownload() {
getLoaderManager().initLoader(LOADER_ALBUMS, null, this);
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public void onResume() {
// Force the exit view to invisible
exitTransitionView.setVisibility(View.INVISIBLE);
super.onResume();
}
@Override
public void onPause() {
//Make sure loader is not reloaded for albums and songs when we return
//These loaders should only be activated by the user pressing the download button
getLoaderManager().destroyLoader(LOADER_ALBUMS);
getLoaderManager().destroyLoader(LOADER_SONGS);
super.onPause();
}
@Override
protected void onSyncProcessEnded(MediaSyncEvent event) {
}
/**
* Loader callbacks
*/
/** {@inheritDoc} */
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri;
switch (i) {
case LOADER_ARTIST:
uri = MediaContract.Artists.buildArtistUri(hostInfo.getId(), artistId);
return new CursorLoader(getActivity(), uri,
DetailsQuery.PROJECTION, null, null, null);
case LOADER_ALBUMS:
uri = MediaContract.AlbumArtists.buildAlbumsForArtistListUri(hostInfo.getId(), artistId);
return new CursorLoader(getActivity(), uri,
AlbumListQuery.PROJECTION, null, null, null);
case LOADER_SONGS:
uri = MediaContract.Songs.buildArtistSongsListUri(hostInfo.getId(), artistId);
return new CursorLoader(getActivity(), uri,
AlbumSongsListQuery.PROJECTION, null, null, AlbumSongsListQuery.SORT);
default:
return null;
}
}
/** {@inheritDoc} */
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null && cursor.getCount() > 0) {
switch (cursorLoader.getId()) {
case LOADER_ARTIST:
displayArtistDetails(cursor);
break;
case LOADER_ALBUMS:
createAlbumList(cursor);
getLoaderManager().initLoader(LOADER_SONGS, null, this);
break;
case LOADER_SONGS:
downloadSongs(cursor);
}
}
}
/** {@inheritDoc} */
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
// Release loader's data
}
@OnClick(R.id.fab)
public void onFabClicked(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.artistid = artistId;
Player.Open action = new Player.Open(item);
action.execute(hostManager.getConnection(), new ApiCallback<String>() {
@Override
public void onSuccess(String result) {
if (!isAdded()) return;
// Check whether we should switch to the remote
boolean switchToRemote = PreferenceManager
.getDefaultSharedPreferences(getActivity())
.getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
if (switchToRemote) {
int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
}
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
}
}, callbackHandler);
}
@OnClick(R.id.add_to_playlist)
public void onAddToPlaylistClicked(View v) {
final PlaylistType.Item playListItem = new PlaylistType.Item();
playListItem.artistid = artistId;
MediaPlayerUtils.queue(this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO);
}
private FileDownloadHelper.SongInfo createSongInfo(Cursor cursor) {
FileDownloadHelper.SongInfo songInfo = null;
String albumTitle = albumTitles.get(cursor.getInt(AlbumSongsListQuery.ALBUMID));
if (albumTitle != null) {
// Add this song to the list
songInfo = new FileDownloadHelper.SongInfo(
artistTitle,
albumTitle,
cursor.getInt(AlbumSongsListQuery.SONGID),
cursor.getInt(AlbumSongsListQuery.TRACK),
cursor.getString(AlbumSongsListQuery.TITLE),
cursor.getString(AlbumSongsListQuery.FILE));
}
return songInfo;
}
private void createAlbumList(Cursor cursor) {
if (cursor.moveToFirst()) {
do {
albumTitles.put(cursor.getInt(AlbumListQuery.ALBUMID), cursor.getString(AlbumListQuery.TITLE));
} while(cursor.moveToNext());
}
}
private void downloadSongs(Cursor cursor) {
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) { }
};
final ArrayList<FileDownloadHelper.SongInfo> songInfoList = new ArrayList<>(cursor.getCount());
if (cursor.moveToFirst()) {
do {
FileDownloadHelper.SongInfo songInfo = createSongInfo(cursor);
if (songInfo != null) {
songInfoList.add(songInfo);
}
} while (cursor.moveToNext());
}
// Check if the directory exists and whether to overwrite it
File file = new File(songInfoList.get(0).getAbsoluteDirectoryPath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.download_dir_exists)
.setPositiveButton(R.string.overwrite,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), hostInfo,
songInfoList, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNeutralButton(R.string.download_with_new_name,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), hostInfo,
songInfoList, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
.setMessage(R.string.confirm_artist_download)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), hostInfo,
songInfoList, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
.setNegativeButton(android.R.string.cancel, noopClickListener)
.show();
}
}
private void displayArtistDetails(Cursor cursor) {
cursor.moveToFirst();
artistTitle = cursor.getString(DetailsQuery.ARTIST);
mediaTitle.setText(artistTitle);
mediaUndertitle.setText(cursor.getString(DetailsQuery.GENRE));
mediaDescription.setText(cursor.getString(DetailsQuery.DESCRIPTION));
setArt(cursor.getString(DetailsQuery.THUMBNAIL), cursor.getString(DetailsQuery.FANART));
}
private void setArt(String poster, String fanart) {
final Resources resources = getActivity().getResources();
// Images
DisplayMetrics displayMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height),
artWidth = displayMetrics.widthPixels;
int posterWidth = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width);
int posterHeight = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth);
UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager,
poster, artistTitle,
mediaPoster, posterWidth, posterHeight);
UIUtils.loadImageIntoImageview(hostManager,
TextUtils.isEmpty(fanart) ? poster : fanart,
mediaArt, artWidth, artHeight);
}
/**
* Returns the shared element if visible
* @return View if visible, null otherwise
*/
public View getSharedElement() {
if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
return mediaPoster;
}
return null;
}
private interface DetailsQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Artists.ARTISTID,
MediaContract.Artists.ARTIST,
MediaContract.Artists.GENRE,
MediaContract.Artists.THUMBNAIL,
MediaContract.Artists.DESCRIPTION,
MediaContract.Artists.FANART
};
int ID = 0;
int ARTISTID = 1;
int ARTIST = 2;
int GENRE = 3;
int THUMBNAIL = 4;
int DESCRIPTION = 5;
int FANART = 6;
}
private interface AlbumListQuery {
String[] PROJECTION = {
BaseColumns._ID,
MediaContract.Albums.ALBUMID,
MediaContract.Albums.TITLE,
};
int ID = 0;
int ALBUMID = 1;
int TITLE = 2;
}
/**
* Movie cast list query parameters.
*/
private interface AlbumSongsListQuery {
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;
}
}

View File

@ -94,6 +94,9 @@ public class MusicActivity extends BaseActivity
musicListFragment.setExitSharedElementCallback(new SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// Clearing must be done in the reentering fragment
// as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might
// be a v4 support package bug.
if (clearSharedElements) {
names.clear();
sharedElements.clear();
@ -264,12 +267,12 @@ public class MusicActivity extends BaseActivity
}
@TargetApi(21)
public void onArtistSelected(int artistId, String artistName) {
selectedArtistId = artistId;
selectedArtistName = artistName;
public void onArtistSelected(ArtistListFragment.ViewHolder viewHolder) {
selectedArtistId = viewHolder.artistId;
selectedArtistName = viewHolder.artistName;
// Replace list fragment
AlbumListFragment albumListFragment = AlbumListFragment.newInstanceForArtist(artistId);
final ArtistDetailsFragment artistDetailsFragment = ArtistDetailsFragment.newInstance(viewHolder);
FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction();
// Setup animations
@ -277,33 +280,34 @@ public class MusicActivity extends BaseActivity
android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() {
@Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
if (clearSharedElements) {
names.clear();
sharedElements.clear();
clearSharedElements = false;
View sharedView = artistDetailsFragment.getSharedElement();
if (sharedView == null) { // shared element not visible
clearSharedElements = true;
}
}
};
albumListFragment.setExitSharedElementCallback(seCallback);
artistDetailsFragment.setEnterSharedElementCallback(seCallback);
//Fade added to prevent shared element from disappearing very shortly at the start of the transition.
Transition fade = TransitionInflater
artistDetailsFragment.setEnterTransition(TransitionInflater
.from(this)
.inflateTransition(android.R.transition.fade);
albumListFragment.setExitTransition(fade);
albumListFragment.setReenterTransition(fade);
albumListFragment.setSharedElementReturnTransition(TransitionInflater.from(
this).inflateTransition(R.transition.change_image));
.inflateTransition(R.transition.media_details));
artistDetailsFragment.setReturnTransition(null);
Transition changeImageTransition = TransitionInflater.from(
this).inflateTransition(R.transition.change_image);
artistDetailsFragment.setSharedElementReturnTransition(changeImageTransition);
artistDetailsFragment.setSharedElementEnterTransition(changeImageTransition);
fragTrans.addSharedElement(viewHolder.artView, viewHolder.artView.getTransitionName());
} else {
fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0);
}
fragTrans.replace(R.id.fragment_container, albumListFragment)
fragTrans.replace(R.id.fragment_container, artistDetailsFragment)
.addToBackStack(null)
.commit();
navigationDrawerFragment.animateDrawerToggle(true);
setupActionBar(null, artistName, null, null);
setupActionBar(null, selectedArtistName, null, null);
}
@TargetApi(21)

View File

@ -0,0 +1,143 @@
<?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.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/art"
android:layout_width="match_parent"
android:layout_height="@dimen/now_playing_art_height"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:contentDescription="@string/thumbnail"
android:scaleType="centerCrop"/>
<com.melnykov.fab.ObservableScrollView
android:id="@+id/media_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true">
<!-- Album details information -->
<RelativeLayout
android:id="@+id/media_panel_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/remote_content_hmargin"
android:layout_marginRight="@dimen/remote_content_hmargin"
android:transitionGroup="true">
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/media_title_margin_top"
style="@style/TextAppearance.Media.Title"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:background="?attr/contentBackgroundColor"/>
<TextView
android:id="@+id/media_undertitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Subtitle"
android:paddingLeft="@dimen/poster_width_plus_padding"
android:paddingRight="@dimen/default_padding"
android:layout_below="@id/media_title"
android:background="?attr/contentBackgroundColor"/>
<!-- Media actions buttons -->
<LinearLayout
android:id="@+id/media_actions_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/buttonbar_height"
android:layout_below="@id/media_undertitle"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:orientation="horizontal"
style="@style/ButtonBar">
<Space
android:id="@+id/media_actions_bar_space"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/add_to_playlist"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconAddToQueue"
android:contentDescription="@string/add_to_playlist"/>
<ImageButton
android:id="@+id/download"
android:layout_width="@dimen/buttonbar_button_width"
android:layout_height="match_parent"
style="@style/Widget.Button.Borderless"
android:src="?attr/iconDownload"
android:contentDescription="@string/download"/>
</LinearLayout>
<ImageView
android:id="@+id/poster"
android:layout_width="@dimen/albumdetail_poster_width"
android:layout_height="@dimen/albumdetail_poster_heigth"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="@dimen/default_padding"
android:layout_alignBottom="@id/media_undertitle"
android:contentDescription="@string/poster"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/media_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.Media.Info"
android:paddingLeft="@dimen/default_padding"
android:paddingRight="@dimen/default_padding"
android:layout_below="@id/media_actions_bar"
android:background="?attr/contentBackgroundColor"
/>
</RelativeLayout>
</com.melnykov.fab.ObservableScrollView>
<com.melnykov.fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="@dimen/default_padding"
android:layout_marginRight="@dimen/double_padding"
android:src="@drawable/ic_play_arrow_white_24dp"
app:fab_colorNormal="?attr/fabColorNormal"
app:fab_colorPressed="?attr/fabColorPressed"/>
<!-- View that will be shown with the circularReveal when user presses the FAB -->
<View
android:id="@+id/exit_transition_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
</RelativeLayout>

View File

@ -272,6 +272,7 @@
<string name="confirm_movie_download">Are you sure you want to download this movie?\nThe movie will be downloaded in the background, but it can take a while to finish.</string>
<string name="confirm_episode_download">Are you sure you want to download this episode?\nThe episode will be downloaded in the background, but it can take a while to finish.</string>
<string name="confirm_album_download">Are you sure you want to download this album?\nThe album will be downloaded in the background, but it can take a while to finish.</string>
<string name="confirm_artist_download">Are you sure you want to download all albums?\nThe albums will be downloaded in the background, but it can take a while to finish.</string>
<string name="download_file_exists">File already exists.\nDo you want to overwrite it or download with a new name?</string>
<string name="download_dir_exists">Download directory already exists.\nIf any files have the same name, do you want to overwrite them or download them with a new name?</string>