From f0f21d118a522ee1751c43a9d12800bb731967d2 Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Tue, 26 Jan 2016 13:24:01 +0100 Subject: [PATCH] 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 --- .../org/xbmc/kore/provider/MediaContract.java | 11 +- .../org/xbmc/kore/provider/MediaDatabase.java | 11 +- .../org/xbmc/kore/provider/MediaProvider.java | 23 +- .../xbmc/kore/ui/AlbumDetailsFragment.java | 2 +- .../org/xbmc/kore/ui/AlbumListFragment.java | 12 +- .../xbmc/kore/ui/ArtistDetailsFragment.java | 117 +++++ .../org/xbmc/kore/ui/ArtistListFragment.java | 46 +- .../xbmc/kore/ui/ArtistOverviewFragment.java | 481 ++++++++++++++++++ .../java/org/xbmc/kore/ui/MusicActivity.java | 40 +- .../res/layout/fragment_artist_details.xml | 143 ++++++ app/src/main/res/values/strings.xml | 1 + 11 files changed, 839 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java create mode 100644 app/src/main/res/layout/fragment_artist_details.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 10453ce..818b436 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaContract.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaContract.java @@ -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() 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 7083ff1..66bb9e2 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaDatabase.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaDatabase.java @@ -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 = 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 b84b060..e8ba879 100644 --- a/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java +++ b/app/src/main/java/org/xbmc/kore/provider/MediaProvider.java @@ -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; } } diff --git a/app/src/main/java/org/xbmc/kore/ui/AlbumDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/AlbumDetailsFragment.java index 18c135b..f1579b2 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AlbumDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AlbumDetailsFragment.java @@ -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: diff --git a/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java index 89b9257..0ef933e 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AlbumListFragment.java @@ -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); diff --git a/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java new file mode 100644 index 0000000..7d42504 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/ArtistDetailsFragment.java @@ -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; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java b/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java index 64a5dd3..e432034 100644 --- a/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/ArtistListFragment.java @@ -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() { diff --git a/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java b/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java new file mode 100644 index 0000000..174a9f3 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/ArtistOverviewFragment.java @@ -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 { + 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 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 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 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 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() { + @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 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; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/MusicActivity.java b/app/src/main/java/org/xbmc/kore/ui/MusicActivity.java index 65aded4..ac251ed 100644 --- a/app/src/main/java/org/xbmc/kore/ui/MusicActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/MusicActivity.java @@ -94,6 +94,9 @@ public class MusicActivity extends BaseActivity musicListFragment.setExitSharedElementCallback(new SharedElementCallback() { @Override public void onMapSharedElements(List names, Map 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 names, Map 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) diff --git a/app/src/main/res/layout/fragment_artist_details.xml b/app/src/main/res/layout/fragment_artist_details.xml new file mode 100644 index 0000000..0eb7844 --- /dev/null +++ b/app/src/main/res/layout/fragment_artist_details.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05c89db..5ee1b0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -272,6 +272,7 @@ 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. 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. 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. + 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. File already exists.\nDo you want to overwrite it or download with a new name? Download directory already exists.\nIf any files have the same name, do you want to overwrite them or download them with a new name?