package com.simplecity.amp_library.ui.fragments; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.BroadcastReceiver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.SharedElementCallback; import android.support.v4.view.ViewCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.view.ActionMode; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.transition.Transition; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.bignerdranch.android.multiselector.ModalMultiSelectorCallback; import com.bignerdranch.android.multiselector.MultiSelector; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.RequestManager; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.crashlytics.android.core.CrashlyticsCore; import com.simplecity.amp_library.R; import com.simplecity.amp_library.glide.utils.AlwaysCrossFade; import com.simplecity.amp_library.glide.utils.GlideUtils; import com.simplecity.amp_library.model.AdaptableItem; import com.simplecity.amp_library.model.Album; import com.simplecity.amp_library.model.AlbumArtist; import com.simplecity.amp_library.model.Genre; import com.simplecity.amp_library.model.Playlist; import com.simplecity.amp_library.model.Song; import com.simplecity.amp_library.sql.databases.BlacklistHelper; import com.simplecity.amp_library.ui.activities.MainActivity; import com.simplecity.amp_library.ui.adapters.DetailAdapter; import com.simplecity.amp_library.ui.adapters.ItemAdapter; import com.simplecity.amp_library.ui.modelviews.BaseAdaptableItem; import com.simplecity.amp_library.ui.modelviews.DiscNumberView; import com.simplecity.amp_library.ui.modelviews.EmptyView; import com.simplecity.amp_library.ui.modelviews.HorizontalAlbumView; import com.simplecity.amp_library.ui.modelviews.HorizontalRecyclerView; import com.simplecity.amp_library.ui.modelviews.SongView; import com.simplecity.amp_library.ui.modelviews.ViewType; import com.simplecity.amp_library.ui.recyclerview.ItemTouchHelperCallback; import com.simplecity.amp_library.ui.views.NonScrollImageButton; import com.simplecity.amp_library.utils.ColorUtils; import com.simplecity.amp_library.utils.ComparisonUtils; import com.simplecity.amp_library.utils.DataManager; import com.simplecity.amp_library.utils.DialogUtils; import com.simplecity.amp_library.utils.DrawableUtils; import com.simplecity.amp_library.utils.MenuUtils; import com.simplecity.amp_library.utils.MusicUtils; import com.simplecity.amp_library.utils.Operators; import com.simplecity.amp_library.utils.PermissionUtils; import com.simplecity.amp_library.utils.PlaylistUtils; import com.simplecity.amp_library.utils.ResourceUtils; import com.simplecity.amp_library.utils.ShuttleUtils; import com.simplecity.amp_library.utils.SortManager; import com.simplecity.amp_library.utils.StringUtils; import com.simplecity.amp_library.utils.ThemeUtils; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; import rx.subscriptions.CompositeSubscription; public class DetailFragment extends BaseFragment implements MusicUtils.Defs, RecyclerView.RecyclerListener, View.OnClickListener, DetailAdapter.Listener, HorizontalRecyclerView.HorizontalAdapter.ItemListener { private static final String TAG = "DetailFragment"; public static String ARG_MODEL = "model"; private static final String ARG_TRANSITION_NAME = "transition_name"; private AlbumArtist albumArtist; private Album album; private Genre genre; private Playlist playlist; private TextView lineOne; private TextView lineTwo; private NonScrollImageButton overflowButton; private MultiSelector multiSelector = new MultiSelector(); private ActionMode actionMode; boolean inActionMode; private View rootView; private DetailAdapter adapter; private BroadcastReceiver receiver; ImageView headerImageView; private SharedPreferences prefs; private SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener; FloatingActionButton fab; private RecyclerView recyclerView; private ItemTouchHelper itemTouchHelper; private HorizontalRecyclerView horizontalRecyclerView; HeaderView headerItem; private View headerView; private CompositeSubscription subscriptions; private RequestManager requestManager; private Album currentSlideShowAlbum; View textProtectionScrim; float headerTranslation; float headerImageTranslation; private Subscription slideShowObservable; private boolean sortChanged = false; public DetailFragment() { } public static DetailFragment newInstance(Serializable model) { return newInstance(model, null); } public static DetailFragment newInstance(Serializable model, String transitionName) { final DetailFragment fragment = new DetailFragment(); final Bundle args = new Bundle(); args.putSerializable(ARG_MODEL, model); args.putString(ARG_TRANSITION_NAME, transitionName); fragment.setArguments(args); return fragment; } @Override public void onCreate(final Bundle icicle) { super.onCreate(icicle); setHasOptionsMenu(true); setEnterSharedElementCallback(enterSharedElementCallback); prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); Serializable object = getArguments().getSerializable(ARG_MODEL); if (object instanceof AlbumArtist) { albumArtist = (AlbumArtist) object; } else if (object instanceof Album) { album = (Album) object; } else if (object instanceof Genre) { genre = (Genre) object; } else if (object instanceof Playlist) { playlist = (Playlist) object; } if (adapter == null) { adapter = new DetailAdapter(); adapter.setListener(this); } receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction() != null && intent.getAction().equals("restartLoader")) { refreshAdapterItems(); } } }; sharedPreferenceChangeListener = (sharedPreferences, key) -> { if (key.equals("pref_theme_highlight_color") || key.equals("pref_theme_accent_color") || key.equals("pref_theme_white_accent")) { themeUIComponents(); } else if (key.equals("songWhitelist")) { refreshAdapterItems(); } }; prefs.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); if (requestManager == null) { requestManager = Glide.with(this); } if (headerItem == null) { headerItem = new HeaderView(); } if (horizontalRecyclerView == null) { horizontalRecyclerView = new HorizontalRecyclerView(); horizontalRecyclerView.setListener(this); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { rootView = inflater.inflate(R.layout.fragment_detail, container, false); recyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); if (canEdit()) { itemTouchHelper = new ItemTouchHelper(new ItemTouchHelperCallback( (from, to) -> { long songViewCount = Stream.of(adapter.items) .filter(adaptableItem -> adaptableItem instanceof SongView).count(); int offset = (int) (adapter.getItemCount() - songViewCount); if(to >= offset) { adapter.moveItem(from, to); } }, (from, to) -> { // The 'offset' here is the number of items in the list which are not // SongViews. We need this to determine the actual playlist positions of the items. long songViewCount = Stream.of(adapter.items) .filter(adaptableItem -> adaptableItem instanceof SongView).count(); int offset = (int) (adapter.getItemCount() - songViewCount); from -= offset; to -= offset; try { MediaStore.Audio.Playlists.Members.moveItem(getActivity().getContentResolver(), playlist.id, from, to); } catch (IllegalArgumentException e) { CrashlyticsCore.getInstance().log(String.format("Failed to move playlist item from %s to %s. Adapter count: %s. Error:%s", from, to, adapter.getItemCount(), e.getMessage())); } }, null )); itemTouchHelper.attachToRecyclerView(recyclerView); } fab = (FloatingActionButton) rootView.findViewById(R.id.fab); fab.setOnClickListener(this); lineOne = (TextView) rootView.findViewById(R.id.line_one); lineTwo = (TextView) rootView.findViewById(R.id.line_two); overflowButton = (NonScrollImageButton) rootView.findViewById(R.id.btn_overflow); overflowButton.setOnClickListener(this); if (albumArtist != null) { lineOne.setText(albumArtist.name); overflowButton.setContentDescription(getString(R.string.btn_options, albumArtist.name)); } else if (album != null) { lineOne.setText(album.name); overflowButton.setContentDescription(getString(R.string.btn_options, album.name)); } else if (genre != null) { lineOne.setText(genre.name); overflowButton.setVisibility(View.GONE); } else if (playlist != null) { lineOne.setText(playlist.name); overflowButton.setContentDescription(getString(R.string.btn_options, playlist.name)); } textProtectionScrim = rootView.findViewById(R.id.textProtectionScrim); headerImageView = (ImageView) rootView.findViewById(R.id.background); String transitionName = getArguments().getString(ARG_TRANSITION_NAME); ViewCompat.setTransitionName(headerImageView, transitionName); if (transitionName != null) { textProtectionScrim.setVisibility(View.GONE); fab.setVisibility(View.GONE); } int width = ResourceUtils.getScreenSize().width + ResourceUtils.toPixels(60); int height = getResources().getDimensionPixelSize(R.dimen.header_view_height); if (albumArtist != null || album != null) { requestManager .load(albumArtist == null ? album : albumArtist) //Need to override the height/width, as the shared element transition tricks Glide into thinking this ImageView has //the same dimensions as the ImageView that the transition starts with. //So we'll set it to screen width (plus a little extra, which might fix an issue on some devices..) .override(width, height) .diskCacheStrategy(DiskCacheStrategy.SOURCE) .priority(Priority.HIGH) .placeholder(GlideUtils.getPlaceHolderDrawable(albumArtist == null ? album.name : albumArtist.name, false)) .centerCrop() .animate(new AlwaysCrossFade(false)) .into(headerImageView); } actionMode = null; //Set the RecyclerView HeaderView height equal to the headerItem height headerView = rootView.findViewById(R.id.headerView); headerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { headerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); DetailFragment.this.headerItem.height = headerView.getHeight(); } }); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); headerTranslation = headerView.getTranslationY() - dy; headerImageTranslation = headerImageView.getTranslationY() + dy / 2; //Fixes an issue where the image translation gets a little out of sync with //the header translation. if (headerTranslation == 0) { headerImageTranslation = 0; } float ratio = Math.min(1, -headerTranslation / headerView.getHeight()); headerView.setTranslationY(headerTranslation); headerImageView.setTranslationY(headerImageTranslation); //Check to make sure the sliding panel isn't currently expanded or being //dragged. Workaround for issue where the action bar ends up being transparent //when recreating this fragment. if (getActivity() != null) { if (((MainActivity) getActivity()).canSetAlpha()) { ((MainActivity) getActivity()).setActionBarAlpha(ratio, true); } } } }); themeUIComponents(); headerView.setTranslationY(headerTranslation); headerImageView.setTranslationY(headerImageTranslation); return rootView; } private void themeUIComponents() { if (rootView != null) { int themeType = ThemeUtils.getThemeType(getActivity()); if (themeType == ThemeUtils.ThemeType.TYPE_DARK || themeType == ThemeUtils.ThemeType.TYPE_SOLID_DARK) { rootView.setBackgroundColor(getResources().getColor(R.color.bg_dark)); } else if (themeType == ThemeUtils.ThemeType.TYPE_BLACK || themeType == ThemeUtils.ThemeType.TYPE_SOLID_BLACK) { rootView.setBackgroundColor(getResources().getColor(R.color.bg_black)); } else { rootView.setBackgroundColor(getResources().getColor(R.color.bg_light)); } } ThemeUtils.themeRecyclerView(recyclerView); if (fab != null) { fab.setBackgroundTintList(ColorStateList.valueOf(ColorUtils.getAccentColor())); fab.setRippleColor(ColorUtils.darkerise(ColorUtils.getAccentColor(), 0.85f)); } if (overflowButton != null) { overflowButton.setImageDrawable(DrawableUtils.getBaseDrawable(getActivity(), R.drawable.ic_overflow_white)); } recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { ThemeUtils.themeRecyclerView(recyclerView); super.onScrollStateChanged(recyclerView, newState); } }); } @Override public void onResume() { super.onResume(); IntentFilter filter = new IntentFilter(); filter.addAction("restartLoader"); getActivity().registerReceiver(receiver, filter); subscriptions = new CompositeSubscription(); refreshAdapterItems(); } void refreshAdapterItems() { PermissionUtils.RequestStoragePermissions(() -> { if (getActivity() != null && isAdded()) { boolean albumsAscending = getAlbumsAscending(); boolean songsAscending = getSongsAscending(); @SortManager.SongSort int songSort = getSongsSortOrder(); @SortManager.AlbumSort int albumSort = getAlbumsSortOrder(); Observable<List<Song>> observable = null; if (albumArtist != null) { observable = DataManager.getInstance().getSongsRelay() .first() .map(songs -> Stream.of(songs) .filter(song -> Stream.of(albumArtist.albums) .anyMatch(album1 -> album1.id == song.albumId)) .collect(Collectors.toList())); } else if (album != null) { observable = DataManager.getInstance().getSongsRelay() .first() .map(songs -> Stream.of(songs) .filter(song -> song.albumId == album.id) .collect(Collectors.toList())); } else if (genre != null) { observable = genre.getSongsObservable(getContext()); } else if (playlist != null) { observable = playlist.getSongsObservable(getContext()); } subscriptions.add(observable .map(songs -> { songs = Stream.of(songs) .filter(song -> { if (albumArtist != null) { return Stream.of(albumArtist.albums) .anyMatch(album -> album.id == song.albumId); } else if (album != null) { return song.albumId == album.id; } return true; }).collect(Collectors.toList()); List<Album> albums = Stream.of(Operators.songsToAlbums(songs)) .collect(Collectors.toList()); if (playlist != null && albumSort == SortManager.AlbumSort.DEFAULT) { switch (playlist.type) { case Playlist.Type.MOST_PLAYED: Collections.sort(albums, (a, b) -> ComparisonUtils.compareInt(b.songPlayCount, a.songPlayCount)); break; case Playlist.Type.RECENTLY_PLAYED: Collections.sort(albums, (a, b) -> ComparisonUtils.compareLong(b.lastPlayed, a.lastPlayed)); break; case Playlist.Type.RECENTLY_ADDED: Collections.sort(albums, (a, b) -> ComparisonUtils.compareLong(b.dateAdded, a.dateAdded)); break; case Playlist.Type.USER_CREATED: case Playlist.Type.FAVORITES: case Playlist.Type.PODCAST: Collections.sort(albums, (a, b) -> ComparisonUtils.compare(a.name, b.name)); break; } } else { SortManager.getInstance().sortAlbums(albums, albumSort); if (!albumsAscending) { Collections.reverse(albums); } } //If we're not looking at a playlist, or we are, but it's not sorted by 'default', //then we just leave the songs in what ever sort order they came in if (playlist == null || songSort != SortManager.SongSort.DETAIL_DEFAULT) { SortManager.getInstance().sortSongs(songs, songSort); if (!songsAscending) { Collections.reverse(songs); } } if (album == null) { List<AdaptableItem> items = Stream.of(albums) .map(album -> new HorizontalAlbumView(album, requestManager)) .collect(Collectors.toList()); horizontalRecyclerView.setItems(items); } List<AdaptableItem> songViews = Stream.of(songs) .map(song -> { SongView songView = new SongView(song, multiSelector, requestManager); songView.setShowAlbumArt(false); songView.setEditable(canEdit()); songView.setShowTrackNumber(album != null && (songSort == SortManager.SongSort.DETAIL_DEFAULT || songSort == SortManager.SongSort.TRACK_NUMBER)); return songView; }) .collect(Collectors.toList()); if (album != null && album.numDiscs > 1 && (songSort == SortManager.SongSort.DETAIL_DEFAULT || songSort == SortManager.SongSort.TRACK_NUMBER)) { int discNumber = 0; int length = songViews.size(); for (int i = 0; i < length; i++) { SongView songView = (SongView) songViews.get(i); if (discNumber != songView.song.discNumber) { discNumber = songView.song.discNumber; songViews.add(i, new DiscNumberView(discNumber)); } } } List<AdaptableItem> adaptableItems = new ArrayList<>(); adaptableItems.add(headerItem); if (album == null) { adaptableItems.add(horizontalRecyclerView); } adaptableItems.addAll(songViews); return adaptableItems; }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(adaptableItems -> { if (adaptableItems.isEmpty()) { adapter.setEmpty(new EmptyView(R.string.empty_songlist)); } else { if (sortChanged) { //If the sort order has changed, we can't let the RecyclerView calculate the diff and do a nice //animation, as we can't keep track of changes to the scroll position of the recycler view, //so our header translation gets messed up. adapter.items.clear(); adapter.items = adaptableItems; adapter.notifyDataSetChanged(); recyclerView.smoothScrollToPosition(0); sortChanged = false; } else { adapter.setItems(adaptableItems); } } int numSongs = (int) Stream.of(adaptableItems) .filter(value -> value instanceof SongView) .count(); int numAlbums = horizontalRecyclerView.getCount(); if (lineTwo != null) { if (albumArtist != null) { lineTwo.setText(StringUtils.makeAlbumAndSongsLabel(getActivity(), numAlbums, albumArtist.getNumSongs())); } else if (album != null) { lineTwo.setText(String.format("%s%s", album.getAlbumArtist().name, numSongs > 0 ? " | " + album.getNumSongsLabel() : "")); } else if (genre != null) { lineTwo.setText(StringUtils.makeAlbumAndSongsLabel(getActivity(), numAlbums, numSongs)); } else if (playlist != null) { lineTwo.setText(StringUtils.makeAlbumAndSongsLabel(getActivity(), numAlbums, numSongs)); } } //Start the slideshow observable. List<Album> albums = Stream.of(adaptableItems) .filter(adaptableItem -> adaptableItem instanceof SongView) .map(songView -> (Song) songView.getItem()) .map(Song::getAlbum) .distinct() .collect(Collectors.toList()); if (playlist != null || genre != null && !albums.isEmpty()) { if (slideShowObservable != null && !slideShowObservable.isUnsubscribed()) { slideShowObservable.unsubscribe(); } slideShowObservable = Observable.interval(8, TimeUnit.SECONDS) .onBackpressureDrop() .startWith(0L) .map(aLong -> { if (albums.isEmpty() || aLong == 0L && currentSlideShowAlbum != null) { //This is our first emission since onResume, but not our first since view creation. //Skip this load. return null; } return albums.get(new Random().nextInt(albums.size())); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(nextSlideShowAlbum -> { if (nextSlideShowAlbum != null && nextSlideShowAlbum != currentSlideShowAlbum) { //This crazy business is what's required to have a smooth Glide crossfade with no 'white flicker' requestManager .load(nextSlideShowAlbum) .diskCacheStrategy(DiskCacheStrategy.SOURCE) .priority(Priority.HIGH) .error(GlideUtils.getPlaceHolderDrawable(nextSlideShowAlbum.name, true)) .centerCrop() .thumbnail(Glide .with(this) .load(currentSlideShowAlbum) .centerCrop()) .animate(new AlwaysCrossFade(false)) .into(headerImageView); currentSlideShowAlbum = nextSlideShowAlbum; } }); subscriptions.add(slideShowObservable); } })); } }); } @Override public void onPause() { if (receiver != null) { getActivity().unregisterReceiver(receiver); } subscriptions.unsubscribe(); super.onPause(); } @Override public void onDestroy() { prefs.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); super.onDestroy(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); //If we've already inflated artist, playlist or genre sorting items by now, then we're looking //at at the albums detail screen. We need to remove the duplicate sorting menu items. MenuItem artistSortItem = menu.findItem(R.id.artist_sort); if (artistSortItem != null) { artistSortItem.setVisible(false); } if (albumArtist != null || genre != null || playlist != null) { inflater.inflate(R.menu.menu_sort_detail, menu); } else if (album != null) { inflater.inflate(R.menu.menu_sort_detail_album, menu); } } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); //Songs switch (getSongsSortOrder()) { case SortManager.SongSort.DETAIL_DEFAULT: menu.findItem(R.id.sort_song_default).setChecked(true); break; case SortManager.SongSort.NAME: menu.findItem(R.id.sort_song_name).setChecked(true); break; case SortManager.SongSort.TRACK_NUMBER: menu.findItem(R.id.sort_song_track_number).setChecked(true); break; case SortManager.SongSort.DURATION: menu.findItem(R.id.sort_song_duration).setChecked(true); break; case SortManager.SongSort.DATE: menu.findItem(R.id.sort_song_date).setChecked(true); break; case SortManager.SongSort.YEAR: menu.findItem(R.id.sort_song_year).setChecked(true); break; case SortManager.SongSort.ALBUM_NAME: menu.findItem(R.id.sort_song_album_name).setChecked(true); break; case SortManager.SongSort.ARTIST_NAME: menu.findItem(R.id.sort_song_artist_name).setChecked(true); break; } menu.findItem(R.id.sort_songs_ascending).setChecked(getSongsAscending()); if (albumArtist != null || playlist != null || genre != null) { switch (getAlbumsSortOrder()) { case SortManager.AlbumSort.DEFAULT: menu.findItem(R.id.sort_album_default).setChecked(true); break; case SortManager.AlbumSort.NAME: menu.findItem(R.id.sort_album_name).setChecked(true); break; case SortManager.AlbumSort.YEAR: menu.findItem(R.id.sort_album_year).setChecked(true); break; case SortManager.AlbumSort.ARTIST_NAME: menu.findItem(R.id.sort_album_artist_name).setChecked(true); break; } menu.findItem(R.id.sort_albums_ascending).setChecked(getAlbumsAscending()); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { //Songs case R.id.sort_song_default: setSongsSortOrder(SortManager.SongSort.DETAIL_DEFAULT); sortChanged = true; break; case R.id.sort_song_name: setSongsSortOrder(SortManager.SongSort.NAME); sortChanged = true; break; case R.id.sort_song_track_number: setSongsSortOrder(SortManager.SongSort.TRACK_NUMBER); sortChanged = true; break; case R.id.sort_song_duration: setSongsSortOrder(SortManager.SongSort.DURATION); sortChanged = true; break; case R.id.sort_song_year: setSongsSortOrder(SortManager.SongSort.YEAR); sortChanged = true; break; case R.id.sort_song_date: setSongsSortOrder(SortManager.SongSort.DATE); sortChanged = true; break; case R.id.sort_song_album_name: setSongsSortOrder(SortManager.SongSort.ALBUM_NAME); sortChanged = true; break; case R.id.sort_songs_ascending: setSongsAscending(!item.isChecked()); sortChanged = true; break; //Albums case R.id.sort_album_default: setAlbumsOrder(SortManager.AlbumSort.DEFAULT); sortChanged = true; break; case R.id.sort_album_name: setAlbumsOrder(SortManager.AlbumSort.NAME); sortChanged = true; break; case R.id.sort_album_year: setAlbumsOrder(SortManager.AlbumSort.YEAR); sortChanged = true; break; case R.id.sort_albums_ascending: setAlbumsAscending(!item.isChecked()); sortChanged = true; break; } if (sortChanged) { refreshAdapterItems(); } getActivity().supportInvalidateOptionsMenu(); return super.onOptionsItemSelected(item); } private void setSongsSortOrder(@SortManager.SongSort int sortOrder) { if (playlist != null) { SortManager.getInstance().setDetailPlaylistSongsSortOrder(sortOrder); } else if (genre != null) { SortManager.getInstance().setDetailGenreSongsSortOrder(sortOrder); } else if (album != null) { SortManager.getInstance().setDetailAlbumSongsSortOrder(sortOrder); } else { SortManager.getInstance().setDetailSongsSortOrder(sortOrder); } } @SortManager.SongSort private int getSongsSortOrder() { if (playlist != null) { return SortManager.getInstance().getDetailPlaylistSongsSortOrder(); } else if (genre != null) { return SortManager.getInstance().getDetailGenreSongsSortOrder(); } else if (album != null) { return SortManager.getInstance().getDetailAlbumSongsSortOrder(); } else { return SortManager.getInstance().getDetailSongsSortOrder(); } } private void setSongsAscending(boolean ascending) { if (playlist != null) { SortManager.getInstance().setDetailPlaylistSongsAscending(ascending); } else if (genre != null) { SortManager.getInstance().setDetailGenreSongsAscending(ascending); } else { SortManager.getInstance().setDetailSongsAscending(ascending); } } private boolean getSongsAscending() { if (playlist != null) { return SortManager.getInstance().getDetailPlaylistSongsAscending(); } else if (genre != null) { return SortManager.getInstance().getDetailGenreSongsAscending(); } else { return SortManager.getInstance().getDetailSongsAscending(); } } private void setAlbumsOrder(@SortManager.AlbumSort int sortOrder) { if (playlist != null) { SortManager.getInstance().setDetailPlaylistAlbumsSortOrder(sortOrder); } else if (genre != null) { SortManager.getInstance().setDetailGenreAlbumsSortOrder(sortOrder); } else { SortManager.getInstance().setDetailAlbumsSortOrder(sortOrder); } } @SortManager.AlbumSort private int getAlbumsSortOrder() { if (playlist != null) { return SortManager.getInstance().getDetailPlaylistAlbumsSortOrder(); } else if (genre != null) { return SortManager.getInstance().getDetailGenreAlbumsSortOrder(); } else { return SortManager.getInstance().getDetailAlbumsSortOrder(); } } private void setAlbumsAscending(boolean ascending) { if (playlist != null) { SortManager.getInstance().setDetailPlaylistAlbumsAscending(ascending); } else if (genre != null) { SortManager.getInstance().setDetailGenreAlbumsAscending(ascending); } else { SortManager.getInstance().setDetailAlbumsAscending(ascending); } } private boolean getAlbumsAscending() { if (playlist != null) { return SortManager.getInstance().getDetailPlaylistAlbumsAscending(); } else if (genre != null) { return SortManager.getInstance().getDetailGenreAlbumsAscending(); } else { return SortManager.getInstance().getDetailAlbumsAscending(); } } boolean canEdit() { return playlist != null && playlist.canEdit && getSongsSortOrder() == SortManager.SongSort.DETAIL_DEFAULT; } void onCreateActionMode(Menu menu) { ThemeUtils.themeContextualActionBar(getActivity()); inActionMode = true; getActivity().getMenuInflater().inflate(R.menu.context_menu_songs, menu); final SubMenu sub = menu.getItem(0).getSubMenu(); PlaylistUtils.makePlaylistMenu(getActivity(), sub, SONG_FRAGMENT_GROUP_ID); } boolean onActionItemClicked(MenuItem item) { final ArrayList<Song> checkedSongs = getCheckedSongs(); if (checkedSongs == null || checkedSongs.isEmpty()) { return true; } switch (item.getItemId()) { case NEW_PLAYLIST: PlaylistUtils.createPlaylistDialog(getActivity(), checkedSongs); return true; case PLAYLIST_SELECTED: Playlist playlist = (Playlist) item.getIntent().getSerializableExtra(ShuttleUtils.ARG_PLAYLIST); PlaylistUtils.addToPlaylist(getContext(), playlist, checkedSongs); return true; case R.id.delete: new DialogUtils.DeleteDialogBuilder() .context(getContext()) .singleMessageId(R.string.delete_song_desc) .multipleMessage(R.string.delete_song_desc_multiple) .itemNames(Stream.of(checkedSongs) .map(song -> song.name) .collect(Collectors.toList())) .songsToDelete(Observable.just(checkedSongs)) .build() .show(); return true; case R.id.menu_add_to_queue: MusicUtils.addToQueue(getActivity(), checkedSongs); return true; } return false; } void onDestroyActionMode() { actionMode = null; inActionMode = false; multiSelector.clearSelections(); } private static final class SelectorCallback extends ModalMultiSelectorCallback { private final WeakReference<DetailFragment> mParent; SelectorCallback(DetailFragment parent) { super(null); mParent = new WeakReference<>(parent); } @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { final DetailFragment parent = mParent.get(); if (parent != null) { parent.onCreateActionMode(menu); return true; } return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { final DetailFragment parent = mParent.get(); if (parent != null) { if (parent.onActionItemClicked(item)) { mode.finish(); return true; } } return false; } @Override public void onDestroyActionMode(ActionMode actionMode) { super.onDestroyActionMode(actionMode); final DetailFragment parent = mParent.get(); if (parent != null) { parent.onDestroyActionMode(); } } } ArrayList<Song> getCheckedSongs() { return Stream.of(multiSelector.getSelectedPositions()) .map(i -> ((SongView) adapter.items.get(i)).song) .collect(Collectors.toCollection(ArrayList::new)); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.fab: if (albumArtist != null) { MusicUtils.shuffleAll(getActivity(), albumArtist.getSongsObservable()); } else if (album != null) { MusicUtils.shuffleAll(getActivity(), album.getSongsObservable()); } else if (genre != null) { MusicUtils.shuffleAll(getActivity(), genre.getSongsObservable(getContext())); } else if (playlist != null) { MusicUtils.shuffleAll(getActivity(), playlist.getSongsObservable(getContext())); } break; case R.id.btn_overflow: final PopupMenu menu = new PopupMenu(getActivity(), v); if (album != null) { MenuUtils.addAlbumMenuOptions(getActivity(), menu); MenuUtils.addClickHandler((AppCompatActivity) getActivity(), menu, album); menu.getMenu().add(ALBUM_FRAGMENT_GROUP_ID, VIEW_INFO, Menu.NONE, R.string.info); } else if (albumArtist != null) { MenuUtils.addAlbumArtistMenuOptions(getActivity(), menu); MenuUtils.addClickHandler((AppCompatActivity) getActivity(), menu, albumArtist); menu.getMenu().add(ALBUM_FRAGMENT_GROUP_ID, VIEW_INFO, Menu.NONE, R.string.info); } else if (genre != null) { } else if (playlist != null) { MenuUtils.addPlaylistMenuOptions(menu, playlist); //Remove the delete menu option, since we're looking at the playlist we would delete. if (menu.getMenu().findItem(MusicUtils.PlaylistMenuOrder.DELETE_PLAYLIST) != null) { menu.getMenu().removeItem(MusicUtils.PlaylistMenuOrder.DELETE_PLAYLIST); } MenuUtils.addClickHandler(getActivity(), menu, playlist, (materialDialog, dialogAction) -> { //The user might have changed the playlist name lineOne.setText(playlist.name); }, (materialDialog, dialogAction) -> { //If the user clicked 'edit', they've probably set a new 'week' range. Restart the loader. refreshAdapterItems(); }); } menu.show(); break; } } @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { if (holder.getAdapterPosition() != -1) { adapter.items.get(holder.getAdapterPosition()).recycle(holder); } } @Override public void onItemClick(View v, int position, Song song) { if (inActionMode) { multiSelector.setSelected(position, adapter.getItemId(position), !multiSelector.isSelected(position, adapter.getItemId(position))); if (multiSelector.getSelectedPositions().isEmpty()) { if (actionMode != null) { actionMode.finish(); } } } else { List<Song> songs = Stream.of(adapter.items) .filter(adaptableItem -> adaptableItem instanceof SongView) .map(viewHolderAdaptableItem -> ((SongView) viewHolderAdaptableItem).song) .collect(Collectors.toList()); MusicUtils.playAll(songs, songs.indexOf(song), () -> { final String message = getContext().getString(R.string.emptyplaylist); Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); }); } } @Override public void onOverflowClick(View v, final int position, final Song song) { PopupMenu menu = new PopupMenu(getActivity(), v); MenuUtils.addSongMenuOptions(getActivity(), menu); if (playlist != null && playlist.canEdit) { menu.getMenu().add(SONG_FRAGMENT_GROUP_ID, REMOVE, 10, R.string.remove_from_playlist); } MenuUtils.addClickHandler((AppCompatActivity) getActivity(), menu, song, item -> { switch (item.getItemId()) { case BLACKLIST: adapter.removeItem(position); BlacklistHelper.addToBlacklist(song); return true; case REMOVE: if (playlist != null) { adapter.removeItem(position); Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlist.id); getActivity().getContentResolver().delete(ContentUris.withAppendedId(uri, song.playlistSongId), null, null); } } return false; }); menu.show(); } @Override public void onLongClick(View v, int position, Song song) { if (inActionMode) { return; } final SelectorCallback callback = new SelectorCallback(this); callback.setMultiSelector(multiSelector); if (multiSelector.getSelectedPositions().isEmpty()) { actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(callback); inActionMode = true; } multiSelector.setSelected(position, adapter.getItemId(position), !multiSelector.isSelected(position, adapter.getItemId(position))); } @Override public void onStartDrag(RecyclerView.ViewHolder viewHolder) { if (itemTouchHelper != null) { itemTouchHelper.startDrag(viewHolder); } } @Override public void onItemClick(ItemAdapter adapter, View v, int position, Object item) { if (inActionMode) { if (actionMode != null) { actionMode.finish(); } } ((MainActivity) getActivity()).swapFragments((Album) item, v.findViewById(R.id.image)); } @Override public void onOverflowClick(View v, int position, Object item) { PopupMenu menu = new PopupMenu(getActivity(), v); MenuUtils.addAlbumMenuOptions(getActivity(), menu); MenuUtils.addClickHandler((AppCompatActivity) getActivity(), menu, (Album) item); menu.show(); } private static class HeaderView extends BaseAdaptableItem { public int height = ResourceUtils.toPixels(350); HeaderView() { } @Override public int getViewType() { return ViewType.DETAIL_HEADER; } @Override public int getLayoutResId() { return -1; } @Override public void bindView(RecyclerView.ViewHolder holder) { holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)); } @Override public void bindView(RecyclerView.ViewHolder holder, int position, List payloads) { } @Override public RecyclerView.ViewHolder getViewHolder(ViewGroup parent) { FrameLayout headerView = new FrameLayout(parent.getContext()); //Set the headerItem layout params.. Arbitrary height, this will be adjusted via the //ViewTreeObserver in onCreateView headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)); return new ViewHolder(headerView); } @Override public Object getItem() { return null; } private static class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(View itemView) { super(itemView); } @Override public String toString() { return "HeaderView.ViewHolder"; } } } @Override public void setSharedElementEnterTransition(Object transition) { super.setSharedElementEnterTransition(transition); if (ShuttleUtils.hasLollipop()) { ((Transition) transition).addListener(getSharedElementEnterTransitionListenerAdapter()); } } SharedElementCallback enterSharedElementCallback = new SharedElementCallback() { @Override public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) { super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots); if (fab != null) { fab.setVisibility(View.GONE); } } }; private TransitionListenerAdapter getSharedElementEnterTransitionListenerAdapter() { if (ShuttleUtils.hasLollipop()) { return new TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { if (ShuttleUtils.hasLollipop()) { //Todo: //This is a partial fix for an issue where the initial artwork load tricks Glide into thinking the final //image in the transition has the dimensions of the initial image. //The idea is we would call this to force Glide to load a full res image, and we wouldn't need to use the //override call in our initial Glide load in onCreateView. //Unfortunately, because the initial image is square, but our header image is not, the transition isn't very nice.. //So for now, we'll stick with overriding the initial image dimensions.. // if (isAdded()) { // requestManager // .load(albumArtist == null ? album : albumArtist) // .diskCacheStrategy(DiskCacheStrategy.SOURCE) // .priority(Priority.HIGH) // .centerCrop() // .thumbnail(Glide // .with(DetailFragment.this) // .load(albumArtist == null ? album : albumArtist) // .override(headerImageView.getDrawable().getIntrinsicWidth(), headerImageView.getDrawable().getIntrinsicHeight()) // .centerCrop()) // .animate(new AlwaysCrossFade(false)) // .into(headerImageView); // } transition.removeListener(this); //Fade in the text protection scrim textProtectionScrim.setAlpha(0f); textProtectionScrim.setVisibility(View.VISIBLE); ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(textProtectionScrim, View.ALPHA, 0f, 1f); fadeAnimator.setDuration(800); fadeAnimator.start(); //Fade & grow the FAB fab.setAlpha(0f); fab.setVisibility(View.VISIBLE); fadeAnimator = ObjectAnimator.ofFloat(fab, View.ALPHA, 0.5f, 1f); ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(fab, View.SCALE_X, 0f, 1f); ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(fab, View.SCALE_Y, 0f, 1f); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setInterpolator(new OvershootInterpolator(2f)); animatorSet.playTogether(fadeAnimator, scaleXAnimator, scaleYAnimator); animatorSet.setDuration(250); animatorSet.start(); } } }; } return null; } @Override protected String screenName() { return TAG; } }